feat(Core/Player): implement Spell Queue (#20797)
This commit is contained in:
@@ -2163,6 +2163,21 @@ NoResetTalentsCost = 0
|
|||||||
|
|
||||||
ToggleXP.Cost = 100000
|
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
|
||||||
|
|
||||||
#
|
#
|
||||||
###################################################################################################
|
###################################################################################################
|
||||||
|
|
||||||
|
|||||||
@@ -1026,6 +1026,9 @@ void Player::setDeathState(DeathState s, bool /*despawn = false*/)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear all pending spell cast requests when dying
|
||||||
|
SpellQueue.clear();
|
||||||
|
|
||||||
// drunken state is cleared on death
|
// drunken state is cleared on death
|
||||||
SetDrunkValue(0);
|
SetDrunkValue(0);
|
||||||
// lost combo points at any target (targeted combo points clear in Unit::setDeathState)
|
// lost combo points at any target (targeted combo points clear in Unit::setDeathState)
|
||||||
|
|||||||
@@ -1060,6 +1060,18 @@ struct EntryPointData
|
|||||||
[[nodiscard]] bool HasTaxiPath() const { return taxiPath[0] && taxiPath[1]; }
|
[[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<Player>
|
class Player : public Unit, public GridObject<Player>
|
||||||
{
|
{
|
||||||
friend class WorldSession;
|
friend class WorldSession;
|
||||||
@@ -2615,6 +2627,20 @@ public:
|
|||||||
|
|
||||||
std::string GetDebugInfo() const override;
|
std::string GetDebugInfo() const override;
|
||||||
|
|
||||||
|
/*********************************************************/
|
||||||
|
/*** SPELL QUEUE SYSTEM ***/
|
||||||
|
/*********************************************************/
|
||||||
|
protected:
|
||||||
|
uint32 GetSpellQueueWindow() const;
|
||||||
|
void ProcessSpellQueue();
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::deque<PendingSpellCastRequest> 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:
|
protected:
|
||||||
// Gamemaster whisper whitelist
|
// Gamemaster whisper whitelist
|
||||||
WhisperListContainer WhisperList;
|
WhisperListContainer WhisperList;
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ void Player::Update(uint32 p_time)
|
|||||||
|
|
||||||
// used to implement delayed far teleports
|
// used to implement delayed far teleports
|
||||||
SetMustDelayTeleport(true);
|
SetMustDelayTeleport(true);
|
||||||
|
ProcessSpellQueue();
|
||||||
Unit::Update(p_time);
|
Unit::Update(p_time);
|
||||||
SetMustDelayTeleport(false);
|
SetMustDelayTeleport(false);
|
||||||
|
|
||||||
@@ -2255,3 +2256,89 @@ void Player::ProcessTerrainStatusUpdate()
|
|||||||
else
|
else
|
||||||
m_MirrorTimerFlags &= ~(UNDERWATER_INWATER | UNDERWATER_INLAVA | UNDERWATER_INSLIME | UNDERWATER_INDARKWATER);
|
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<int32>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
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)
|
void GlobalCooldownMgr::AddGlobalCooldown(SpellInfo const* spellInfo, uint32 gcd)
|
||||||
{
|
{
|
||||||
m_GlobalCooldowns[spellInfo->StartRecoveryCategory] = GlobalCooldown(gcd, GameTime::GetGameTimeMS().count());
|
m_GlobalCooldowns[spellInfo->StartRecoveryCategory] = GlobalCooldown(gcd, GameTime::GetGameTimeMS().count());
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ public:
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
bool HasGlobalCooldown(SpellInfo const* spellInfo) const;
|
bool HasGlobalCooldown(SpellInfo const* spellInfo) const;
|
||||||
|
uint32 GetGlobalCooldown(SpellInfo const* spellInfo) const;
|
||||||
void AddGlobalCooldown(SpellInfo const* spellInfo, uint32 gcd);
|
void AddGlobalCooldown(SpellInfo const* spellInfo, uint32 gcd);
|
||||||
void CancelGlobalCooldown(SpellInfo const* spellInfo);
|
void CancelGlobalCooldown(SpellInfo const* spellInfo);
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,41 @@ void WorldSession::HandleUseItemOpcode(WorldPacket& recvPacket)
|
|||||||
return;
|
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)
|
if (pItem->GetGUID() != itemGUID)
|
||||||
{
|
{
|
||||||
pUser->SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, nullptr, nullptr);
|
pUser->SendEquipError(EQUIP_ERR_ITEM_NOT_FOUND, nullptr, nullptr);
|
||||||
@@ -341,6 +376,10 @@ void WorldSession::HandleCastSpellOpcode(WorldPacket& recvPacket)
|
|||||||
{
|
{
|
||||||
uint32 spellId;
|
uint32 spellId;
|
||||||
uint8 castCount, castFlags;
|
uint8 castCount, castFlags;
|
||||||
|
|
||||||
|
if (recvPacket.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
recvPacket >> castCount >> spellId >> castFlags;
|
recvPacket >> castCount >> spellId >> castFlags;
|
||||||
TriggerCastFlags triggerFlag = TRIGGERED_NONE;
|
TriggerCastFlags triggerFlag = TRIGGERED_NONE;
|
||||||
|
|
||||||
@@ -365,6 +404,36 @@ void WorldSession::HandleCastSpellOpcode(WorldPacket& recvPacket)
|
|||||||
return;
|
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
|
// client provided targets
|
||||||
SpellCastTargets targets;
|
SpellCastTargets targets;
|
||||||
targets.Read(recvPacket, mover);
|
targets.Read(recvPacket, mover);
|
||||||
@@ -483,6 +552,8 @@ void WorldSession::HandleCancelCastOpcode(WorldPacket& recvPacket)
|
|||||||
recvPacket.read_skip<uint8>(); // counter, increments with every CANCEL packet, don't use for now
|
recvPacket.read_skip<uint8>(); // counter, increments with every CANCEL packet, don't use for now
|
||||||
recvPacket >> spellId;
|
recvPacket >> spellId;
|
||||||
|
|
||||||
|
_player->SpellQueue.clear();
|
||||||
|
|
||||||
_player->InterruptSpell(CURRENT_MELEE_SPELL);
|
_player->InterruptSpell(CURRENT_MELEE_SPELL);
|
||||||
if (_player->IsNonMeleeSpellCast(false))
|
if (_player->IsNonMeleeSpellCast(false))
|
||||||
_player->InterruptNonMeleeSpells(false, spellId, false, true);
|
_player->InterruptNonMeleeSpells(false, spellId, false, true);
|
||||||
|
|||||||
@@ -551,6 +551,7 @@ public:
|
|||||||
bool IsAutoRepeat() const { return m_autoRepeat; }
|
bool IsAutoRepeat() const { return m_autoRepeat; }
|
||||||
void SetAutoRepeat(bool rep) { m_autoRepeat = rep; }
|
void SetAutoRepeat(bool rep) { m_autoRepeat = rep; }
|
||||||
void ReSetTimer() { m_timer = m_casttime > 0 ? m_casttime : 0; }
|
void ReSetTimer() { m_timer = m_casttime > 0 ? m_casttime : 0; }
|
||||||
|
int32 GetCastTimeRemaining() { return m_timer;}
|
||||||
bool IsNextMeleeSwingSpell() const;
|
bool IsNextMeleeSwingSpell() const;
|
||||||
bool IsTriggered() const { return HasTriggeredCastFlag(TRIGGERED_FULL_MASK); };
|
bool IsTriggered() const { return HasTriggeredCastFlag(TRIGGERED_FULL_MASK); };
|
||||||
bool HasTriggeredCastFlag(TriggerCastFlags flag) const { return _triggeredCastFlags & flag; };
|
bool HasTriggeredCastFlag(TriggerCastFlags flag) const { return _triggeredCastFlags & flag; };
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ enum WorldBoolConfigs
|
|||||||
CONFIG_ALLOWS_RANK_MOD_FOR_PET_HEALTH,
|
CONFIG_ALLOWS_RANK_MOD_FOR_PET_HEALTH,
|
||||||
CONFIG_MUNCHING_BLIZZLIKE,
|
CONFIG_MUNCHING_BLIZZLIKE,
|
||||||
CONFIG_ENABLE_DAZE,
|
CONFIG_ENABLE_DAZE,
|
||||||
|
CONFIG_SPELL_QUEUE_ENABLED,
|
||||||
BOOL_CONFIG_VALUE_COUNT
|
BOOL_CONFIG_VALUE_COUNT
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -419,6 +420,7 @@ enum WorldIntConfigs
|
|||||||
CONFIG_WATER_BREATH_TIMER,
|
CONFIG_WATER_BREATH_TIMER,
|
||||||
CONFIG_AUCTION_HOUSE_SEARCH_TIMEOUT,
|
CONFIG_AUCTION_HOUSE_SEARCH_TIMEOUT,
|
||||||
CONFIG_DAILY_RBG_MIN_LEVEL_AP_REWARD,
|
CONFIG_DAILY_RBG_MIN_LEVEL_AP_REWARD,
|
||||||
|
CONFIG_SPELL_QUEUE_WINDOW,
|
||||||
INT_CONFIG_VALUE_COUNT
|
INT_CONFIG_VALUE_COUNT
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1487,6 +1487,10 @@ void World::LoadConfigSettings(bool reload)
|
|||||||
// Realm Availability
|
// Realm Availability
|
||||||
_bool_configs[CONFIG_REALM_LOGIN_ENABLED] = sConfigMgr->GetOption<bool>("World.RealmAvailability", true);
|
_bool_configs[CONFIG_REALM_LOGIN_ENABLED] = sConfigMgr->GetOption<bool>("World.RealmAvailability", true);
|
||||||
|
|
||||||
|
// SpellQueue
|
||||||
|
_bool_configs[CONFIG_SPELL_QUEUE_ENABLED] = sConfigMgr->GetOption<bool>("SpellQueue.Enabled", true);
|
||||||
|
_int_configs[CONFIG_SPELL_QUEUE_WINDOW] = sConfigMgr->GetOption<uint32>("SpellQueue.Window", 400);
|
||||||
|
|
||||||
// call ScriptMgr if we're reloading the configuration
|
// call ScriptMgr if we're reloading the configuration
|
||||||
sScriptMgr->OnAfterConfigLoad(reload);
|
sScriptMgr->OnAfterConfigLoad(reload);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user