feat: display quest reward items in quest details acceptance window

Parse and store reward items (choice and fixed) from SMSG_QUESTGIVER_QUEST_DETAILS
in both WotLK (QuestDetailsParser) and TBC/Classic (TbcPacketParsers) parsers.
Show item icons, names, and counts in the quest acceptance dialog alongside XP/money.
Move QuestRewardItem before QuestDetailsData in header to fix forward-reference.
This commit is contained in:
Kelsi 2026-03-10 19:05:34 -07:00
parent 9f8a0907c4
commit 1ff48259cc
4 changed files with 101 additions and 21 deletions

View file

@ -2086,6 +2086,14 @@ public:
static network::Packet build(uint64_t npcGuid, uint32_t questId);
};
/** Reward item entry (shared by quest detail/offer windows) */
struct QuestRewardItem {
uint32_t itemId = 0;
uint32_t count = 0;
uint32_t displayInfoId = 0;
uint32_t choiceSlot = 0; // Original reward slot index from server payload
};
/** SMSG_QUESTGIVER_QUEST_DETAILS data (simplified) */
struct QuestDetailsData {
uint64_t npcGuid = 0;
@ -2096,6 +2104,8 @@ struct QuestDetailsData {
uint32_t suggestedPlayers = 0;
uint32_t rewardMoney = 0;
uint32_t rewardXp = 0;
std::vector<QuestRewardItem> rewardChoiceItems; // Player picks one of these
std::vector<QuestRewardItem> rewardItems; // These are always given
};
/** SMSG_QUESTGIVER_QUEST_DETAILS parser */
@ -2104,14 +2114,6 @@ public:
static bool parse(network::Packet& packet, QuestDetailsData& data);
};
/** Reward item entry (shared by quest detail/offer windows) */
struct QuestRewardItem {
uint32_t itemId = 0;
uint32_t count = 0;
uint32_t displayInfoId = 0;
uint32_t choiceSlot = 0; // Original reward slot index from server payload
};
/** SMSG_QUESTGIVER_REQUEST_ITEMS data (turn-in progress check) */
struct QuestRequestItemsData {
uint64_t npcGuid = 0;

View file

@ -739,9 +739,15 @@ bool TbcPacketParsers::parseQuestDetails(network::Packet& packet, QuestDetailsDa
if (packet.getReadPos() + 4 <= packet.getSize()) {
uint32_t choiceCount = packet.readUInt32();
for (uint32_t i = 0; i < choiceCount && packet.getReadPos() + 12 <= packet.getSize(); ++i) {
packet.readUInt32(); // itemId
packet.readUInt32(); // count
packet.readUInt32(); // displayInfo
uint32_t itemId = packet.readUInt32();
uint32_t count = packet.readUInt32();
uint32_t dispId = packet.readUInt32();
if (itemId != 0) {
QuestRewardItem ri;
ri.itemId = itemId; ri.count = count; ri.displayInfoId = dispId;
ri.choiceSlot = i;
data.rewardChoiceItems.push_back(ri);
}
}
}
@ -749,9 +755,14 @@ bool TbcPacketParsers::parseQuestDetails(network::Packet& packet, QuestDetailsDa
if (packet.getReadPos() + 4 <= packet.getSize()) {
uint32_t rewardCount = packet.readUInt32();
for (uint32_t i = 0; i < rewardCount && packet.getReadPos() + 12 <= packet.getSize(); ++i) {
packet.readUInt32(); // itemId
packet.readUInt32(); // count
packet.readUInt32(); // displayInfo
uint32_t itemId = packet.readUInt32();
uint32_t count = packet.readUInt32();
uint32_t dispId = packet.readUInt32();
if (itemId != 0) {
QuestRewardItem ri;
ri.itemId = itemId; ri.count = count; ri.displayInfoId = dispId;
data.rewardItems.push_back(ri);
}
}
}

View file

@ -3446,9 +3446,15 @@ bool QuestDetailsParser::parse(network::Packet& packet, QuestDetailsData& data)
/*choiceCount*/ packet.readUInt32();
for (int i = 0; i < 6; i++) {
if (packet.getReadPos() + 12 > packet.getSize()) break;
packet.readUInt32(); // itemId
packet.readUInt32(); // count
packet.readUInt32(); // displayInfo
uint32_t itemId = packet.readUInt32();
uint32_t count = packet.readUInt32();
uint32_t dispId = packet.readUInt32();
if (itemId != 0) {
QuestRewardItem ri;
ri.itemId = itemId; ri.count = count; ri.displayInfoId = dispId;
ri.choiceSlot = static_cast<uint32_t>(i);
data.rewardChoiceItems.push_back(ri);
}
}
}
@ -3457,9 +3463,14 @@ bool QuestDetailsParser::parse(network::Packet& packet, QuestDetailsData& data)
/*rewardCount*/ packet.readUInt32();
for (int i = 0; i < 4; i++) {
if (packet.getReadPos() + 12 > packet.getSize()) break;
packet.readUInt32(); // itemId
packet.readUInt32(); // count
packet.readUInt32(); // displayInfo
uint32_t itemId = packet.readUInt32();
uint32_t count = packet.readUInt32();
uint32_t dispId = packet.readUInt32();
if (itemId != 0) {
QuestRewardItem ri;
ri.itemId = itemId; ri.count = count; ri.displayInfoId = dispId;
data.rewardItems.push_back(ri);
}
}
}

View file

@ -6714,7 +6714,63 @@ void GameScreen::renderQuestDetailsWindow(game::GameHandler& gameHandler) {
ImGui::TextWrapped("%s", processedObjectives.c_str());
}
// Rewards
// Choice reward items (player picks one)
if (!quest.rewardChoiceItems.empty()) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "Choose one reward:");
for (const auto& ri : quest.rewardChoiceItems) {
gameHandler.ensureItemInfo(ri.itemId);
auto* info = gameHandler.getItemInfo(ri.itemId);
VkDescriptorSet iconTex = VK_NULL_HANDLE;
uint32_t dispId = ri.displayInfoId;
if (info && info->valid && info->displayInfoId != 0) dispId = info->displayInfoId;
if (dispId != 0) iconTex = inventoryScreen.getItemIcon(dispId);
std::string label;
if (info && info->valid && !info->name.empty())
label = info->name;
else
label = "Item " + std::to_string(ri.itemId);
if (ri.count > 1) label += " x" + std::to_string(ri.count);
if (iconTex) {
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(18, 18));
ImGui::SameLine();
}
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), " %s", label.c_str());
}
}
// Fixed reward items (always given)
if (!quest.rewardItems.empty()) {
ImGui::Spacing();
ImGui::Separator();
ImGui::TextColored(ImVec4(1.0f, 0.82f, 0.0f, 1.0f), "You will receive:");
for (const auto& ri : quest.rewardItems) {
gameHandler.ensureItemInfo(ri.itemId);
auto* info = gameHandler.getItemInfo(ri.itemId);
VkDescriptorSet iconTex = VK_NULL_HANDLE;
uint32_t dispId = ri.displayInfoId;
if (info && info->valid && info->displayInfoId != 0) dispId = info->displayInfoId;
if (dispId != 0) iconTex = inventoryScreen.getItemIcon(dispId);
std::string label;
if (info && info->valid && !info->name.empty())
label = info->name;
else
label = "Item " + std::to_string(ri.itemId);
if (ri.count > 1) label += " x" + std::to_string(ri.count);
if (iconTex) {
ImGui::Image((void*)(intptr_t)iconTex, ImVec2(18, 18));
ImGui::SameLine();
}
ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 1.0f), " %s", label.c_str());
}
}
// XP and money rewards
if (quest.rewardXp > 0 || quest.rewardMoney > 0) {
ImGui::Spacing();
ImGui::Separator();