From 2c6902d27d5eac403e159dd054263753fc2f506e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 04:37:36 -0700 Subject: [PATCH] fix: mining nodes no longer report invalid target and now open loot after gather MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs fixed: 1. Retry logic (for Classic) re-sent CMSG_GAMEOBJ_USE at 0.15s while the gather cast was in-flight, causing SPELL_FAILED_BAD_TARGETS. Now clears pendingGameObjectLootRetries_ as soon as SMSG_SPELL_START shows the player started a cast (gather accepted). 2. CMSG_LOOT was sent immediately before the gather cast completed, then never sent again — so the loot window never opened. Now tracks the last interacted GO and sends CMSG_LOOT in handleSpellGo once the gather spell completes, matching how the real client behaves. --- include/game/game_handler.hpp | 3 +++ src/game/game_handler.cpp | 17 +++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 3481285b..7e35e203 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -2778,6 +2778,9 @@ private: float timer = 0.0f; }; std::vector pendingGameObjectLootOpens_; + // Tracks the last GO we sent CMSG_GAMEOBJ_USE to; used in handleSpellGo + // to send CMSG_LOOT after a gather cast (mining/herbalism) completes. + uint64_t lastInteractedGoGuid_ = 0; uint64_t pendingLootMoneyGuid_ = 0; uint32_t pendingLootMoneyAmount_ = 0; float pendingLootMoneyNotifyTimer_ = 0.0f; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 3046c4c6..e2baca55 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -16213,6 +16213,14 @@ void GameHandler::handleSpellStart(network::Packet& packet) { // If this is the player's own cast, start cast bar if (data.casterUnit == playerGuid && data.castTime > 0) { + // CMSG_GAMEOBJ_USE was accepted — cancel pending USE retries so we don't + // re-send GAMEOBJ_USE mid-gather-cast and get SPELL_FAILED_BAD_TARGETS. + // Keep entries that only have sendLoot (no-cast chests that still need looting). + pendingGameObjectLootRetries_.erase( + std::remove_if(pendingGameObjectLootRetries_.begin(), pendingGameObjectLootRetries_.end(), + [](const PendingLootRetry&) { return true; /* cancel all retries once a gather cast starts */ }), + pendingGameObjectLootRetries_.end()); + casting = true; castIsChannel = false; currentCastSpellId = data.spellId; @@ -16289,6 +16297,13 @@ void GameHandler::handleSpellGo(network::Packet& packet) { currentCastSpellId = 0; castTimeRemaining = 0.0f; + // If we were gathering a node (mining/herbalism), send CMSG_LOOT now that + // the gather cast completed and the server has made the node lootable. + if (lastInteractedGoGuid_ != 0) { + lootTarget(lastInteractedGoGuid_); + lastInteractedGoGuid_ = 0; + } + // End cast animation on player character if (spellCastAnimCallback_) { spellCastAnimCallback_(playerGuid, false, false); @@ -17369,6 +17384,7 @@ void GameHandler::performGameObjectInteractionNow(uint64_t guid) { auto packet = GameObjectUsePacket::build(guid); socket->send(packet); + lastInteractedGoGuid_ = guid; // For mailbox GameObjects (type 19), open mail UI and request mail list. // In Vanilla/Classic there is no SMSG_SHOW_MAILBOX — the server just sends @@ -18582,6 +18598,7 @@ void GameHandler::handleLootResponse(network::Packet& packet) { const bool wotlkLoot = isActiveExpansion("wotlk"); if (!LootResponseParser::parse(packet, currentLoot, wotlkLoot)) return; lootWindowOpen = true; + lastInteractedGoGuid_ = 0; // loot opened — no need to re-send in handleSpellGo localLootState_[currentLoot.lootGuid] = LocalLootState{currentLoot, false}; // Query item info so loot window can show names instead of IDs