diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index b8b99fa7..17bacc9d 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -702,6 +702,8 @@ public: bool complete = false; // Objective kill counts: objectiveIndex -> (current, required) std::unordered_map> killCounts; + // Quest item progress: itemId -> current count + std::unordered_map itemCounts; }; const std::vector& getQuestLog() const { return questLog_; } void abandonQuest(uint32_t questId); diff --git a/include/game/opcode_table.hpp b/include/game/opcode_table.hpp index 79d87b4b..e01f3335 100644 --- a/include/game/opcode_table.hpp +++ b/include/game/opcode_table.hpp @@ -257,6 +257,7 @@ enum class LogicalOpcode : uint16_t { SMSG_QUESTGIVER_QUEST_COMPLETE, CMSG_QUESTLOG_REMOVE_QUEST, SMSG_QUESTUPDATE_ADD_KILL, + SMSG_QUESTUPDATE_ADD_ITEM, SMSG_QUESTUPDATE_COMPLETE, CMSG_QUEST_QUERY, SMSG_QUEST_QUERY_RESPONSE, diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 8395702a..c3e63c5c 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1762,6 +1762,7 @@ struct LootItem { uint32_t randomSuffix = 0; uint32_t randomPropertyId = 0; uint8_t lootSlotType = 0; + bool isQuestItem = false; }; /** SMSG_LOOT_RESPONSE data */ diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 7bebf3dc..6a6f6d7e 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1456,6 +1456,30 @@ void GameHandler::handlePacket(network::Packet& packet) { } break; } + case Opcode::SMSG_QUESTUPDATE_ADD_ITEM: { + // Quest item count update: itemId + count + if (packet.getSize() - packet.getReadPos() >= 8) { + uint32_t itemId = packet.readUInt32(); + uint32_t count = packet.readUInt32(); + queryItemInfo(itemId, 0); + + std::string itemLabel = "item #" + std::to_string(itemId); + if (const ItemQueryResponseData* info = getItemInfo(itemId)) { + if (!info->name.empty()) itemLabel = info->name; + } + + bool updatedAny = false; + for (auto& quest : questLog_) { + if (quest.complete) continue; + quest.itemCounts[itemId] = count; + updatedAny = true; + } + addSystemChatMessage("Quest item: " + itemLabel + " (" + std::to_string(count) + ")"); + LOG_INFO("Quest item update: itemId=", itemId, " count=", count, + " trackedQuestsUpdated=", updatedAny); + } + break; + } case Opcode::SMSG_QUESTUPDATE_COMPLETE: { // Quest objectives completed - mark as ready to turn in uint32_t questId = packet.readUInt32(); diff --git a/src/game/opcode_table.cpp b/src/game/opcode_table.cpp index 017cf616..4cf9f314 100644 --- a/src/game/opcode_table.cpp +++ b/src/game/opcode_table.cpp @@ -208,6 +208,7 @@ static const OpcodeNameEntry kOpcodeNames[] = { {"SMSG_QUESTGIVER_QUEST_COMPLETE", LogicalOpcode::SMSG_QUESTGIVER_QUEST_COMPLETE}, {"CMSG_QUESTLOG_REMOVE_QUEST", LogicalOpcode::CMSG_QUESTLOG_REMOVE_QUEST}, {"SMSG_QUESTUPDATE_ADD_KILL", LogicalOpcode::SMSG_QUESTUPDATE_ADD_KILL}, + {"SMSG_QUESTUPDATE_ADD_ITEM", LogicalOpcode::SMSG_QUESTUPDATE_ADD_ITEM}, {"SMSG_QUESTUPDATE_COMPLETE", LogicalOpcode::SMSG_QUESTUPDATE_COMPLETE}, {"CMSG_QUEST_QUERY", LogicalOpcode::CMSG_QUEST_QUERY}, {"SMSG_QUEST_QUERY_RESPONSE", LogicalOpcode::SMSG_QUEST_QUERY_RESPONSE}, @@ -549,6 +550,7 @@ void OpcodeTable::loadWotlkDefaults() { {LogicalOpcode::SMSG_QUESTGIVER_QUEST_COMPLETE, 0x191}, {LogicalOpcode::CMSG_QUESTLOG_REMOVE_QUEST, 0x194}, {LogicalOpcode::SMSG_QUESTUPDATE_ADD_KILL, 0x196}, + {LogicalOpcode::SMSG_QUESTUPDATE_ADD_ITEM, 0x197}, {LogicalOpcode::SMSG_QUESTUPDATE_COMPLETE, 0x195}, {LogicalOpcode::CMSG_QUEST_QUERY, 0x05C}, {LogicalOpcode::SMSG_QUEST_QUERY_RESPONSE, 0x05D}, diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index c9ef2b26..e0053083 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -2861,6 +2861,12 @@ network::Packet LootReleasePacket::build(uint64_t lootGuid) { } bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data) { + data = LootResponseData{}; + if (packet.getSize() - packet.getReadPos() < 14) { + LOG_WARNING("LootResponseParser: packet too short"); + return false; + } + data.lootGuid = packet.readUInt64(); data.lootType = packet.readUInt8(); data.gold = packet.readUInt32(); @@ -2868,6 +2874,10 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data) data.items.reserve(itemCount); for (uint8_t i = 0; i < itemCount; ++i) { + if (packet.getSize() - packet.getReadPos() < 22) { + LOG_WARNING("LootResponseParser: truncated regular item list"); + return false; + } LootItem item; item.slotIndex = packet.readUInt8(); item.itemId = packet.readUInt32(); @@ -2879,7 +2889,30 @@ bool LootResponseParser::parse(network::Packet& packet, LootResponseData& data) data.items.push_back(item); } - LOG_INFO("Loot response: ", (int)itemCount, " items, ", data.gold, " copper"); + uint8_t questItemCount = 0; + if (packet.getSize() - packet.getReadPos() >= 1) { + questItemCount = packet.readUInt8(); + data.items.reserve(data.items.size() + questItemCount); + for (uint8_t i = 0; i < questItemCount; ++i) { + if (packet.getSize() - packet.getReadPos() < 22) { + LOG_WARNING("LootResponseParser: truncated quest item list"); + return false; + } + LootItem item; + item.slotIndex = packet.readUInt8(); + item.itemId = packet.readUInt32(); + item.count = packet.readUInt32(); + item.displayInfoId = packet.readUInt32(); + item.randomSuffix = packet.readUInt32(); + item.randomPropertyId = packet.readUInt32(); + item.lootSlotType = packet.readUInt8(); + item.isQuestItem = true; + data.items.push_back(item); + } + } + + LOG_INFO("Loot response: ", (int)itemCount, " regular + ", (int)questItemCount, + " quest items, ", data.gold, " copper"); return true; } diff --git a/src/ui/quest_log_screen.cpp b/src/ui/quest_log_screen.cpp index 5ac774dc..ff0bdec0 100644 --- a/src/ui/quest_log_screen.cpp +++ b/src/ui/quest_log_screen.cpp @@ -160,6 +160,21 @@ void QuestLogScreen::render(game::GameHandler& gameHandler) { ImGui::TextWrapped("%s", processedObjectives.c_str()); } + if (!sel.killCounts.empty() || !sel.itemCounts.empty()) { + ImGui::Separator(); + ImGui::TextColored(ImVec4(0.8f, 0.9f, 1.0f, 1.0f), "Tracked Progress"); + for (const auto& [entry, progress] : sel.killCounts) { + ImGui::BulletText("Kill %u: %u/%u", entry, progress.first, progress.second); + } + for (const auto& [itemId, count] : sel.itemCounts) { + std::string itemLabel = "Item " + std::to_string(itemId); + if (const auto* info = gameHandler.getItemInfo(itemId)) { + if (!info->name.empty()) itemLabel = info->name; + } + ImGui::BulletText("%s: %u", itemLabel.c_str(), count); + } + } + // Abandon button if (!sel.complete) { ImGui::Separator();