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.
This commit is contained in:
Kelsi 2026-03-11 22:30:16 -07:00
parent 1693abffd3
commit c9c20ce433
3 changed files with 137 additions and 0 deletions

View file

@ -1131,6 +1131,10 @@ public:
uint32_t required = 0;
};
std::array<ItemObjective, 6> itemObjectives{}; // zeroed by default
// Reward data parsed from SMSG_QUEST_QUERY_RESPONSE
int32_t rewardMoney = 0; // copper; positive=reward, negative=cost
std::array<QuestRewardItem, 4> rewardItems{}; // guaranteed reward items
std::array<QuestRewardItem, 6> rewardChoiceItems{}; // player picks one of these
};
const std::vector<QuestLogEntry>& getQuestLog() const { return questLog_; }
void abandonQuest(uint32_t questId);

View file

@ -438,6 +438,49 @@ QuestQueryObjectives extractQuestQueryObjectives(const std::vector<uint8_t>& 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<uint32_t, 4> itemId{};
std::array<uint32_t, 4> itemCount{};
std::array<uint32_t, 6> choiceItemId{};
std::array<uint32_t, 6> choiceItemCount{};
bool valid = false;
};
static QuestQueryRewards tryParseQuestRewards(const std::vector<uint8_t>& 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<int32_t>(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;
}

View file

@ -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<uint32_t>(sel.rewardMoney) / 10000;
uint32_t rs = static_cast<uint32_t>(sel.rewardMoney % 10000) / 100;
uint32_t rc = static_cast<uint32_t>(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);