From e64b566d7292ce85db40faa06f212b09645dcf85 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 11 Mar 2026 00:05:05 -0700 Subject: [PATCH] fix: correct TBC quest objective parsing and show creature names in quest log SMSG_QUEST_QUERY_RESPONSE uses 40 fixed uint32 fields + 4 strings for both Classic/Turtle and TBC, but the isClassicLayout flag was only set for stride-3 expansions (Classic/Turtle). TBC (stride 4) was incorrectly using the WotLK 55-field path, causing objective parsing to fail. - Extend isClassicLayout to cover stride <= 4 (includes TBC) - Refactor extractQuestQueryObjectives to try both layouts with fallback, matching the robustness of pickBestQuestQueryTexts - Pre-fetch creature/GO/item name queries when quest objectives are parsed so names are ready before the player opens the quest log - Quest log detail view: show creature names instead of raw entry IDs for kill objectives, and show required count (x/y) for item objectives --- src/game/game_handler.cpp | 59 ++++++++++++++++++++++++++++--------- src/ui/quest_log_screen.cpp | 9 ++++-- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 191efc37..5f4663e5 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -384,30 +384,25 @@ static uint32_t readU32At(const std::vector& d, size_t pos) { | (static_cast(d[pos + 3]) << 24); } -QuestQueryObjectives extractQuestQueryObjectives(const std::vector& data, bool classicHint) { +// Try to parse objective block starting at `startPos` with `nStrings` strings before it. +// Returns a valid QuestQueryObjectives if the data looks plausible, otherwise invalid. +static QuestQueryObjectives tryParseQuestObjectivesAt(const std::vector& data, + size_t startPos, int nStrings) { QuestQueryObjectives out; - if (data.size() < 16) return out; - - const size_t base = 8; // questId(4) + questMethod(4) already at start - // Number of fixed uint32 fields before the first string (title). - const size_t fixedFields = classicHint ? 40u : 55u; - size_t pos = base + fixedFields * 4; - - // Number of strings before the objective data. - const int nStrings = classicHint ? 4 : 5; + size_t pos = startPos; // Scan past each string (null-terminated). for (int si = 0; si < nStrings; ++si) { while (pos < data.size() && data[pos] != 0) ++pos; - if (pos >= data.size()) return out; + if (pos >= data.size()) return out; // truncated ++pos; // consume null terminator } // Read 4 entity objectives: int32 npcOrGoId + uint32 count each. for (int i = 0; i < 4; ++i) { if (pos + 8 > data.size()) return out; - out.kills[i].npcOrGoId = static_cast(readU32At(data, pos)); pos += 4; - out.kills[i].required = readU32At(data, pos); pos += 4; + out.kills[i].npcOrGoId = static_cast(readU32At(data, pos)); pos += 4; + out.kills[i].required = readU32At(data, pos); pos += 4; } // Read 6 item objectives: uint32 itemId + uint32 count each. @@ -421,6 +416,28 @@ QuestQueryObjectives extractQuestQueryObjectives(const std::vector& dat return out; } +QuestQueryObjectives extractQuestQueryObjectives(const std::vector& data, bool classicHint) { + if (data.size() < 16) return {}; + + // questId(4) + questMethod(4) prefix before the fixed integer header. + const size_t base = 8; + // Classic/TBC: 40 fixed uint32 fields + 4 strings before objectives. + // WotLK: 55 fixed uint32 fields + 5 strings before objectives. + const size_t classicStart = base + 40u * 4u; + const size_t wotlkStart = base + 55u * 4u; + + // Try the expected layout first, then fall back to the other. + if (classicHint) { + auto r = tryParseQuestObjectivesAt(data, classicStart, 4); + if (r.valid) return r; + return tryParseQuestObjectivesAt(data, wotlkStart, 5); + } else { + auto r = tryParseQuestObjectivesAt(data, wotlkStart, 5); + if (r.valid) return r; + return tryParseQuestObjectivesAt(data, classicStart, 4); + } +} + } // namespace @@ -4315,7 +4332,9 @@ void GameHandler::handlePacket(network::Packet& packet) { uint32_t questId = packet.readUInt32(); packet.readUInt32(); // questMethod - const bool isClassicLayout = packetParsers_ && packetParsers_->questLogStride() == 3; + // Classic/Turtle = stride 3, TBC = stride 4 — all use 40 fixed fields + 4 strings. + // WotLK = stride 5, uses 55 fixed fields + 5 strings. + const bool isClassicLayout = packetParsers_ && packetParsers_->questLogStride() <= 4; const QuestQueryTextCandidate parsed = pickBestQuestQueryTexts(packet.getData(), isClassicLayout); const QuestQueryObjectives objs = extractQuestQueryObjectives(packet.getData(), isClassicLayout); @@ -4355,6 +4374,18 @@ void GameHandler::handlePacket(network::Packet& packet) { // Now that we have the objective creature IDs, apply any packed kill // counts from the player update fields that arrived at login. applyPackedKillCountsFromFields(q); + // Pre-fetch creature/GO names and item info so objective display is + // populated by the time the player opens the quest log. + for (int i = 0; i < 4; ++i) { + int32_t id = objs.kills[i].npcOrGoId; + if (id == 0 || objs.kills[i].required == 0) continue; + if (id > 0) queryCreatureInfo(static_cast(id), 0); + else queryGameObjectInfo(static_cast(-id), 0); + } + for (int i = 0; i < 6; ++i) { + if (objs.items[i].itemId != 0 && objs.items[i].required != 0) + queryItemInfo(objs.items[i].itemId, 0); + } LOG_DEBUG("Quest ", questId, " objectives parsed: kills=[", objs.kills[0].npcOrGoId, "/", objs.kills[0].required, ", ", objs.kills[1].npcOrGoId, "/", objs.kills[1].required, ", ", diff --git a/src/ui/quest_log_screen.cpp b/src/ui/quest_log_screen.cpp index 00fbd173..d524d0c1 100644 --- a/src/ui/quest_log_screen.cpp +++ b/src/ui/quest_log_screen.cpp @@ -379,14 +379,19 @@ void QuestLogScreen::render(game::GameHandler& gameHandler) { 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); + std::string name = gameHandler.getCachedCreatureName(entry); + if (name.empty()) name = "Unknown (" + std::to_string(entry) + ")"; + ImGui::BulletText("%s: %u/%u", name.c_str(), 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); + uint32_t required = 1; + auto reqIt = sel.requiredItemCounts.find(itemId); + if (reqIt != sel.requiredItemCounts.end()) required = reqIt->second; + ImGui::BulletText("%s: %u/%u", itemLabel.c_str(), count, required); } }