From 759d6046bbdb1f1a64b383441a1e3def27865d60 Mon Sep 17 00:00:00 2001 From: Pavel Okhlopkov Date: Fri, 10 Apr 2026 19:50:56 +0300 Subject: [PATCH] fix(quest): quest log population, NPC marker updates on accept/abandon - Delegate GameHandler::getQuestGiverStatus() to QuestHandler instead of reading from GameHandler's own empty npcQuestStatus_ map - Immediately add quest to local log in acceptQuest() instead of waiting for field updates, fixing quests not appearing after accept - Handle duplicate accept path (server already has quest) by also adding to local log - Remove early return on empty questLog_ in applyQuestStateFromFields() - Re-query nearby quest giver NPC statuses on abandon so markers refresh Signed-off-by: Pavel Okhlopkov --- include/game/game_handler.hpp | 5 +--- src/game/game_handler.cpp | 4 +++ src/game/quest_handler.cpp | 47 ++++++++++++++++++++++++++++++++--- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index cac41b7c..7cff33f0 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1699,10 +1699,7 @@ public: bool isServerMovementAllowed() const; // Quest giver status (! and ? markers) - QuestGiverStatus getQuestGiverStatus(uint64_t guid) const { - auto it = npcQuestStatus_.find(guid); - return (it != npcQuestStatus_.end()) ? it->second : QuestGiverStatus::NONE; - } + QuestGiverStatus getQuestGiverStatus(uint64_t guid) const; const std::unordered_map& getNpcQuestStatuses() const; // Charge callback — fires when player casts a charge spell toward target diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 3d38b20b..9eeb836b 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2261,6 +2261,10 @@ const std::unordered_map& GameHandler::getNpcQuestSt static const std::unordered_map empty; return empty; } +QuestGiverStatus GameHandler::getQuestGiverStatus(uint64_t guid) const { + if (questHandler_) return questHandler_->getQuestGiverStatus(guid); + return QuestGiverStatus::NONE; +} const std::vector& GameHandler::getQuestLog() const { if (questHandler_) return questHandler_->getQuestLog(); static const std::vector empty; diff --git a/src/game/quest_handler.cpp b/src/game/quest_handler.cpp index 6b5b12c4..3f4ee6ab 100644 --- a/src/game/quest_handler.cpp +++ b/src/game/quest_handler.cpp @@ -339,6 +339,8 @@ void QuestHandler::registerOpcodes(DispatchTable& table) { if (packet.hasRemaining(9)) { uint64_t npcGuid = packet.readUInt64(); uint8_t status = owner_.getPacketParsers()->readQuestGiverStatus(packet); + LOG_INFO("SMSG_QUESTGIVER_STATUS: npcGuid=0x", std::hex, npcGuid, std::dec, + " status=", static_cast(status)); npcQuestStatus_[npcGuid] = static_cast(status); } }; @@ -1075,8 +1077,17 @@ void QuestHandler::acceptQuest() { const bool inLocalLog = hasQuestInLog(questId); const int serverSlot = findQuestLogSlotIndexFromServer(questId); if (serverSlot >= 0) { - LOG_INFO("Ignoring duplicate quest accept already in server quest log: questId=", questId, - " slot=", serverSlot); + LOG_INFO("Quest already in server quest log: questId=", questId, + " slot=", serverSlot, " inLocalLog=", inLocalLog); + // Ensure it's in our local log even if server already has it + addQuestToLocalLogIfMissing(questId, currentQuestDetails_.title, currentQuestDetails_.objectives); + requestQuestQuery(questId, false); + // Re-query NPC status from server + if (npcGuid && owner_.getSocket()) { + network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY)); + qsPkt.writeUInt64(npcGuid); + owner_.getSocket()->send(qsPkt); + } questDetailsOpen_ = false; questDetailsOpenTime_ = std::chrono::steady_clock::time_point{}; currentQuestDetails_ = QuestDetailsData{}; @@ -1094,6 +1105,9 @@ void QuestHandler::acceptQuest() { pendingQuestAcceptTimeouts_[questId] = 5.0f; pendingQuestAcceptNpcGuids_[questId] = npcGuid; + // Immediately add to local quest log using available details + addQuestToLocalLogIfMissing(questId, currentQuestDetails_.title, currentQuestDetails_.objectives); + // Play quest-accept sound if (auto* ac = owner_.services().audioCoordinator) { if (auto* sfx = ac->getUiSoundManager()) @@ -1223,6 +1237,19 @@ void QuestHandler::abandonQuest(uint32_t questId) { } } + // Re-query nearby quest giver NPCs so markers refresh (e.g. "?" → "!") + if (owner_.getSocket()) { + for (const auto& [guid, entity] : owner_.getEntityManager().getEntities()) { + if (entity->getType() != ObjectType::UNIT) continue; + auto unit = std::static_pointer_cast(entity); + if (unit->getNpcFlags() & 0x02) { + network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY)); + qsPkt.writeUInt64(guid); + owner_.getSocket()->send(qsPkt); + } + } + } + // Remove any quest POI minimap markers for this quest. gossipPois_.erase( std::remove_if(gossipPois_.begin(), gossipPois_.end(), @@ -1376,7 +1403,7 @@ bool QuestHandler::resyncQuestLogFromServerSlots(bool forceQueryMetadata) { void QuestHandler::applyQuestStateFromFields(const std::map& fields) { const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START); - if (ufQuestStart == 0xFFFF || questLog_.empty()) return; + if (ufQuestStart == 0xFFFF) return; const uint8_t qStride = owner_.getPacketParsers() ? owner_.getPacketParsers()->questLogStride() : 5; if (qStride < 2) return; @@ -1391,6 +1418,20 @@ void QuestHandler::applyQuestStateFromFields(const std::map& uint32_t questId = idIt->second; if (questId == 0) continue; + // Add quest to local log only if we have a pending accept for it + if (!hasQuestInLog(questId) && pendingQuestAcceptTimeouts_.count(questId) != 0) { + addQuestToLocalLogIfMissing(questId, "Quest #" + std::to_string(questId), ""); + requestQuestQuery(questId, false); + // Re-query quest giver status for the NPC that gave us this quest + auto pendingIt = pendingQuestAcceptNpcGuids_.find(questId); + if (pendingIt != pendingQuestAcceptNpcGuids_.end() && pendingIt->second != 0 && owner_.getSocket()) { + network::Packet qsPkt(wireOpcode(Opcode::CMSG_QUESTGIVER_STATUS_QUERY)); + qsPkt.writeUInt64(pendingIt->second); + owner_.getSocket()->send(qsPkt); + } + clearPendingQuestAccept(questId); + } + auto stateIt = fields.find(stateField); if (stateIt == fields.end()) continue; bool serverComplete = ((stateIt->second & 0xFF) == kQuestStatusComplete);