fix: wire SpellHandler::updateTimers and remove stale cast state members

SpellHandler::updateTimers() was never called after PR #23 extraction,
so cast bar timers, spell cooldowns, and unit cast state timers never
ticked. Also removes duplicate cast/queue/spell members left in
GameHandler that shadowed the SpellHandler versions, and fixes
MovementHandler writing to those stale members on world portal.

Demotes SMSG_SPELL_START/CAST_RESULT debug logs to LOG_DEBUG.
This commit is contained in:
Kelsi 2026-03-29 16:49:17 -07:00
parent d32b35c583
commit 209c257745
5 changed files with 15 additions and 45 deletions

View file

@ -796,7 +796,7 @@ public:
// 400ms spell-queue window: next spell to cast when current finishes
uint32_t getQueuedSpellId() const;
void cancelQueuedSpell() { queuedSpellId_ = 0; queuedSpellTarget_ = 0; }
void cancelQueuedSpell() { if (spellHandler_) spellHandler_->cancelQueuedSpell(); }
// Unit cast state (aliased from handler_types.hpp)
using UnitCastState = game::UnitCastState;
@ -2543,24 +2543,9 @@ private:
uint64_t playerTransportStickyGuid_ = 0; // Last transport player was on (temporary retention)
float playerTransportStickyTimer_ = 0.0f; // Seconds to keep sticky transport alive after transient clears
std::unique_ptr<TransportManager> transportManager_; // Transport movement manager
std::unordered_set<uint32_t> knownSpells;
std::unordered_map<uint32_t, float> spellCooldowns; // spellId -> remaining seconds
uint32_t weaponProficiency_ = 0; // bitmask from SMSG_SET_PROFICIENCY itemClass=2
uint32_t armorProficiency_ = 0; // bitmask from SMSG_SET_PROFICIENCY itemClass=4
std::vector<MinimapPing> minimapPings_;
uint8_t castCount = 0;
bool casting = false;
bool castIsChannel = false;
uint32_t currentCastSpellId = 0;
float castTimeRemaining = 0.0f;
// Repeat-craft queue: re-cast the same profession spell N more times after current cast finishes
uint32_t craftQueueSpellId_ = 0;
int craftQueueRemaining_ = 0;
// Spell queue: next spell to cast within the 400ms window before current cast ends
uint32_t queuedSpellId_ = 0;
uint64_t queuedSpellTarget_ = 0;
// Per-unit cast state (keyed by GUID, populated from SMSG_SPELL_START)
std::unordered_map<uint64_t, UnitCastState> unitCastStates_;
uint64_t pendingGameObjectInteractGuid_ = 0;
// Talents (dual-spec support)
@ -2588,7 +2573,6 @@ private:
float areaTriggerCheckTimer_ = 0.0f;
bool areaTriggerSuppressFirst_ = false; // suppress first check after map transfer
float castTimeTotal = 0.0f;
std::array<ActionBarSlot, ACTION_BAR_SLOTS> actionBar{};
std::unordered_map<uint32_t, std::string> macros_; // client-side macro text (persisted in char config)
std::vector<AuraSlot> playerAuras;

View file

@ -79,6 +79,7 @@ public:
auto it = unitCastStates_.find(guid);
return (it != unitCastStates_.end() && it->second.casting) ? &it->second : nullptr;
}
void clearUnitCastStates() { unitCastStates_.clear(); }
// Target cast helpers
bool isTargetCasting() const;

View file

@ -688,10 +688,6 @@ GameHandler::GameHandler() {
wardenHandler_ = std::make_unique<WardenHandler>(*this);
wardenHandler_->initModuleManager();
// Default spells always available
knownSpells.insert(6603); // Attack
knownSpells.insert(8690); // Hearthstone
// Default action bar layout
actionBar[0].type = ActionBarSlot::SPELL;
actionBar[0].id = 6603; // Attack in slot 1
@ -1118,6 +1114,8 @@ for (auto& [guid, entity] : entityController_->getEntityManager().getEntities())
}
void GameHandler::updateTimers(float deltaTime) {
if (spellHandler_) spellHandler_->updateTimers(deltaTime);
if (auctionSearchDelayTimer_ > 0.0f) {
auctionSearchDelayTimer_ -= deltaTime;
if (auctionSearchDelayTimer_ < 0.0f) auctionSearchDelayTimer_ = 0.0f;
@ -4637,14 +4635,9 @@ void GameHandler::selectCharacter(uint64_t characterGuid) {
pendingQuestAcceptNpcGuids_.clear();
npcQuestStatus_.clear();
if (combatHandler_) combatHandler_->resetAllCombatState();
if (spellHandler_) { spellHandler_->casting_ = false; spellHandler_->castIsChannel_ = false; spellHandler_->currentCastSpellId_ = 0; }
if (spellHandler_) spellHandler_->resetCastState();
pendingGameObjectInteractGuid_ = 0;
lastInteractedGoGuid_ = 0;
if (spellHandler_) { spellHandler_->castTimeRemaining_ = 0.0f; spellHandler_->castTimeTotal_ = 0.0f; }
craftQueueSpellId_ = 0;
craftQueueRemaining_ = 0;
queuedSpellId_ = 0;
queuedSpellTarget_ = 0;
playerDead_ = false;
releasedSpirit_ = false;
corpseGuid_ = 0;

View file

@ -1880,10 +1880,6 @@ void MovementHandler::handleNewWorld(network::Packet& packet) {
owner_.stopAutoAttack();
owner_.tabCycleStale = true;
owner_.resetCastState();
owner_.craftQueueSpellId_ = 0;
owner_.craftQueueRemaining_ = 0;
owner_.queuedSpellId_ = 0;
owner_.queuedSpellTarget_ = 0;
if (owner_.socket) {
network::Packet ack(wireOpcode(Opcode::MSG_MOVE_WORLDPORT_ACK));
@ -1935,7 +1931,7 @@ void MovementHandler::handleNewWorld(network::Packet& packet) {
owner_.otherPlayerVisibleItemEntries_.clear();
owner_.otherPlayerVisibleDirty_.clear();
otherPlayerMoveTimeMs_.clear();
owner_.unitCastStates_.clear();
if (owner_.spellHandler_) owner_.spellHandler_->clearUnitCastStates();
owner_.unitAurasCache_.clear();
owner_.clearCombatText();
owner_.getEntityManager().clear();
@ -1949,10 +1945,6 @@ void MovementHandler::handleNewWorld(network::Packet& packet) {
owner_.areaTriggerSuppressFirst_ = true;
owner_.stopAutoAttack();
owner_.resetCastState();
owner_.craftQueueSpellId_ = 0;
owner_.craftQueueRemaining_ = 0;
owner_.queuedSpellId_ = 0;
owner_.queuedSpellTarget_ = 0;
if (owner_.socket) {
network::Packet ack(wireOpcode(Opcode::MSG_MOVE_WORLDPORT_ACK));

View file

@ -204,7 +204,7 @@ bool SpellHandler::isTargetCastInterruptible() const {
}
void SpellHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
LOG_WARNING("castSpell: spellId=", spellId, " target=0x", std::hex, targetGuid, std::dec);
LOG_DEBUG("castSpell: spellId=", spellId, " target=0x", std::hex, targetGuid, std::dec);
// Attack (6603) routes to auto-attack instead of cast
if (spellId == 6603) {
uint64_t target = targetGuid != 0 ? targetGuid : owner_.targetGuid;
@ -323,8 +323,8 @@ void SpellHandler::castSpell(uint32_t spellId, uint64_t targetGuid) {
auto packet = owner_.packetParsers_
? owner_.packetParsers_->buildCastSpell(spellId, target, ++castCount_)
: CastSpellPacket::build(spellId, target, ++castCount_);
LOG_WARNING("CMSG_CAST_SPELL: spellId=", spellId, " target=0x", std::hex, target, std::dec,
" castCount=", static_cast<int>(castCount_), " packetSize=", packet.getSize());
LOG_DEBUG("CMSG_CAST_SPELL: spellId=", spellId, " target=0x", std::hex, target, std::dec,
" castCount=", static_cast<int>(castCount_), " packetSize=", packet.getSize());
owner_.socket->send(packet);
LOG_INFO("Casting spell: ", spellId, " on 0x", std::hex, target, std::dec);
@ -851,9 +851,9 @@ void SpellHandler::handleSpellStart(network::Packet& packet) {
LOG_WARNING("Failed to parse SMSG_SPELL_START, size=", packet.getSize());
return;
}
LOG_WARNING("SMSG_SPELL_START: caster=0x", std::hex, data.casterUnit, std::dec,
" spell=", data.spellId, " castTime=", data.castTime,
" isPlayer=", (data.casterUnit == owner_.playerGuid));
LOG_DEBUG("SMSG_SPELL_START: caster=0x", std::hex, data.casterUnit, std::dec,
" spell=", data.spellId, " castTime=", data.castTime,
" isPlayer=", (data.casterUnit == owner_.playerGuid));
// Track cast bar for any non-player caster
if (data.casterUnit != owner_.playerGuid && data.castTime > 0) {
@ -2066,12 +2066,12 @@ void SpellHandler::handleCastResult(network::Packet& packet) {
uint32_t castResultSpellId = 0;
uint8_t castResult = 0;
if (owner_.packetParsers_->parseCastResult(packet, castResultSpellId, castResult)) {
LOG_WARNING("SMSG_CAST_RESULT: spellId=", castResultSpellId, " result=", static_cast<int>(castResult));
LOG_DEBUG("SMSG_CAST_RESULT: spellId=", castResultSpellId, " result=", static_cast<int>(castResult));
if (castResult != 0) {
casting_ = false; castIsChannel_ = false; currentCastSpellId_ = 0; castTimeRemaining_ = 0.0f;
owner_.lastInteractedGoGuid_ = 0;
owner_.craftQueueSpellId_ = 0; owner_.craftQueueRemaining_ = 0;
owner_.queuedSpellId_ = 0; owner_.queuedSpellTarget_ = 0;
craftQueueSpellId_ = 0; craftQueueRemaining_ = 0;
queuedSpellId_ = 0; queuedSpellTarget_ = 0;
int playerPowerType = -1;
if (auto pe = owner_.getEntityManager().getEntity(owner_.playerGuid)) {
if (auto pu = std::dynamic_pointer_cast<Unit>(pe))