aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOvahlord <dreadkiller@gmx.de>2023-11-25 00:00:10 +0100
committerfunjoker <funjoker109@gmail.com>2023-12-01 23:29:27 +0100
commit79b2ff56fa210614a38d96ae359843e12f20b358 (patch)
tree5e4dbccf62b04b644aa3aad19b9701956aa3fabd
parent477bd4b0d7b37aa53289ad53652875831d66b885 (diff)
Core/Spells: Implement spell queue (#29409)
(cherry picked from commit 27019a62a4294f8dd48d975f85b1907c5adee0e3)
-rw-r--r--src/server/game/Entities/Player/Player.cpp267
-rw-r--r--src/server/game/Entities/Player/Player.h14
-rw-r--r--src/server/game/Handlers/SpellHandler.cpp195
-rw-r--r--src/server/game/Server/Packets/SpellPackets.h8
-rw-r--r--src/server/game/Server/Protocol/Opcodes.cpp2
-rw-r--r--src/server/game/Server/WorldSession.h2
-rw-r--r--src/server/game/Spells/Spell.h1
-rw-r--r--src/server/game/Spells/SpellCastRequest.h43
-rw-r--r--src/server/game/Spells/SpellHistory.cpp16
-rw-r--r--src/server/game/Spells/SpellHistory.h1
10 files changed, 388 insertions, 161 deletions
diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp
index 531a103cc30..7bac3c5a9e2 100644
--- a/src/server/game/Entities/Player/Player.cpp
+++ b/src/server/game/Entities/Player/Player.cpp
@@ -110,6 +110,7 @@
#include "Spell.h"
#include "SpellAuraEffects.h"
#include "SpellAuras.h"
+#include "SpellCastRequest.h"
#include "SpellHistory.h"
#include "SpellMgr.h"
#include "SpellPackets.h"
@@ -938,6 +939,10 @@ void Player::Update(uint32 p_time)
Unit::Update(p_time);
SetCanDelayTeleport(false);
+ // Unit::Update updates the spell history and spell states. We can now check if we can launch another pending cast.
+ if (CanExecutePendingSpellCastRequest())
+ ExecutePendingSpellCastRequest();
+
time_t now = GameTime::GetGameTime();
UpdatePvPFlag(now);
@@ -1238,6 +1243,9 @@ void Player::setDeathState(DeathState s)
return;
}
+ // clear all pending spell cast requests when dying
+ CancelPendingCastRequest();
+
// drunken state is cleared on death
SetDrunkValue(0);
SetPower(POWER_COMBO_POINTS, 0);
@@ -28921,3 +28929,262 @@ uint32 TraitMgr::PlayerDataAccessor::GetPrimarySpecialization() const
{
return AsUnderlyingType(_player->GetPrimarySpecialization());
}
+
+void Player::RequestSpellCast(std::unique_ptr<SpellCastRequest> castRequest)
+{
+ // We are overriding an already existing spell cast request so inform the client that the old cast is being replaced
+ if (_pendingSpellCastRequest)
+ CancelPendingCastRequest();
+
+ _pendingSpellCastRequest = std::move(castRequest);
+
+ // If we can process the cast request right now, do it.
+ if (CanExecutePendingSpellCastRequest())
+ ExecutePendingSpellCastRequest();
+}
+
+void Player::CancelPendingCastRequest()
+{
+ if (!_pendingSpellCastRequest)
+ return;
+
+ // We have to inform the client that the cast has been canceled. Otherwise the cast button will remain highlightened
+ WorldPackets::Spells::CastFailed castFailed;
+ castFailed.CastID = _pendingSpellCastRequest->CastRequest.CastID;
+ castFailed.SpellID = _pendingSpellCastRequest->CastRequest.SpellID;
+ castFailed.Reason = SPELL_FAILED_DONT_REPORT;
+ SendDirectMessage(castFailed.Write());
+
+ _pendingSpellCastRequest = nullptr;
+}
+
+// A spell can be queued up within 400 milliseconds before global cooldown expires or the cast finishes
+static constexpr Milliseconds SPELL_QUEUE_TIME_WINDOW = 400ms;
+
+bool Player::CanRequestSpellCast(SpellInfo const* spellInfo, Unit const* castingUnit) const
+{
+ if (castingUnit->GetSpellHistory()->GetRemainingGlobalCooldown(spellInfo) > SPELL_QUEUE_TIME_WINDOW)
+ return false;
+
+ for (CurrentSpellTypes spellSlot : { CURRENT_MELEE_SPELL, CURRENT_GENERIC_SPELL })
+ if (Spell const* spell = GetCurrentSpell(spellSlot))
+ if (Milliseconds(spell->GetRemainingCastTime()) > SPELL_QUEUE_TIME_WINDOW)
+ return false;
+
+ return true;
+}
+
+void Player::ExecutePendingSpellCastRequest()
+{
+ if (!_pendingSpellCastRequest)
+ return;
+
+ TriggerCastFlags triggerFlag = TRIGGERED_NONE;
+
+ Unit* castingUnit = _pendingSpellCastRequest->CastingUnitGUID == GetGUID() ? this : ObjectAccessor::GetUnit(*this, _pendingSpellCastRequest->CastingUnitGUID);
+
+ // client provided targets
+ SpellCastTargets targets(castingUnit, _pendingSpellCastRequest->CastRequest);
+
+ // The spell cast has been requested by using an item. Handle the cast accordingly.
+ if (_pendingSpellCastRequest->ItemData.has_value())
+ {
+ if (ProcessItemCast(*_pendingSpellCastRequest, targets))
+ _pendingSpellCastRequest = nullptr;
+ else
+ CancelPendingCastRequest();
+ return;
+ }
+
+ // check known spell or raid marker spell (which not requires player to know it)
+ SpellInfo const* spellInfo = sSpellMgr->AssertSpellInfo(_pendingSpellCastRequest->CastRequest.SpellID, GetMap()->GetDifficultyID());
+ Player* plrCaster = castingUnit->ToPlayer();
+ if (plrCaster && !plrCaster->HasActiveSpell(spellInfo->Id) && !spellInfo->HasAttribute(SPELL_ATTR8_SKIP_IS_KNOWN_CHECK))
+ {
+ bool allow = false;
+
+ // allow casting of unknown spells for special lock cases
+ if (GameObject* go = targets.GetGOTarget())
+ if (go->GetSpellForLock(plrCaster) == spellInfo)
+ allow = true;
+
+ // allow casting of spells triggered by clientside periodic trigger auras
+ if (castingUnit->HasAuraTypeWithTriggerSpell(SPELL_AURA_PERIODIC_TRIGGER_SPELL_FROM_CLIENT, spellInfo->Id))
+ {
+ allow = true;
+ triggerFlag = TRIGGERED_FULL_MASK;
+ }
+
+ if (!allow)
+ {
+ CancelPendingCastRequest();
+ return;
+ }
+ }
+
+ // Check possible spell cast overrides
+ spellInfo = castingUnit->GetCastSpellInfo(spellInfo, triggerFlag);
+ if (spellInfo->IsPassive())
+ {
+ CancelPendingCastRequest();
+ return;
+ }
+
+ // can't use our own spells when we're in possession of another unit
+ if (isPossessing())
+ {
+ CancelPendingCastRequest();
+ return;
+ }
+
+ // Client is resending autoshot cast opcode when other spell is cast during shoot rotation
+ // Skip it to prevent "interrupt" message
+ // Also check targets! target may have changed and we need to interrupt current spell
+ if (spellInfo->IsAutoRepeatRangedSpell())
+ {
+ if (Spell* spell = castingUnit->GetCurrentSpell(CURRENT_AUTOREPEAT_SPELL))
+ {
+ if (spell->m_spellInfo == spellInfo && spell->m_targets.GetUnitTargetGUID() == targets.GetUnitTargetGUID())
+ {
+ CancelPendingCastRequest();
+ return;
+ }
+ }
+ }
+
+ // auto-selection buff level base at target level (in spellInfo)
+ if (targets.GetUnitTarget())
+ {
+ SpellInfo const* actualSpellInfo = spellInfo->GetAuraRankForLevel(targets.GetUnitTarget()->GetLevelForTarget(this));
+
+ // if rank not found then function return NULL but in explicit cast case original spell can be cast and later failed with appropriate error message
+ if (actualSpellInfo)
+ spellInfo = actualSpellInfo;
+ }
+
+ Spell* spell = new Spell(castingUnit, spellInfo, triggerFlag);
+
+ WorldPackets::Spells::SpellPrepare spellPrepare;
+ spellPrepare.ClientCastID = _pendingSpellCastRequest->CastRequest.CastID;
+ spellPrepare.ServerCastID = spell->m_castId;
+ SendDirectMessage(spellPrepare.Write());
+
+ spell->m_fromClient = true;
+ spell->m_misc.Raw.Data[0] = _pendingSpellCastRequest->CastRequest.Misc[0];
+ spell->m_misc.Raw.Data[1] = _pendingSpellCastRequest->CastRequest.Misc[1];
+ spell->prepare(targets);
+
+ _pendingSpellCastRequest = nullptr;
+}
+
+bool Player::ProcessItemCast(SpellCastRequest& castRequest, SpellCastTargets const& targets)
+{
+ Item* item = GetUseableItemByPos(castRequest.ItemData->PackSlot, castRequest.ItemData->Slot);
+ if (!item)
+ {
+ SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, nullptr, nullptr);
+ return false;
+ }
+
+ if (item->GetGUID() != castRequest.ItemData->CastItem)
+ {
+ SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, nullptr, nullptr);
+ return false;
+ }
+
+ ItemTemplate const* proto = item->GetTemplate();
+ if (!proto)
+ {
+ SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, item, nullptr);
+ return false;
+ }
+
+ // some item classes can be used only in equipped state
+ if (proto->GetInventoryType() != INVTYPE_NON_EQUIP && !item->IsEquipped())
+ {
+ SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, item, nullptr);
+ return false;
+ }
+
+ InventoryResult msg = CanUseItem(item);
+ if (msg != EQUIP_ERR_OK)
+ {
+ SendEquipError(msg, item, nullptr);
+ return false;
+ }
+
+ // only allow conjured consumable, bandage, poisons (all should have the 2^21 item flag set in DB)
+ if (proto->GetClass() == ITEM_CLASS_CONSUMABLE && !proto->HasFlag(ITEM_FLAG_IGNORE_DEFAULT_ARENA_RESTRICTIONS) && InArena())
+ {
+ SendEquipError(EQUIP_ERR_NOT_DURING_ARENA_MATCH, item, nullptr);
+ return false;
+ }
+
+ // don't allow items banned in arena
+ if (proto->HasFlag(ITEM_FLAG_NOT_USEABLE_IN_ARENA) && InArena())
+ {
+ SendEquipError(EQUIP_ERR_NOT_DURING_ARENA_MATCH, item, nullptr);
+ return false;
+ }
+
+ if (IsInCombat())
+ {
+ for (ItemEffectEntry const* effect : item->GetEffects())
+ {
+ if (SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(effect->SpellID, GetMap()->GetDifficultyID()))
+ {
+ if (!spellInfo->CanBeUsedInCombat(this))
+ {
+ SendEquipError(EQUIP_ERR_NOT_IN_COMBAT, item, nullptr);
+ return false;
+ }
+ }
+ }
+ }
+
+ // check also BIND_ON_ACQUIRE and BIND_QUEST for .additem or .additemset case by GM (not binded at adding to inventory)
+ if (item->GetBonding() == BIND_ON_USE || item->GetBonding() == BIND_ON_ACQUIRE || item->GetBonding() == BIND_QUEST)
+ {
+ if (!item->IsSoulBound())
+ {
+ item->SetState(ITEM_CHANGED, this);
+ item->SetBinding(true);
+ GetSession()->GetCollectionMgr()->AddItemAppearance(item);
+ }
+ }
+
+ RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::ItemUse);
+
+ // Note: If script stop casting it must send appropriate data to client to prevent stuck item in gray state.
+ if (!sScriptMgr->OnItemUse(this, item, targets, castRequest.CastRequest.CastID))
+ {
+ // no script or script not process request by self
+ CastItemUseSpell(item, targets, castRequest.CastRequest.CastID, castRequest.CastRequest.Misc);
+ }
+
+ return true;
+}
+
+bool Player::CanExecutePendingSpellCastRequest()
+{
+ if (!_pendingSpellCastRequest)
+ return false;
+
+ Unit const* castingUnit = _pendingSpellCastRequest->CastingUnitGUID == GetGUID() ? this : ObjectAccessor::GetUnit(*this, _pendingSpellCastRequest->CastingUnitGUID);
+ if (!castingUnit || !castingUnit->IsInWorld() || (castingUnit != this && GetUnitBeingMoved() != castingUnit))
+ {
+ // If the casting unit is no longer available, just cancel the entire spell cast request and be done with it
+ CancelPendingCastRequest();
+ return false;
+ }
+
+ // Generic and melee spells have to wait, channeled spells can be processed immediately.
+ if (!castingUnit->GetCurrentSpell(CURRENT_CHANNELED_SPELL) && castingUnit->HasUnitState(UNIT_STATE_CASTING))
+ return false;
+
+ // Waiting for the global cooldown to expire before attempting to execute the cast request
+ if (castingUnit->GetSpellHistory()->GetRemainingGlobalCooldown(sSpellMgr->AssertSpellInfo(_pendingSpellCastRequest->CastRequest.SpellID, GetMap()->GetDifficultyID())) > 0ms)
+ return false;
+
+ return true;
+}
diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h
index b4a32eba905..e15f22fdd7a 100644
--- a/src/server/game/Entities/Player/Player.h
+++ b/src/server/game/Entities/Player/Player.h
@@ -57,6 +57,7 @@ struct PvpTalentEntry;
struct QuestPackageItemEntry;
struct RewardPackEntry;
struct SkillRaceClassInfoEntry;
+struct SpellCastRequest;
struct TalentEntry;
struct TrainerSpell;
struct TransferAbortParams;
@@ -3147,6 +3148,19 @@ class TC_GAME_API Player : public Unit, public GridObject<Player>
bool _usePvpItemLevels;
ObjectGuid _areaSpiritHealerGUID;
+
+ // Spell cast request handling
+ public:
+ // Queues up a spell cast request that has been received via packet and processes it whenever possible.
+ void RequestSpellCast(std::unique_ptr<SpellCastRequest> castRequest);
+ void CancelPendingCastRequest();
+ bool CanRequestSpellCast(SpellInfo const* spell, Unit const* castingUnit) const;
+
+ private:
+ std::unique_ptr<SpellCastRequest> _pendingSpellCastRequest;
+ void ExecutePendingSpellCastRequest();
+ bool ProcessItemCast(SpellCastRequest& castRequest, SpellCastTargets const& targets);
+ bool CanExecutePendingSpellCastRequest();
};
TC_GAME_API void AddItemsSetItem(Player* player, Item const* item);
diff --git a/src/server/game/Handlers/SpellHandler.cpp b/src/server/game/Handlers/SpellHandler.cpp
index 48f51f4632c..1204a90755a 100644
--- a/src/server/game/Handlers/SpellHandler.cpp
+++ b/src/server/game/Handlers/SpellHandler.cpp
@@ -36,6 +36,7 @@
#include "ScriptMgr.h"
#include "Spell.h"
#include "SpellAuraEffects.h"
+#include "SpellCastRequest.h"
#include "SpellMgr.h"
#include "SpellPackets.h"
#include "TemporarySummon.h"
@@ -44,96 +45,22 @@
void WorldSession::HandleUseItemOpcode(WorldPackets::Spells::UseItem& packet)
{
- Player* user = _player;
-
// ignore for remote control state
- if (user->GetUnitBeingMoved() != user)
- return;
-
- Item* item = user->GetUseableItemByPos(packet.PackSlot, packet.Slot);
- if (!item)
- {
- user->SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, nullptr, nullptr);
- return;
- }
-
- if (item->GetGUID() != packet.CastItem)
- {
- user->SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, nullptr, nullptr);
- return;
- }
-
- ItemTemplate const* proto = item->GetTemplate();
- if (!proto)
- {
- user->SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, item, nullptr);
- return;
- }
-
- // some item classes can be used only in equipped state
- if (proto->GetInventoryType() != INVTYPE_NON_EQUIP && !item->IsEquipped())
- {
- user->SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, item, nullptr);
- return;
- }
-
- InventoryResult msg = user->CanUseItem(item);
- if (msg != EQUIP_ERR_OK)
- {
- user->SendEquipError(msg, item, nullptr);
+ if (_player->GetUnitBeingMoved() != _player)
return;
- }
- // only allow conjured consumable, bandage, poisons (all should have the 2^21 item flag set in DB)
- if (proto->GetClass() == ITEM_CLASS_CONSUMABLE && !proto->HasFlag(ITEM_FLAG_IGNORE_DEFAULT_ARENA_RESTRICTIONS) && user->InArena())
- {
- user->SendEquipError(EQUIP_ERR_NOT_DURING_ARENA_MATCH, item, nullptr);
- return;
- }
-
- // don't allow items banned in arena
- if (proto->HasFlag(ITEM_FLAG_NOT_USEABLE_IN_ARENA) && user->InArena())
+ // Skip casting invalid spells right away
+ SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(packet.Cast.SpellID, _player->GetMap()->GetDifficultyID());
+ if (!spellInfo)
{
- user->SendEquipError(EQUIP_ERR_NOT_DURING_ARENA_MATCH, item, nullptr);
+ TC_LOG_ERROR("network", "WorldSession::HandleUseItemOpcode: attempted to cast a non-existing spell (Id: {})", packet.Cast.SpellID);
return;
}
- if (user->IsInCombat())
- {
- for (ItemEffectEntry const* effect : item->GetEffects())
- {
- if (SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(effect->SpellID, user->GetMap()->GetDifficultyID()))
- {
- if (!spellInfo->CanBeUsedInCombat(user))
- {
- user->SendEquipError(EQUIP_ERR_NOT_IN_COMBAT, item, nullptr);
- return;
- }
- }
- }
- }
-
- // check also BIND_ON_ACQUIRE and BIND_QUEST for .additem or .additemset case by GM (not binded at adding to inventory)
- if (item->GetBonding() == BIND_ON_USE || item->GetBonding() == BIND_ON_ACQUIRE || item->GetBonding() == BIND_QUEST)
- {
- if (!item->IsSoulBound())
- {
- item->SetState(ITEM_CHANGED, user);
- item->SetBinding(true);
- GetCollectionMgr()->AddItemAppearance(item);
- }
- }
-
- user->RemoveAurasWithInterruptFlags(SpellAuraInterruptFlags::ItemUse);
-
- SpellCastTargets targets(user, packet.Cast);
-
- // Note: If script stop casting it must send appropriate data to client to prevent stuck item in gray state.
- if (!sScriptMgr->OnItemUse(user, item, targets, packet.Cast.CastID))
- {
- // no script or script not process request by self
- user->CastItemUseSpell(item, targets, packet.Cast.CastID, packet.Cast.Misc);
- }
+ if (_player->CanRequestSpellCast(spellInfo, _player))
+ _player->RequestSpellCast(std::make_unique<SpellCastRequest>(std::move(packet.Cast), _player->GetGUID(), SpellCastRequestItemData(packet.PackSlot, packet.Slot, packet.CastItem)));
+ else
+ Spell::SendCastResult(_player, spellInfo, {}, packet.Cast.CastID, SPELL_FAILED_SPELL_IN_PROGRESS);
}
void WorldSession::HandleOpenItemOpcode(WorldPackets::Spells::OpenItem& packet)
@@ -299,103 +226,46 @@ void WorldSession::HandleGameobjectReportUse(WorldPackets::GameObject::GameObjRe
void WorldSession::HandleCastSpellOpcode(WorldPackets::Spells::CastSpell& cast)
{
- // ignore for remote control state (for player case)
- Unit* mover = _player->GetUnitBeingMoved();
- if (mover != _player && mover->GetTypeId() == TYPEID_PLAYER)
- return;
-
- SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(cast.Cast.SpellID, mover->GetMap()->GetDifficultyID());
+ // Skip casting invalid spells right away
+ SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(cast.Cast.SpellID, _player->GetMap()->GetDifficultyID());
if (!spellInfo)
{
- TC_LOG_ERROR("network", "WORLD: unknown spell id {}", cast.Cast.SpellID);
+ TC_LOG_ERROR("network", "WorldSession::HandleCastSpellOpcode: attempted to cast a non-existing spell (Id: {})", cast.Cast.SpellID);
return;
}
- Unit* caster = mover;
- if (caster->GetTypeId() == TYPEID_UNIT && !caster->ToCreature()->HasSpell(spellInfo->Id))
+ // ignore for remote control state (for player case)
+ Unit* mover = _player->GetUnitBeingMoved();
+ if (mover != _player && mover->GetTypeId() == TYPEID_PLAYER)
+ return;
+
+ Unit* castingUnit = mover;
+ if (castingUnit->IsCreature() && !castingUnit->ToCreature()->HasSpell(spellInfo->Id))
{
// If the vehicle creature does not have the spell but it allows the passenger to cast own spells
// change caster to player and let him cast
- if (!_player->IsOnVehicle(caster) || spellInfo->CheckVehicle(_player) != SPELL_CAST_OK)
+ if (!_player->IsOnVehicle(castingUnit) || spellInfo->CheckVehicle(_player) != SPELL_CAST_OK)
return;
- caster = _player;
+ castingUnit = _player;
}
- TriggerCastFlags triggerFlag = TRIGGERED_NONE;
-
- // client provided targets
- SpellCastTargets targets(caster, cast.Cast);
-
- // check known spell or raid marker spell (which not requires player to know it)
- if (caster->GetTypeId() == TYPEID_PLAYER && !caster->ToPlayer()->HasActiveSpell(spellInfo->Id) && !spellInfo->HasAttribute(SPELL_ATTR8_SKIP_IS_KNOWN_CHECK))
- {
- bool allow = false;
-
- // allow casting of unknown spells for special lock cases
- if (GameObject* go = targets.GetGOTarget())
- if (go->GetSpellForLock(caster->ToPlayer()) == spellInfo)
- allow = true;
-
- // allow casting of spells triggered by clientside periodic trigger auras
- if (caster->HasAuraTypeWithTriggerSpell(SPELL_AURA_PERIODIC_TRIGGER_SPELL_FROM_CLIENT, spellInfo->Id))
- {
- allow = true;
- triggerFlag = TRIGGERED_FULL_MASK;
- }
-
- if (!allow)
- return;
- }
-
- // Check possible spell cast overrides
- spellInfo = caster->GetCastSpellInfo(spellInfo, triggerFlag);
-
- if (spellInfo->IsPassive())
- return;
-
- // can't use our own spells when we're in possession of another unit,
- if (_player->isPossessing())
- return;
-
- // Client is resending autoshot cast opcode when other spell is cast during shoot rotation
- // Skip it to prevent "interrupt" message
- // Also check targets! target may have changed and we need to interrupt current spell
- if (spellInfo->IsAutoRepeatRangedSpell())
- if (Spell* spell = caster->GetCurrentSpell(CURRENT_AUTOREPEAT_SPELL))
- if (spell->m_spellInfo == spellInfo && spell->m_targets.GetUnitTargetGUID() == targets.GetUnitTargetGUID())
- return;
-
- // auto-selection buff level base at target level (in spellInfo)
- if (targets.GetUnitTarget())
- {
- SpellInfo const* actualSpellInfo = spellInfo->GetAuraRankForLevel(targets.GetUnitTarget()->GetLevelForTarget(caster));
-
- // if rank not found then function return NULL but in explicit cast case original spell can be cast and later failed with appropriate error message
- if (actualSpellInfo)
- spellInfo = actualSpellInfo;
- }
-
- if (cast.Cast.MoveUpdate)
+ if (cast.Cast.MoveUpdate.has_value())
HandleMovementOpcode(CMSG_MOVE_STOP, *cast.Cast.MoveUpdate);
- Spell* spell = new Spell(caster, spellInfo, triggerFlag);
-
- WorldPackets::Spells::SpellPrepare spellPrepare;
- spellPrepare.ClientCastID = cast.Cast.CastID;
- spellPrepare.ServerCastID = spell->m_castId;
- SendPacket(spellPrepare.Write());
-
- spell->m_fromClient = true;
- spell->m_misc.Raw.Data[0] = cast.Cast.Misc[0];
- spell->m_misc.Raw.Data[1] = cast.Cast.Misc[1];
- spell->prepare(targets);
+ if (_player->CanRequestSpellCast(spellInfo, castingUnit))
+ _player->RequestSpellCast(std::make_unique<SpellCastRequest>(std::move(cast.Cast), castingUnit->GetGUID()));
+ else
+ Spell::SendCastResult(_player, spellInfo, {}, cast.Cast.CastID, SPELL_FAILED_SPELL_IN_PROGRESS);
}
void WorldSession::HandleCancelCastOpcode(WorldPackets::Spells::CancelCast& packet)
{
if (_player->IsNonMeleeSpellCast(false))
+ {
_player->InterruptNonMeleeSpells(false, packet.SpellID, false);
+ _player->CancelPendingCastRequest(); // canceling casts also cancels pending spell cast requests
+ }
}
void WorldSession::HandleCancelAuraOpcode(WorldPackets::Spells::CancelAura& cancelAura)
@@ -495,6 +365,11 @@ void WorldSession::HandleCancelAutoRepeatSpellOpcode(WorldPackets::Spells::Cance
_player->InterruptSpell(CURRENT_AUTOREPEAT_SPELL);
}
+void WorldSession::HandleCancelQueuedSpellOpcode(WorldPackets::Spells::CancelQueuedSpell& /*cancelQueuedSpell*/)
+{
+ _player->CancelPendingCastRequest();
+}
+
void WorldSession::HandleCancelChanneling(WorldPackets::Spells::CancelChannelling& cancelChanneling)
{
// ignore for remote control state (for player case)
diff --git a/src/server/game/Server/Packets/SpellPackets.h b/src/server/game/Server/Packets/SpellPackets.h
index 84d00a08f36..dcffa9458a7 100644
--- a/src/server/game/Server/Packets/SpellPackets.h
+++ b/src/server/game/Server/Packets/SpellPackets.h
@@ -1052,6 +1052,14 @@ namespace WorldPackets
uint16 OverrideID = 0;
};
+ class CancelQueuedSpell final : public ClientPacket
+ {
+ public:
+ CancelQueuedSpell(WorldPacket&& packet) : ClientPacket(CMSG_CANCEL_QUEUED_SPELL, std::move(packet)) { }
+
+ void Read() override { }
+ };
+
ByteBuffer& operator>>(ByteBuffer& buffer, SpellCastRequest& request);
}
}
diff --git a/src/server/game/Server/Protocol/Opcodes.cpp b/src/server/game/Server/Protocol/Opcodes.cpp
index 9575891718f..6ee3b9b353f 100644
--- a/src/server/game/Server/Protocol/Opcodes.cpp
+++ b/src/server/game/Server/Protocol/Opcodes.cpp
@@ -278,7 +278,7 @@ void OpcodeTable::Initialize()
DEFINE_HANDLER(CMSG_CANCEL_MASTER_LOOT_ROLL, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
DEFINE_HANDLER(CMSG_CANCEL_MOD_SPEED_NO_CONTROL_AURAS, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleCancelModSpeedNoControlAuras);
DEFINE_HANDLER(CMSG_CANCEL_MOUNT_AURA, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleCancelMountAuraOpcode);
- DEFINE_HANDLER(CMSG_CANCEL_QUEUED_SPELL, STATUS_UNHANDLED, PROCESS_INPLACE, &WorldSession::Handle_NULL);
+ DEFINE_HANDLER(CMSG_CANCEL_QUEUED_SPELL, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleCancelQueuedSpellOpcode);
DEFINE_HANDLER(CMSG_CANCEL_TEMP_ENCHANTMENT, STATUS_LOGGEDIN, PROCESS_INPLACE, &WorldSession::HandleCancelTempEnchantmentOpcode);
DEFINE_HANDLER(CMSG_CANCEL_TRADE, STATUS_LOGGEDIN_OR_RECENTLY_LOGGOUT, PROCESS_THREADUNSAFE, &WorldSession::HandleCancelTradeOpcode);
DEFINE_HANDLER(CMSG_CAN_DUEL, STATUS_LOGGEDIN, PROCESS_THREADUNSAFE, &WorldSession::HandleCanDuel);
diff --git a/src/server/game/Server/WorldSession.h b/src/server/game/Server/WorldSession.h
index 296eda86e61..bf297c185d4 100644
--- a/src/server/game/Server/WorldSession.h
+++ b/src/server/game/Server/WorldSession.h
@@ -699,6 +699,7 @@ namespace WorldPackets
class CancelGrowthAura;
class CancelMountAura;
class CancelModSpeedNoControlAuras;
+ class CancelQueuedSpell;
class PetCancelAura;
class CancelCast;
class CastSpell;
@@ -1500,6 +1501,7 @@ class TC_GAME_API WorldSession
void HandleCancelMountAuraOpcode(WorldPackets::Spells::CancelMountAura& cancelMountAura);
void HandleCancelModSpeedNoControlAuras(WorldPackets::Spells::CancelModSpeedNoControlAuras& cancelModSpeedNoControlAuras);
void HandleCancelAutoRepeatSpellOpcode(WorldPackets::Spells::CancelAutoRepeatSpell& cancelAutoRepeatSpell);
+ void HandleCancelQueuedSpellOpcode(WorldPackets::Spells::CancelQueuedSpell& cancelQueuedSpell);
void HandleMissileTrajectoryCollision(WorldPackets::Spells::MissileTrajectoryCollision& packet);
void HandleUpdateMissileTrajectory(WorldPackets::Spells::UpdateMissileTrajectory& packet);
diff --git a/src/server/game/Spells/Spell.h b/src/server/game/Spells/Spell.h
index ad87c90f21b..ccdb50c57fe 100644
--- a/src/server/game/Spells/Spell.h
+++ b/src/server/game/Spells/Spell.h
@@ -598,6 +598,7 @@ class TC_GAME_API Spell
UsedSpellMods m_appliedMods;
int32 GetCastTime() const { return m_casttime; }
+ int32 GetRemainingCastTime() const { return m_timer; }
bool IsAutoRepeat() const { return m_autoRepeat; }
void SetAutoRepeat(bool rep) { m_autoRepeat = rep; }
void ReSetTimer() { m_timer = m_casttime > 0 ? m_casttime : 0; }
diff --git a/src/server/game/Spells/SpellCastRequest.h b/src/server/game/Spells/SpellCastRequest.h
new file mode 100644
index 00000000000..f78be8314db
--- /dev/null
+++ b/src/server/game/Spells/SpellCastRequest.h
@@ -0,0 +1,43 @@
+/*
+ * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+ * more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef SpellCastRequest_h__
+#define SpellCastRequest_h__
+
+#include "SpellPackets.h"
+
+struct SpellCastRequestItemData
+{
+ SpellCastRequestItemData(uint8 packSlot, uint8 slot, ObjectGuid castItem) :
+ PackSlot(packSlot), Slot(slot), CastItem(castItem) { }
+
+ uint8 PackSlot = 0;
+ uint8 Slot = 0;
+ ObjectGuid CastItem;
+};
+
+struct SpellCastRequest
+{
+ SpellCastRequest(WorldPackets::Spells::SpellCastRequest&& castRequest, ObjectGuid castingUnitGUID, Optional<SpellCastRequestItemData> itemData = {}) :
+ CastRequest(castRequest), CastingUnitGUID(castingUnitGUID), ItemData(itemData) { }
+
+ WorldPackets::Spells::SpellCastRequest CastRequest;
+ ObjectGuid CastingUnitGUID;
+ Optional<SpellCastRequestItemData> ItemData;
+};
+
+#endif // SpellCastRequest_h__
diff --git a/src/server/game/Spells/SpellHistory.cpp b/src/server/game/Spells/SpellHistory.cpp
index 934345b137d..7e090101530 100644
--- a/src/server/game/Spells/SpellHistory.cpp
+++ b/src/server/game/Spells/SpellHistory.cpp
@@ -950,6 +950,22 @@ void SpellHistory::CancelGlobalCooldown(SpellInfo const* spellInfo)
_globalCooldowns[spellInfo->StartRecoveryCategory] = Clock::time_point(Clock::duration(0));
}
+SpellHistory::Duration SpellHistory::GetRemainingGlobalCooldown(SpellInfo const* spellInfo) const
+{
+ Clock::time_point end;
+ auto cdItr = _globalCooldowns.find(spellInfo->StartRecoveryCategory);
+ if (cdItr == _globalCooldowns.end())
+ return Duration::zero();
+
+ end = cdItr->second;
+ Clock::time_point now = GameTime::GetTime<Clock>();
+ if (end < now)
+ return Duration::zero();
+
+ Clock::duration remaining = end - now;
+ return std::chrono::duration_cast<std::chrono::milliseconds>(remaining);
+}
+
Player* SpellHistory::GetPlayerOwner() const
{
return _owner->GetCharmerOrOwnerPlayerOrPlayerItself();
diff --git a/src/server/game/Spells/SpellHistory.h b/src/server/game/Spells/SpellHistory.h
index f15a51c95c5..0e86193ef02 100644
--- a/src/server/game/Spells/SpellHistory.h
+++ b/src/server/game/Spells/SpellHistory.h
@@ -171,6 +171,7 @@ public:
bool HasGlobalCooldown(SpellInfo const* spellInfo) const;
void AddGlobalCooldown(SpellInfo const* spellInfo, Duration duration);
void CancelGlobalCooldown(SpellInfo const* spellInfo);
+ Duration GetRemainingGlobalCooldown(SpellInfo const* spellInfo) const;
void SaveCooldownStateBeforeDuel();
void RestoreCooldownStateAfterDuel();