diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index 403e97569..5cbb2d82b 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -2163,6 +2163,21 @@ NoResetTalentsCost = 0 ToggleXP.Cost = 100000 +# +# SpellQueue.Enabled +# Description: Enable SpellQueue. +# Default: 0 - (Disabled) +# 1 - (Enabled) + +SpellQueue.Enabled = 1 + +# +# SpellQueue.Window +# Description: Time (in milliseconds) for spells to be queued. +# Default: 400 - (400ms) + +SpellQueue.Window = 400 + # ################################################################################################### diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index b93d86a03..887600406 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -1026,6 +1026,9 @@ void Player::setDeathState(DeathState s, bool /*despawn = false*/) return; } + // clear all pending spell cast requests when dying + SpellQueue.clear(); + // drunken state is cleared on death SetDrunkValue(0); // lost combo points at any target (targeted combo points clear in Unit::setDeathState) diff --git a/src/server/game/Entities/Player/Player.h b/src/server/game/Entities/Player/Player.h index 778ae0a83..6637a7697 100644 --- a/src/server/game/Entities/Player/Player.h +++ b/src/server/game/Entities/Player/Player.h @@ -1060,6 +1060,18 @@ struct EntryPointData [[nodiscard]] bool HasTaxiPath() const { return taxiPath[0] && taxiPath[1]; } }; +struct PendingSpellCastRequest +{ + uint32 spellId; + uint32 category; + WorldPacket requestPacket; + bool isItem = false; + bool cancelInProgress = false; + + PendingSpellCastRequest(uint32 spellId, uint32 category, WorldPacket&& packet, bool item = false, bool cancel = false) + : spellId(spellId), category(category), requestPacket(std::move(packet)), isItem(item) , cancelInProgress(cancel) {} +}; + class Player : public Unit, public GridObject { friend class WorldSession; @@ -2615,7 +2627,21 @@ public: std::string GetDebugInfo() const override; - protected: + /*********************************************************/ + /*** SPELL QUEUE SYSTEM ***/ + /*********************************************************/ +protected: + uint32 GetSpellQueueWindow() const; + void ProcessSpellQueue(); + +public: + std::deque SpellQueue; + const PendingSpellCastRequest* GetCastRequest(uint32 category) const; + bool CanExecutePendingSpellCastRequest(SpellInfo const* spellInfo); + void ExecuteOrCancelSpellCastRequest(PendingSpellCastRequest* castRequest, bool isCancel = false); + bool CanRequestSpellCast(SpellInfo const* spellInfo); + +protected: // Gamemaster whisper whitelist WhisperListContainer WhisperList; diff --git a/src/server/game/Entities/Player/PlayerUpdates.cpp b/src/server/game/Entities/Player/PlayerUpdates.cpp index d17321348..fc9eb7570 100644 --- a/src/server/game/Entities/Player/PlayerUpdates.cpp +++ b/src/server/game/Entities/Player/PlayerUpdates.cpp @@ -79,6 +79,7 @@ void Player::Update(uint32 p_time) // used to implement delayed far teleports SetMustDelayTeleport(true); + ProcessSpellQueue(); Unit::Update(p_time); SetMustDelayTeleport(false); @@ -2255,3 +2256,89 @@ void Player::ProcessTerrainStatusUpdate() else m_MirrorTimerFlags &= ~(UNDERWATER_INWATER | UNDERWATER_INLAVA | UNDERWATER_INSLIME | UNDERWATER_INDARKWATER); } + +uint32 Player::GetSpellQueueWindow() const +{ + return sWorld->getIntConfig(CONFIG_SPELL_QUEUE_WINDOW); +} + +bool Player::CanExecutePendingSpellCastRequest(SpellInfo const* spellInfo) +{ + if (GetGlobalCooldownMgr().HasGlobalCooldown(spellInfo)) + return false; + + if (GetSpellCooldownDelay(spellInfo->Id) > GetSpellQueueWindow()) + return false; + + for (CurrentSpellTypes spellSlot : {CURRENT_MELEE_SPELL, CURRENT_GENERIC_SPELL}) + if (Spell* spell = GetCurrentSpell(spellSlot)) + { + bool autoshot = spell->m_spellInfo->IsAutoRepeatRangedSpell(); + if (IsNonMeleeSpellCast(false, true, true, autoshot)) + return false; + } + return true; +} + +const PendingSpellCastRequest* Player::GetCastRequest(uint32 category) const +{ + for (const PendingSpellCastRequest& request : SpellQueue) + if (request.category == category) + return &request; + return nullptr; +} + +bool Player::CanRequestSpellCast(SpellInfo const* spellInfo) +{ + if (!sWorld->getBoolConfig(CONFIG_SPELL_QUEUE_ENABLED)) + return false; + + // Check for existing cast request with the same category + if (GetCastRequest(spellInfo->StartRecoveryCategory)) + return false; + + if (GetGlobalCooldownMgr().GetGlobalCooldown(spellInfo) > GetSpellQueueWindow()) + return false; + + if (GetSpellCooldownDelay(spellInfo->Id) > GetSpellQueueWindow()) + return false; + + // If there is an existing cast that will last longer than the allowable + // spell queue window, then we can't request a new spell cast + for (CurrentSpellTypes spellSlot : { CURRENT_MELEE_SPELL, CURRENT_GENERIC_SPELL }) + if (Spell* spell = GetCurrentSpell(spellSlot)) + if (spell->GetCastTimeRemaining() > static_cast(GetSpellQueueWindow())) + return false; + + return true; +} + +void Player::ExecuteOrCancelSpellCastRequest(PendingSpellCastRequest* request, bool isCancel /* = false*/) +{ + if (isCancel) + request->cancelInProgress = true; + + if (WorldSession* session = GetSession()) + { + if (request->isItem) + session->HandleUseItemOpcode(request->requestPacket); + else + session->HandleCastSpellOpcode(request->requestPacket); + } +} + +void Player::ProcessSpellQueue() +{ + while (!SpellQueue.empty()) + { + PendingSpellCastRequest& request = SpellQueue.front(); // Peek at the first spell + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(request.spellId); + if (CanExecutePendingSpellCastRequest(spellInfo)) + { + ExecuteOrCancelSpellCastRequest(&request); + SpellQueue.pop_front(); // Remove from the queue + } + else // If the first spell can't execute, stop processing + break; + } +} diff --git a/src/server/game/Entities/Unit/CharmInfo.cpp b/src/server/game/Entities/Unit/CharmInfo.cpp index 8a4c6d761..6a7cde11a 100644 --- a/src/server/game/Entities/Unit/CharmInfo.cpp +++ b/src/server/game/Entities/Unit/CharmInfo.cpp @@ -400,6 +400,22 @@ bool GlobalCooldownMgr::HasGlobalCooldown(SpellInfo const* spellInfo) const return itr != m_GlobalCooldowns.end() && itr->second.duration && getMSTimeDiff(itr->second.cast_time, GameTime::GetGameTimeMS().count()) < itr->second.duration; } +uint32 GlobalCooldownMgr::GetGlobalCooldown(SpellInfo const* spellInfo) const +{ + if (!spellInfo) + return 0; + + auto itr = m_GlobalCooldowns.find(spellInfo->StartRecoveryCategory); + if (itr == m_GlobalCooldowns.end() || itr->second.duration == 0) + return 0; + + uint32 start = itr->second.cast_time; + uint32 delay = itr->second.duration; + uint32 now = getMSTime(); + + return (start + delay > now) ? (start + delay) - now : 0; +} + void GlobalCooldownMgr::AddGlobalCooldown(SpellInfo const* spellInfo, uint32 gcd) { m_GlobalCooldowns[spellInfo->StartRecoveryCategory] = GlobalCooldown(gcd, GameTime::GetGameTimeMS().count()); diff --git a/src/server/game/Entities/Unit/CharmInfo.h b/src/server/game/Entities/Unit/CharmInfo.h index 9efc7e32a..1b22d5fac 100644 --- a/src/server/game/Entities/Unit/CharmInfo.h +++ b/src/server/game/Entities/Unit/CharmInfo.h @@ -83,6 +83,7 @@ public: public: bool HasGlobalCooldown(SpellInfo const* spellInfo) const; + uint32 GetGlobalCooldown(SpellInfo const* spellInfo) const; void AddGlobalCooldown(SpellInfo const* spellInfo, uint32 gcd); void CancelGlobalCooldown(SpellInfo const* spellInfo); diff --git a/src/server/game/Handlers/SpellHandler.cpp b/src/server/game/Handlers/SpellHandler.cpp index 3bf376688..7f11d413f 100644 --- a/src/server/game/Handlers/SpellHandler.cpp +++ b/src/server/game/Handlers/SpellHandler.cpp @@ -85,6 +85,41 @@ void WorldSession::HandleUseItemOpcode(WorldPacket& recvPacket) return; } + SpellInfo const* spellInfo = sSpellMgr->GetSpellInfo(spellId); + + if (!spellInfo) + { + LOG_ERROR("network.opcode", "WORLD: unknown spell id {}", spellId); + recvPacket.rfinish(); // prevent spam at ignore packet + return; + } + + // fail if we are cancelling pending request + if (!_player->SpellQueue.empty()) + { + PendingSpellCastRequest& request = _player->SpellQueue.front(); // Peek at the first spell + if (request.cancelInProgress) + { + pUser->SendEquipError(EQUIP_ERR_NONE, pItem, nullptr); + recvPacket.rfinish(); // prevent spam at ignore packet + return; + } + } + + // try queue spell if it can't be executed right now + if (!_player->CanExecutePendingSpellCastRequest(spellInfo)) + if (_player->CanRequestSpellCast(spellInfo)) + { + recvPacket.rpos(0); // Reset read position to the start of the buffer. + _player->SpellQueue.emplace_back( + spellId, + spellInfo->GetCategory(), + std::move(recvPacket), // Move ownership of recvPacket + true // itemCast + ); + return; + } + if (pItem->GetGUID() != itemGUID) { pUser->SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, nullptr, nullptr); @@ -341,6 +376,10 @@ void WorldSession::HandleCastSpellOpcode(WorldPacket& recvPacket) { uint32 spellId; uint8 castCount, castFlags; + + if (recvPacket.empty()) + return; + recvPacket >> castCount >> spellId >> castFlags; TriggerCastFlags triggerFlag = TRIGGERED_NONE; @@ -365,6 +404,36 @@ void WorldSession::HandleCastSpellOpcode(WorldPacket& recvPacket) return; } + // fail if we are cancelling pending request + if (!_player->SpellQueue.empty()) + { + PendingSpellCastRequest& request = _player->SpellQueue.front(); // Peek at the first spell + if (request.cancelInProgress) + { + Spell* spell = new Spell(_player, spellInfo, TRIGGERED_NONE); + spell->m_cast_count = castCount; + spell->SendCastResult(SPELL_FAILED_DONT_REPORT); + spell->finish(false); + recvPacket.rfinish(); // prevent spam at ignore packet + return; + } + } + + // try queue spell if it can't be executed right now + if (!_player->CanExecutePendingSpellCastRequest(spellInfo)) + { + if (_player->CanRequestSpellCast(spellInfo)) + { + recvPacket.rpos(0); // Reset read position to the start of the buffer. + _player->SpellQueue.emplace_back( + spellId, + spellInfo->GetCategory(), + std::move(recvPacket) // Move ownership of recvPacket + ); + return; + } + } + // client provided targets SpellCastTargets targets; targets.Read(recvPacket, mover); @@ -483,6 +552,8 @@ void WorldSession::HandleCancelCastOpcode(WorldPacket& recvPacket) recvPacket.read_skip(); // counter, increments with every CANCEL packet, don't use for now recvPacket >> spellId; + _player->SpellQueue.clear(); + _player->InterruptSpell(CURRENT_MELEE_SPELL); if (_player->IsNonMeleeSpellCast(false)) _player->InterruptNonMeleeSpells(false, spellId, false, true); diff --git a/src/server/game/Spells/Spell.h b/src/server/game/Spells/Spell.h index 4d44af2e4..be8ff1802 100644 --- a/src/server/game/Spells/Spell.h +++ b/src/server/game/Spells/Spell.h @@ -551,6 +551,7 @@ public: bool IsAutoRepeat() const { return m_autoRepeat; } void SetAutoRepeat(bool rep) { m_autoRepeat = rep; } void ReSetTimer() { m_timer = m_casttime > 0 ? m_casttime : 0; } + int32 GetCastTimeRemaining() { return m_timer;} bool IsNextMeleeSwingSpell() const; bool IsTriggered() const { return HasTriggeredCastFlag(TRIGGERED_FULL_MASK); }; bool HasTriggeredCastFlag(TriggerCastFlags flag) const { return _triggeredCastFlags & flag; }; diff --git a/src/server/game/World/IWorld.h b/src/server/game/World/IWorld.h index 1263de1a5..cfcea0d63 100644 --- a/src/server/game/World/IWorld.h +++ b/src/server/game/World/IWorld.h @@ -183,6 +183,7 @@ enum WorldBoolConfigs CONFIG_ALLOWS_RANK_MOD_FOR_PET_HEALTH, CONFIG_MUNCHING_BLIZZLIKE, CONFIG_ENABLE_DAZE, + CONFIG_SPELL_QUEUE_ENABLED, BOOL_CONFIG_VALUE_COUNT }; @@ -419,6 +420,7 @@ enum WorldIntConfigs CONFIG_WATER_BREATH_TIMER, CONFIG_AUCTION_HOUSE_SEARCH_TIMEOUT, CONFIG_DAILY_RBG_MIN_LEVEL_AP_REWARD, + CONFIG_SPELL_QUEUE_WINDOW, INT_CONFIG_VALUE_COUNT }; diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index 5955ec6b5..5618ae3f8 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -1487,6 +1487,10 @@ void World::LoadConfigSettings(bool reload) // Realm Availability _bool_configs[CONFIG_REALM_LOGIN_ENABLED] = sConfigMgr->GetOption("World.RealmAvailability", true); + // SpellQueue + _bool_configs[CONFIG_SPELL_QUEUE_ENABLED] = sConfigMgr->GetOption("SpellQueue.Enabled", true); + _int_configs[CONFIG_SPELL_QUEUE_WINDOW] = sConfigMgr->GetOption("SpellQueue.Window", 400); + // call ScriptMgr if we're reloading the configuration sScriptMgr->OnAfterConfigLoad(reload); }