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
This commit is contained in:
Kelsi 2026-03-11 00:05:05 -07:00
parent 73439a4457
commit e64b566d72
2 changed files with 52 additions and 16 deletions

View file

@ -384,30 +384,25 @@ static uint32_t readU32At(const std::vector<uint8_t>& d, size_t pos) {
| (static_cast<uint32_t>(d[pos + 3]) << 24);
}
QuestQueryObjectives extractQuestQueryObjectives(const std::vector<uint8_t>& 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<uint8_t>& 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<int32_t>(readU32At(data, pos)); pos += 4;
out.kills[i].required = readU32At(data, pos); pos += 4;
out.kills[i].npcOrGoId = static_cast<int32_t>(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<uint8_t>& dat
return out;
}
QuestQueryObjectives extractQuestQueryObjectives(const std::vector<uint8_t>& 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<uint32_t>(id), 0);
else queryGameObjectInfo(static_cast<uint32_t>(-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, ", ",

View file

@ -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);
}
}