From c9c20ce433f4cff2c43a2ac2bf184cb0fdf4772d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 11 Mar 2026 22:30:16 -0700 Subject: [PATCH] Display quest rewards (money and items) in quest log details pane Parses reward money, guaranteed items, and choice items from SMSG_QUEST_QUERY_RESPONSE fixed header for both Classic/TBC (40-field) and WotLK (55-field) layouts. Rewards are shown in the quest details pane below objective progress with icons, names, and counts. --- include/game/game_handler.hpp | 4 ++ src/game/game_handler.cpp | 59 ++++++++++++++++++++++++++++ src/ui/quest_log_screen.cpp | 74 +++++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 5f6ad349..4173fbdb 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -1131,6 +1131,10 @@ public: uint32_t required = 0; }; std::array itemObjectives{}; // zeroed by default + // Reward data parsed from SMSG_QUEST_QUERY_RESPONSE + int32_t rewardMoney = 0; // copper; positive=reward, negative=cost + std::array rewardItems{}; // guaranteed reward items + std::array rewardChoiceItems{}; // player picks one of these }; const std::vector& getQuestLog() const { return questLog_; } void abandonQuest(uint32_t questId); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index b84edf19..b267f67b 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -438,6 +438,49 @@ QuestQueryObjectives extractQuestQueryObjectives(const std::vector& dat } } +// Parse quest reward fields from SMSG_QUEST_QUERY_RESPONSE fixed header. +// Classic/TBC: 40 fixed fields; WotLK: 55 fixed fields. +struct QuestQueryRewards { + int32_t rewardMoney = 0; + std::array itemId{}; + std::array itemCount{}; + std::array choiceItemId{}; + std::array choiceItemCount{}; + bool valid = false; +}; + +static QuestQueryRewards tryParseQuestRewards(const std::vector& data, + bool classicLayout) { + const size_t base = 8; // after questId(4) + questMethod(4) + const size_t fieldCount = classicLayout ? 40u : 55u; + const size_t headerEnd = base + fieldCount * 4u; + if (data.size() < headerEnd) return {}; + + // Field indices (0-based) for each expansion: + // Classic/TBC: rewardMoney=[14], rewardItemId[4]=[20..23], rewardItemCount[4]=[24..27], + // rewardChoiceItemId[6]=[28..33], rewardChoiceItemCount[6]=[34..39] + // WotLK: rewardMoney=[17], rewardItemId[4]=[30..33], rewardItemCount[4]=[34..37], + // rewardChoiceItemId[6]=[38..43], rewardChoiceItemCount[6]=[44..49] + const size_t moneyField = classicLayout ? 14u : 17u; + const size_t itemIdField = classicLayout ? 20u : 30u; + const size_t itemCountField = classicLayout ? 24u : 34u; + const size_t choiceIdField = classicLayout ? 28u : 38u; + const size_t choiceCntField = classicLayout ? 34u : 44u; + + QuestQueryRewards out; + out.rewardMoney = static_cast(readU32At(data, base + moneyField * 4u)); + for (size_t i = 0; i < 4; ++i) { + out.itemId[i] = readU32At(data, base + (itemIdField + i) * 4u); + out.itemCount[i] = readU32At(data, base + (itemCountField + i) * 4u); + } + for (size_t i = 0; i < 6; ++i) { + out.choiceItemId[i] = readU32At(data, base + (choiceIdField + i) * 4u); + out.choiceItemCount[i] = readU32At(data, base + (choiceCntField + i) * 4u); + } + out.valid = true; + return out; +} + } // namespace @@ -4529,6 +4572,7 @@ void GameHandler::handlePacket(network::Packet& packet) { const bool isClassicLayout = packetParsers_ && packetParsers_->questLogStride() <= 4; const QuestQueryTextCandidate parsed = pickBestQuestQueryTexts(packet.getData(), isClassicLayout); const QuestQueryObjectives objs = extractQuestQueryObjectives(packet.getData(), isClassicLayout); + const QuestQueryRewards rwds = tryParseQuestRewards(packet.getData(), isClassicLayout); for (auto& q : questLog_) { if (q.questId != questId) continue; @@ -4584,6 +4628,21 @@ void GameHandler::handlePacket(network::Packet& packet) { objs.kills[2].npcOrGoId, "/", objs.kills[2].required, ", ", objs.kills[3].npcOrGoId, "/", objs.kills[3].required, "]"); } + + // Store reward data and pre-fetch item info for icons. + if (rwds.valid) { + q.rewardMoney = rwds.rewardMoney; + for (int i = 0; i < 4; ++i) { + q.rewardItems[i].itemId = rwds.itemId[i]; + q.rewardItems[i].count = (rwds.itemId[i] != 0) ? rwds.itemCount[i] : 0; + if (rwds.itemId[i] != 0) queryItemInfo(rwds.itemId[i], 0); + } + for (int i = 0; i < 6; ++i) { + q.rewardChoiceItems[i].itemId = rwds.choiceItemId[i]; + q.rewardChoiceItems[i].count = (rwds.choiceItemId[i] != 0) ? rwds.choiceItemCount[i] : 0; + if (rwds.choiceItemId[i] != 0) queryItemInfo(rwds.choiceItemId[i], 0); + } + } break; } diff --git a/src/ui/quest_log_screen.cpp b/src/ui/quest_log_screen.cpp index ef54d6fc..09eca800 100644 --- a/src/ui/quest_log_screen.cpp +++ b/src/ui/quest_log_screen.cpp @@ -414,6 +414,80 @@ void QuestLogScreen::render(game::GameHandler& gameHandler, InventoryScreen& inv } } + // Reward summary + bool hasAnyReward = (sel.rewardMoney != 0); + for (const auto& ri : sel.rewardItems) if (ri.itemId) hasAnyReward = true; + for (const auto& ri : sel.rewardChoiceItems) if (ri.itemId) hasAnyReward = true; + if (hasAnyReward) { + ImGui::Separator(); + ImGui::TextColored(ImVec4(1.0f, 0.9f, 0.5f, 1.0f), "Rewards"); + + // Money reward + if (sel.rewardMoney > 0) { + uint32_t rg = static_cast(sel.rewardMoney) / 10000; + uint32_t rs = static_cast(sel.rewardMoney % 10000) / 100; + uint32_t rc = static_cast(sel.rewardMoney % 100); + if (rg > 0) + ImGui::Text("%ug %us %uc", rg, rs, rc); + else if (rs > 0) + ImGui::Text("%us %uc", rs, rc); + else + ImGui::Text("%uc", rc); + } + + // Guaranteed reward items + bool anyFixed = false; + for (const auto& ri : sel.rewardItems) if (ri.itemId) { anyFixed = true; break; } + if (anyFixed) { + ImGui::TextDisabled("You will receive:"); + for (const auto& ri : sel.rewardItems) { + if (!ri.itemId) continue; + std::string name = "Item " + std::to_string(ri.itemId); + uint32_t dispId = 0; + const auto* info = gameHandler.getItemInfo(ri.itemId); + if (info && info->valid) { + if (!info->name.empty()) name = info->name; + dispId = info->displayInfoId; + } + VkDescriptorSet icon = dispId ? invScreen.getItemIcon(dispId) : VK_NULL_HANDLE; + if (icon) { + ImGui::Image((ImTextureID)(uintptr_t)icon, ImVec2(16, 16)); + ImGui::SameLine(); + } + if (ri.count > 1) + ImGui::Text("%s x%u", name.c_str(), ri.count); + else + ImGui::Text("%s", name.c_str()); + } + } + + // Choice reward items + bool anyChoice = false; + for (const auto& ri : sel.rewardChoiceItems) if (ri.itemId) { anyChoice = true; break; } + if (anyChoice) { + ImGui::TextDisabled("Choose one of:"); + for (const auto& ri : sel.rewardChoiceItems) { + if (!ri.itemId) continue; + std::string name = "Item " + std::to_string(ri.itemId); + uint32_t dispId = 0; + const auto* info = gameHandler.getItemInfo(ri.itemId); + if (info && info->valid) { + if (!info->name.empty()) name = info->name; + dispId = info->displayInfoId; + } + VkDescriptorSet icon = dispId ? invScreen.getItemIcon(dispId) : VK_NULL_HANDLE; + if (icon) { + ImGui::Image((ImTextureID)(uintptr_t)icon, ImVec2(16, 16)); + ImGui::SameLine(); + } + if (ri.count > 1) + ImGui::Text("%s x%u", name.c_str(), ri.count); + else + ImGui::Text("%s", name.c_str()); + } + } + } + // Track / Abandon buttons ImGui::Separator(); bool isTracked = gameHandler.isQuestTracked(sel.questId);