From 7e55d21cddc4a69bd9271f08ffe12318b8929614 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 10 Mar 2026 23:33:38 -0700 Subject: [PATCH] feat: read quest completion state from update fields on login and mid-session resyncQuestLogFromServerSlots now reads the state field (slot*stride+1) alongside the quest ID field, and marks quest.complete=true when the server reports QuestStatus=1 (complete/ready-to-turn-in). Previously, quests that were already complete before login would remain incorrectly marked as incomplete until SMSG_QUESTUPDATE_COMPLETE fired, which only happens when objectives are NEWLY completed during the session. applyQuestStateFromFields() is a lightweight companion called from both the CREATE and VALUES update handlers that applies the same state-field check to already-tracked quests mid-session, catching the case where the last objective completes via an update-field delta rather than the dedicated quest-complete packet. Works across all expansion strides (Classic stride=3, TBC stride=4, WotLK stride=5); guarded against stride<2 (no state field available). --- include/game/game_handler.hpp | 1 + src/game/game_handler.cpp | 85 ++++++++++++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 11969c88..331248f3 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -2362,6 +2362,7 @@ private: void loadSkillLineAbilityDbc(); void extractSkillFields(const std::map& fields); void extractExploredZoneFields(const std::map& fields); + void applyQuestStateFromFields(const std::map& fields); NpcDeathCallback npcDeathCallback_; NpcAggroCallback npcAggroCallback_; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 0783c91a..e49f2afd 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -8193,6 +8193,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { maybeDetectVisibleItemLayout(); extractSkillFields(lastPlayerFields_); extractExploredZoneFields(lastPlayerFields_); + applyQuestStateFromFields(lastPlayerFields_); } break; } @@ -8544,6 +8545,7 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { if (slotsChanged) rebuildOnlineInventory(); extractSkillFields(lastPlayerFields_); extractExploredZoneFields(lastPlayerFields_); + applyQuestStateFromFields(lastPlayerFields_); } // Update item stack count / durability for online items @@ -14805,15 +14807,38 @@ bool GameHandler::resyncQuestLogFromServerSlots(bool forceQueryMetadata) { const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START); const uint8_t qStride = packetParsers_ ? packetParsers_->questLogStride() : 5; - std::unordered_set serverQuestIds; - serverQuestIds.reserve(25); + + // Collect quest IDs and their completion state from update fields. + // State field (slot*stride+1) uses the same QuestStatus enum across all expansions: + // 0 = none, 1 = complete (ready to turn in), 3 = incomplete/active, etc. + static constexpr uint32_t kQuestStatusComplete = 1; + + std::unordered_map serverQuestComplete; // questId → complete + serverQuestComplete.reserve(25); for (uint16_t slot = 0; slot < 25; ++slot) { - const uint16_t idField = ufQuestStart + slot * qStride; + const uint16_t idField = ufQuestStart + slot * qStride; + const uint16_t stateField = ufQuestStart + slot * qStride + 1; auto it = lastPlayerFields_.find(idField); if (it == lastPlayerFields_.end()) continue; - if (it->second != 0) serverQuestIds.insert(it->second); + uint32_t questId = it->second; + if (questId == 0) continue; + + bool complete = false; + if (qStride >= 2) { + auto stateIt = lastPlayerFields_.find(stateField); + if (stateIt != lastPlayerFields_.end()) { + // Lower byte is the quest state; treat any variant of "complete" as done. + uint32_t state = stateIt->second & 0xFF; + complete = (state == kQuestStatusComplete); + } + } + serverQuestComplete[questId] = complete; } + std::unordered_set serverQuestIds; + serverQuestIds.reserve(serverQuestComplete.size()); + for (const auto& [qid, _] : serverQuestComplete) serverQuestIds.insert(qid); + const size_t localBefore = questLog_.size(); std::erase_if(questLog_, [&](const QuestLogEntry& q) { return q.questId == 0 || serverQuestIds.count(q.questId) == 0; @@ -14827,6 +14852,20 @@ bool GameHandler::resyncQuestLogFromServerSlots(bool forceQueryMetadata) { ++added; } + // Apply server-authoritative completion state to all tracked quests. + // This initialises quest.complete correctly on login for quests that were + // already complete before the current session started. + size_t marked = 0; + for (auto& quest : questLog_) { + auto it = serverQuestComplete.find(quest.questId); + if (it == serverQuestComplete.end()) continue; + if (it->second && !quest.complete) { + quest.complete = true; + ++marked; + LOG_DEBUG("Quest ", quest.questId, " marked complete from update fields"); + } + } + if (forceQueryMetadata) { for (uint32_t questId : serverQuestIds) { requestQuestQuery(questId, false); @@ -14834,10 +14873,46 @@ bool GameHandler::resyncQuestLogFromServerSlots(bool forceQueryMetadata) { } LOG_INFO("Quest log resync from server slots: server=", serverQuestIds.size(), - " localBefore=", localBefore, " removed=", removed, " added=", added); + " localBefore=", localBefore, " removed=", removed, " added=", added, + " markedComplete=", marked); return true; } +// Apply quest completion state from player update fields to already-tracked local quests. +// Called from VALUES update handler so quests that complete mid-session (or that were +// complete on login) get quest.complete=true without waiting for SMSG_QUESTUPDATE_COMPLETE. +void GameHandler::applyQuestStateFromFields(const std::map& fields) { + const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START); + if (ufQuestStart == 0xFFFF || questLog_.empty()) return; + + const uint8_t qStride = packetParsers_ ? packetParsers_->questLogStride() : 5; + if (qStride < 2) return; // Need at least 2 fields per slot (id + state) + + static constexpr uint32_t kQuestStatusComplete = 1; + + for (uint16_t slot = 0; slot < 25; ++slot) { + const uint16_t idField = ufQuestStart + slot * qStride; + const uint16_t stateField = idField + 1; + auto idIt = fields.find(idField); + if (idIt == fields.end()) continue; + uint32_t questId = idIt->second; + if (questId == 0) continue; + + auto stateIt = fields.find(stateField); + if (stateIt == fields.end()) continue; + bool serverComplete = ((stateIt->second & 0xFF) == kQuestStatusComplete); + if (!serverComplete) continue; + + for (auto& quest : questLog_) { + if (quest.questId == questId && !quest.complete) { + quest.complete = true; + LOG_INFO("Quest ", questId, " marked complete from VALUES update field state"); + break; + } + } + } +} + void GameHandler::clearPendingQuestAccept(uint32_t questId) { pendingQuestAcceptTimeouts_.erase(questId); pendingQuestAcceptNpcGuids_.erase(questId);