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.
This commit is contained in:
Kelsi
2026-07-24 00:40:47 -07:00
parent 701bf71603
commit 07443e7535
5 changed files with 87 additions and 0 deletions

View File

@@ -1610,6 +1610,9 @@ public:
// Quest log
using QuestLogEntry = QuestHandler::QuestLogEntry;
const std::vector<QuestLogEntry>& 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<uint32_t, uint32_t>& carriedCounts);
int getMaxQuestLogSlots() const;
// QuestSort.dbc name for negative ZoneOrSort values (class/profession/seasonal)
const std::string& getQuestSortName(uint32_t sortId) const;

View File

@@ -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<uint32_t, uint32_t>& carriedCounts);
void clearPendingQuestAccept(uint32_t questId);
void triggerQuestAcceptResync(uint32_t questId, uint64_t npcGuid, const char* reason);

View File

@@ -2736,6 +2736,11 @@ const std::vector<GameHandler::QuestLogEntry>& GameHandler::getQuestLog() const
static const std::vector<QuestLogEntry> empty;
return empty;
}
void GameHandler::reconcileQuestItemObjectives(
const std::unordered_map<uint32_t, uint32_t>& carriedCounts) {
if (questHandler_) questHandler_->reconcileItemObjectivesFromInventory(carriedCounts);
}
int GameHandler::getMaxQuestLogSlots() const {
return questHandler_ ? questHandler_->maxQuestLogSlots() : 25;
}

View File

@@ -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<uint32_t, uint32_t> 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<uint32_t>(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<uint32_t>(1, slot.item.stackCount);
}
}
owner_.reconcileQuestItemObjectives(carriedCounts);
}
void InventoryHandler::maybeDetectVisibleItemLayout() {

View File

@@ -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<uint32_t, uint32_t>& 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;