From 4fcf869e34e552857591c632c4069e5eb2dc2c47 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Thu, 19 Feb 2026 00:56:24 -0800 Subject: [PATCH] Stabilize quest log details loading and turn-in item sync --- include/game/game_handler.hpp | 8 +++ include/ui/quest_log_screen.hpp | 3 ++ src/game/game_handler.cpp | 91 ++++++++++++++++++++++++--------- src/ui/game_screen.cpp | 45 +++++++++++----- src/ui/quest_log_screen.cpp | 29 ++++++++--- 5 files changed, 135 insertions(+), 41 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 8e732096..2869d9c5 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -704,10 +704,16 @@ public: std::unordered_map> killCounts; // Quest item progress: itemId -> current count std::unordered_map itemCounts; + // Server-authoritative quest item requirements from REQUEST_ITEMS + std::unordered_map requiredItemCounts; }; const std::vector& getQuestLog() const { return questLog_; } void abandonQuest(uint32_t questId); bool requestQuestQuery(uint32_t questId, bool force = false); + bool isQuestQueryPending(uint32_t questId) const { + return pendingQuestQueryIds_.count(questId) > 0; + } + void clearQuestQueryPending(uint32_t questId) { pendingQuestQueryIds_.erase(questId); } const std::unordered_map& getWorldStates() const { return worldStates_; } std::optional getWorldState(uint32_t key) const { auto it = worldStates_.find(key); @@ -1437,6 +1443,8 @@ private: // Quest log std::vector questLog_; std::unordered_set pendingQuestQueryIds_; + int questQueryTracePacketsLeft_ = 0; + uint32_t questQueryTraceQuestId_ = 0; // Quest giver status per NPC std::unordered_map npcQuestStatus_; diff --git a/include/ui/quest_log_screen.hpp b/include/ui/quest_log_screen.hpp index d2db34e7..7e289e92 100644 --- a/include/ui/quest_log_screen.hpp +++ b/include/ui/quest_log_screen.hpp @@ -3,6 +3,7 @@ #include "game/game_handler.hpp" #include #include +#include namespace wowee { namespace ui { @@ -18,6 +19,8 @@ private: bool lKeyWasDown = false; int selectedIndex = -1; uint32_t lastDetailRequestQuestId_ = 0; + double lastDetailRequestAt_ = 0.0; + std::unordered_set questDetailQueryNoResponse_; }; }} // namespace wowee::ui diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 3eb12581..e35d66bd 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -932,6 +932,25 @@ void GameHandler::handlePacket(network::Packet& packet) { // Translate wire opcode to logical opcode via expansion table auto logicalOp = opcodeTable_.fromWire(opcode); + + if (questQueryTracePacketsLeft_ > 0) { + int logicalId = logicalOp ? static_cast(*logicalOp) : -1; + size_t limit = std::min(24, packet.getSize()); + std::string hex; + const auto& raw = packet.getData(); + for (size_t i = 0; i < limit && i < raw.size(); ++i) { + char buf[4]; + snprintf(buf, sizeof(buf), "%02x ", raw[i]); + hex += buf; + } + LOG_INFO("QTRACE RX: wire=0x", std::hex, opcode, std::dec, + " logical=", logicalId, + " size=", packet.getSize(), + " qid=", questQueryTraceQuestId_, + " head=[", hex, "]"); + --questQueryTracePacketsLeft_; + } + if (!logicalOp) { static std::unordered_set loggedUnknownWireOpcodes; if (loggedUnknownWireOpcodes.insert(opcode).second) { @@ -1861,6 +1880,10 @@ void GameHandler::handlePacket(network::Packet& packet) { bool updatedAny = false; for (auto& quest : questLog_) { if (quest.complete) continue; + const bool tracksItem = + quest.requiredItemCounts.count(itemId) > 0 || + quest.itemCounts.count(itemId) > 0; + if (!tracksItem) continue; quest.itemCounts[itemId] = count; updatedAny = true; } @@ -1940,6 +1963,8 @@ void GameHandler::handlePacket(network::Packet& packet) { } uint32_t questId = packet.readUInt32(); + LOG_INFO("Quest query RX: wire=0x", std::hex, opcode, std::dec, + " questId=", questId, " payloadSize=", packet.getSize()); packet.readUInt32(); // questMethod const bool isClassicLayout = packetParsers_ && packetParsers_->questLogStride() == 3; @@ -1964,7 +1989,7 @@ void GameHandler::handlePacket(network::Packet& packet) { q.title = parsed.title; } if (!parsed.objectives.empty() && - (q.objectives.empty() || parsed.objectives.size() > q.objectives.size())) { + (q.objectives.empty() || q.objectives.size() < 16)) { q.objectives = parsed.objectives; } break; @@ -8657,28 +8682,12 @@ void GameHandler::selectGossipOption(uint32_t optionId) { void GameHandler::selectGossipQuest(uint32_t questId) { if (state != WorldState::IN_WORLD || !socket || !gossipWindowOpen) return; - // Check if quest is in our quest log and completable - bool isInLog = false; - bool isCompletable = false; - for (const auto& quest : questLog_) { - if (quest.questId == questId) { - isInLog = true; - isCompletable = quest.complete; - break; - } - } - - if (isInLog && isCompletable) { - // Quest is ready to turn in - request reward - network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_REQUEST_REWARD)); - packet.writeUInt64(currentGossip.npcGuid); - packet.writeUInt32(questId); - socket->send(packet); - } else { - // New quest or not completable - query details - auto packet = QuestgiverQueryQuestPacket::build(currentGossip.npcGuid, questId); - socket->send(packet); - } + // Always query quest from gossip and let the server drive next step: + // - details (new quest), or + // - request items (turn-in check), or + // - offer reward. + auto packet = QuestgiverQueryQuestPacket::build(currentGossip.npcGuid, questId); + socket->send(packet); gossipWindowOpen = false; } @@ -8689,6 +8698,11 @@ bool GameHandler::requestQuestQuery(uint32_t questId, bool force) { network::Packet pkt(wireOpcode(Opcode::CMSG_QUEST_QUERY)); pkt.writeUInt32(questId); + questQueryTraceQuestId_ = questId; + questQueryTracePacketsLeft_ = 60; + LOG_INFO("Quest query TX: questId=", questId, " wireOpcode=0x", + std::hex, wireOpcode(Opcode::CMSG_QUEST_QUERY), std::dec, + " force=", force ? 1 : 0); socket->send(pkt); pendingQuestQueryIds_.insert(questId); return true; @@ -8785,6 +8799,37 @@ void GameHandler::handleQuestRequestItems(network::Packet& packet) { for (const auto& item : data.requiredItems) { queryItemInfo(item.itemId, 0); } + + // Server-authoritative turn-in requirements: sync quest-log summary so + // UI doesn't show stale/inferred objective numbers. + for (auto& q : questLog_) { + if (q.questId != data.questId) continue; + q.complete = data.isCompletable(); + q.requiredItemCounts.clear(); + + std::ostringstream oss; + if (!data.completionText.empty()) { + oss << data.completionText; + if (!data.requiredItems.empty() || data.requiredMoney > 0) oss << "\n\n"; + } + if (!data.requiredItems.empty()) { + oss << "Required items:"; + for (const auto& item : data.requiredItems) { + std::string itemLabel = "Item " + std::to_string(item.itemId); + if (const auto* info = getItemInfo(item.itemId)) { + if (!info->name.empty()) itemLabel = info->name; + } + q.requiredItemCounts[item.itemId] = item.count; + oss << "\n- " << itemLabel << " x" << item.count; + } + } + if (data.requiredMoney > 0) { + if (!data.requiredItems.empty()) oss << "\n"; + oss << "\nRequired money: " << formatCopperAmount(data.requiredMoney); + } + q.objectives = oss.str(); + break; + } } void GameHandler::handleQuestOfferReward(network::Packet& packet) { diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 2469783d..33a90cd7 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -4532,6 +4532,23 @@ void GameScreen::renderQuestRequestItemsWindow(game::GameHandler& gameHandler) { bool open = true; const auto& quest = gameHandler.getQuestRequestItems(); + auto countItemInInventory = [&](uint32_t itemId) -> uint32_t { + const auto& inv = gameHandler.getInventory(); + uint32_t total = 0; + for (int i = 0; i < inv.getBackpackSize(); ++i) { + const auto& slot = inv.getBackpackSlot(i); + if (!slot.empty() && slot.item.itemId == itemId) total += slot.item.stackCount; + } + for (int bag = 0; bag < game::Inventory::NUM_BAG_SLOTS; ++bag) { + int bagSize = inv.getBagSize(bag); + for (int s = 0; s < bagSize; ++s) { + const auto& slot = inv.getBagSlot(bag, s); + if (!slot.empty() && slot.item.itemId == itemId) total += slot.item.stackCount; + } + } + return total; + }; + std::string processedTitle = replaceGenderPlaceholders(quest.title, gameHandler); if (ImGui::Begin(processedTitle.c_str(), &open, ImGuiWindowFlags_NoCollapse)) { if (!quest.completionText.empty()) { @@ -4545,11 +4562,17 @@ void GameScreen::renderQuestRequestItemsWindow(game::GameHandler& gameHandler) { ImGui::Separator(); ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Required Items:"); for (const auto& item : quest.requiredItems) { + uint32_t have = countItemInInventory(item.itemId); + bool enough = have >= item.count; auto* info = gameHandler.getItemInfo(item.itemId); - if (info && info->valid) - ImGui::Text(" %s x%u", info->name.c_str(), item.count); - else - ImGui::Text(" Item %u x%u", item.itemId, item.count); + const char* name = (info && info->valid) ? info->name.c_str() : nullptr; + if (name && *name) { + ImGui::TextColored(enough ? ImVec4(0.6f, 1.0f, 0.6f, 1.0f) : ImVec4(1.0f, 0.6f, 0.6f, 1.0f), + " %s %u/%u", name, have, item.count); + } else { + ImGui::TextColored(enough ? ImVec4(0.6f, 1.0f, 0.6f, 1.0f) : ImVec4(1.0f, 0.6f, 0.6f, 1.0f), + " Item %u %u/%u", item.itemId, have, item.count); + } } } @@ -4566,19 +4589,17 @@ void GameScreen::renderQuestRequestItemsWindow(game::GameHandler& gameHandler) { ImGui::Separator(); ImGui::Spacing(); float buttonW = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * 0.5f; - if (quest.isCompletable()) { - if (ImGui::Button("Complete Quest", ImVec2(buttonW, 0))) { - gameHandler.completeQuest(); - } - } else { - ImGui::BeginDisabled(); - ImGui::Button("Incomplete", ImVec2(buttonW, 0)); - ImGui::EndDisabled(); + if (ImGui::Button("Complete Quest", ImVec2(buttonW, 0))) { + gameHandler.completeQuest(); } ImGui::SameLine(); if (ImGui::Button("Cancel", ImVec2(buttonW, 0))) { gameHandler.closeQuestRequestItems(); } + + if (!quest.isCompletable()) { + ImGui::TextDisabled("Server flagged this quest as incomplete; completion will be server-validated."); + } } ImGui::End(); diff --git a/src/ui/quest_log_screen.cpp b/src/ui/quest_log_screen.cpp index 25e4d68e..efc1dc57 100644 --- a/src/ui/quest_log_screen.cpp +++ b/src/ui/quest_log_screen.cpp @@ -284,11 +284,14 @@ void QuestLogScreen::render(game::GameHandler& gameHandler) { if (clicked) { selectedIndex = static_cast(i); if (q.objectives.empty()) { - if (gameHandler.requestQuestQuery(q.questId)) { + if (!questDetailQueryNoResponse_.count(q.questId) && + gameHandler.requestQuestQuery(q.questId)) { lastDetailRequestQuestId_ = q.questId; + lastDetailRequestAt_ = ImGui::GetTime(); } } else if (lastDetailRequestQuestId_ == q.questId) { lastDetailRequestQuestId_ = 0; + questDetailQueryNoResponse_.erase(q.questId); } } ImGui::PopID(); @@ -310,23 +313,37 @@ void QuestLogScreen::render(game::GameHandler& gameHandler) { ImGui::Separator(); if (sel.objectives.empty()) { - if (lastDetailRequestQuestId_ != sel.questId) { - if (gameHandler.requestQuestQuery(sel.questId)) { - lastDetailRequestQuestId_ = sel.questId; - } + bool noResponse = questDetailQueryNoResponse_.count(sel.questId) > 0; + bool pending = noResponse ? false : gameHandler.isQuestQueryPending(sel.questId); + const bool requestTimedOut = + (lastDetailRequestQuestId_ == sel.questId) && + ((ImGui::GetTime() - lastDetailRequestAt_) > 5.0); + if (lastDetailRequestQuestId_ == sel.questId && !pending) { + lastDetailRequestQuestId_ = 0; + questDetailQueryNoResponse_.erase(sel.questId); + } else if (requestTimedOut) { + lastDetailRequestQuestId_ = 0; + pending = false; + questDetailQueryNoResponse_.insert(sel.questId); + noResponse = true; + gameHandler.clearQuestQueryPending(sel.questId); } - if (lastDetailRequestQuestId_ == sel.questId) { + if (pending) { ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.8f, 1.0f), "Loading quest details..."); } else { ImGui::TextColored(ImVec4(0.7f, 0.7f, 0.8f, 1.0f), "Quest summary not available yet."); } if (ImGui::Button("Retry Details")) { + questDetailQueryNoResponse_.erase(sel.questId); + gameHandler.clearQuestQueryPending(sel.questId); if (gameHandler.requestQuestQuery(sel.questId, true)) { lastDetailRequestQuestId_ = sel.questId; + lastDetailRequestAt_ = ImGui::GetTime(); } } } else { if (lastDetailRequestQuestId_ == sel.questId) lastDetailRequestQuestId_ = 0; + questDetailQueryNoResponse_.erase(sel.questId); ImGui::TextColored(ImVec4(0.82f, 0.9f, 1.0f, 1.0f), "Summary"); std::string processedObjectives = replaceGenderPlaceholders(sel.objectives, gameHandler); float textHeight = ImGui::GetContentRegionAvail().y * 0.45f;