From 07443e7535aaf6ddfeaddd6700ad249773913330 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 24 Jul 2026 00:40:47 -0700 Subject: [PATCH] fix: derive quest collect-item progress from bag contents 3.3.5a servers do not push collect-item objective counts (unlike kill credit, which arrives in the quest-log update fields), so the tracker relying solely on SMSG_QUESTUPDATE_ADD_ITEM never advanced when quest items were looted. Reconcile item objectives against actual bag contents on every inventory rebuild instead. --- include/game/game_handler.hpp | 3 +++ include/game/quest_handler.hpp | 8 ++++++ src/game/game_handler.cpp | 5 ++++ src/game/inventory_handler.cpp | 23 ++++++++++++++++ src/game/quest_handler.cpp | 48 ++++++++++++++++++++++++++++++++++ 5 files changed, 87 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index cfa52c86..dd8ab206 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1610,6 +1610,9 @@ public: // Quest log using QuestLogEntry = QuestHandler::QuestLogEntry; const std::vector& getQuestLog() const; + // Reconcile collect-item quest objectives against current bag contents. + // Forwards to QuestHandler; called by InventoryHandler after each rebuild. + void reconcileQuestItemObjectives(const std::unordered_map& carriedCounts); int getMaxQuestLogSlots() const; // QuestSort.dbc name for negative ZoneOrSort values (class/profession/seasonal) const std::string& getQuestSortName(uint32_t sortId) const; diff --git a/include/game/quest_handler.hpp b/include/game/quest_handler.hpp index 208c0aff..11052d28 100644 --- a/include/game/quest_handler.hpp +++ b/include/game/quest_handler.hpp @@ -137,6 +137,14 @@ public: bool resyncQuestLogFromServerSlots(bool forceQueryMetadata); void applyQuestStateFromFields(const FlatFieldMap& fields); void applyPackedKillCountsFromFields(QuestLogEntry& quest); + // Reconcile collect-item objective progress against the player's actual + // bag contents. In 3.3.5a the server does not push item objective counts + // (unlike kill credit, which arrives packed in the quest-log update + // fields) — the authentic client derives them by counting matching item + // IDs in the bags. Called after every inventory rebuild. `carriedCounts` + // maps itemId -> total quantity currently held across backpack + bags. + void reconcileItemObjectivesFromInventory( + const std::unordered_map& carriedCounts); void clearPendingQuestAccept(uint32_t questId); void triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid, const char* reason); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index f2473514..96a72375 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2736,6 +2736,11 @@ const std::vector& GameHandler::getQuestLog() const static const std::vector empty; return empty; } + +void GameHandler::reconcileQuestItemObjectives( + const std::unordered_map& carriedCounts) { + if (questHandler_) questHandler_->reconcileItemObjectivesFromInventory(carriedCounts); +} int GameHandler::getMaxQuestLogSlots() const { return questHandler_ ? questHandler_->maxQuestLogSlots() : 25; } diff --git a/src/game/inventory_handler.cpp b/src/game/inventory_handler.cpp index e2825a24..0b396f91 100644 --- a/src/game/inventory_handler.cpp +++ b/src/game/inventory_handler.cpp @@ -3484,6 +3484,29 @@ void InventoryHandler::rebuildOnlineInventory() { }(), " keyring=", [&](){ int c = 0; for (auto g : owner_.keyringSlotGuidsRef()) if (g) c++; return c; }()); + + // Reconcile collect-item quest objectives against what the player is + // actually carrying. In 3.3.5a the server never pushes item objective + // counts, so this bag-count pass is the only thing that advances "collect + // N of item" progress when quest items are looted (or removed). Count + // backpack + the four equipped bags — the same set the server checks at + // turn-in — summing stacks per item id. + std::unordered_map carriedCounts; + const auto& inv = owner_.inventoryRef(); + for (int i = 0; i < inv.getBackpackSize(); i++) { + const auto& slot = inv.getBackpackSlot(i); + if (!slot.empty()) + carriedCounts[slot.item.itemId] += std::max(1, slot.item.stackCount); + } + for (int bagIdx = 0; bagIdx < 4; bagIdx++) { + int numSlots = inv.getBagSize(bagIdx); + for (int s = 0; s < numSlots; s++) { + const auto& slot = inv.getBagSlot(bagIdx, s); + if (!slot.empty()) + carriedCounts[slot.item.itemId] += std::max(1, slot.item.stackCount); + } + } + owner_.reconcileQuestItemObjectives(carriedCounts); } void InventoryHandler::maybeDetectVisibleItemLayout() { diff --git a/src/game/quest_handler.cpp b/src/game/quest_handler.cpp index b535fc5f..06a7e21c 100644 --- a/src/game/quest_handler.cpp +++ b/src/game/quest_handler.cpp @@ -27,6 +27,54 @@ QuestGiverStatus QuestHandler::getQuestGiverStatus(uint64_t guid) const { return (it != npcQuestStatus_.end()) ? it->second : QuestGiverStatus::NONE; } +void QuestHandler::reconcileItemObjectivesFromInventory( + const std::unordered_map& carriedCounts) { + bool changedAny = false; + bool maybeCompletedObjective = false; + for (auto& quest : questLog_) { + if (quest.complete) continue; + for (const auto& obj : quest.itemObjectives) { + if (obj.itemId == 0 || obj.required == 0) continue; + // Keep the derived required-count map in sync so the + // SMSG_QUESTUPDATE_ADD_ITEM path and this one agree. + quest.requiredItemCounts[obj.itemId] = obj.required; + + auto cit = carriedCounts.find(obj.itemId); + const uint32_t held = (cit != carriedCounts.end()) ? cit->second : 0; + const uint32_t newCount = std::min(held, obj.required); + uint32_t& tracked = quest.itemCounts[obj.itemId]; + if (tracked == newCount) continue; + + const bool wasComplete = tracked >= obj.required; + tracked = newCount; + changedAny = true; + if (!wasComplete && newCount >= obj.required) + maybeCompletedObjective = true; + + // Push the per-objective progress to the on-screen tracker. + std::string itemLabel = "item #" + std::to_string(obj.itemId); + if (const ItemQueryResponseData* info = owner_.getItemInfo(obj.itemId)) { + if (!info->name.empty()) itemLabel = info->name; + } else { + // Name not cached yet — request it so the next refresh labels + // the objective properly. + owner_.queryItemInfo(obj.itemId, 0); + } + if (owner_.questProgressCallbackRef()) + owner_.questProgressCallbackRef()(quest.title, itemLabel, newCount, obj.required); + } + } + + if (changedAny && owner_.addonEventCallbackRef()) { + owner_.addonEventCallbackRef()("QUEST_WATCH_UPDATE", {}); + owner_.addonEventCallbackRef()("QUEST_LOG_UPDATE", {}); + owner_.addonEventCallbackRef()("UNIT_QUEST_LOG_CHANGED", {"player"}); + } + // Collecting the last item may have made a quest turn-in-able (! → ?), + // so refresh nearby giver markers immediately. + if (maybeCompletedObjective) requeryNearbyQuestGiverStatus(); +} + void QuestHandler::requeryNearbyQuestGiverStatus() { if (questGiverRequeryCooldown_ > 0.0f) { questGiverRequeryPending_ = true;