From 50f965186844d07f2a67fe79ce06b50e3303fb05 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 18 Feb 2026 05:11:29 -0800 Subject: [PATCH] Fix quest log not showing server-side quests on login MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SMSG_QUEST_QUERY_RESPONSE skipped only 18 uint32s before reading the title string, but Classic layout has 40 fields before the title (16 header + 8 reward items + 12 choice items + 4 POI fields). Reading from the wrong position landed inside reward item data where empty slots contain 0-bytes, so readString() returned "", overwriting the "Quest #N" placeholder with an empty title — making quests invisible in the UI even though they were in the quest log. Fixes: - Expansion-aware skip count: 40 for Classic/Turtle, 55 for WotLK - Guard: only update title if parsed string is non-empty and printable - Also scan quest log fields in VALUES update path (not just CREATE_OBJECT2), so quests are detected even when the server sends partial updates --- src/game/game_handler.cpp | 109 ++++++++++++++++++++++++++++---------- 1 file changed, 82 insertions(+), 27 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 8d965ece..d636b351 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1583,37 +1583,54 @@ void GameHandler::handlePacket(network::Packet& packet) { } uint32_t questId = packet.readUInt32(); - uint32_t questMethod = packet.readUInt32(); // Quest method/type + uint32_t questMethod = packet.readUInt32(); LOG_INFO(" questId=", questId, " questMethod=", questMethod); - // Parse quest title (after method comes level, flags, type, etc., then title string) - // Skip intermediate fields to get to title - if (packet.getReadPos() + 16 < packet.getSize()) { - packet.readUInt32(); // quest level - packet.readUInt32(); // min level - packet.readUInt32(); // sort ID (zone) - packet.readUInt32(); // quest type/info - packet.readUInt32(); // suggested players - packet.readUInt32(); // reputation objective faction - packet.readUInt32(); // reputation objective value - packet.readUInt32(); // required opposite faction - packet.readUInt32(); // next quest in chain - packet.readUInt32(); // XP ID - packet.readUInt32(); // reward or required money - packet.readUInt32(); // reward money max level - packet.readUInt32(); // reward spell - packet.readUInt32(); // reward spell cast - packet.readUInt32(); // reward honor - packet.readUInt32(); // reward honor multiplier - packet.readUInt32(); // source item ID - packet.readUInt32(); // quest flags - // ... there are many more fields before title, let's try to read title string - if (packet.getReadPos() + 1 < packet.getSize()) { - std::string title = packet.readString(); - LOG_INFO(" Quest title: ", title); + // SMSG_QUEST_QUERY_RESPONSE layout varies by expansion. + // + // Classic/Turtle (1.12.x) after questId+questMethod: + // 16 header uint32s (questLevel, zoneOrSort, type, suggestedPlayers, + // repFaction, repValue, nextChain, xpId, + // rewMoney, rewMoneyMax, rewSpell, rewSpellCast, + // rewHonor, rewHonorMult, srcItemId, questFlags) + // 8 reward items (4 slots × 2: itemId + count) + // 12 choice items (6 slots × 2: itemId + count) + // 4 POI uint32s (mapId, x, y, opt) + // = 40 uint32s before title string + // + // WotLK (3.3.5) after questId+questMethod: + // 21 header uint32s (adds minLevel, questInfoId, 2nd repFaction/Value, questFlags2) + // 12 reward items (4 slots × 3: itemId + count + displayId) + // 18 choice items (6 slots × 3: itemId + count + displayId) + // 4 POI uint32s + // = 55 uint32s before title string + // + // Read all numeric fields, then look for the title string. + // Using packetParsers_->questLogStride() as expansion discriminator: + // stride==3 → Classic layout (40 skips) + // stride==5 → WotLK layout (55 skips) + const bool isClassicLayout = packetParsers_ && packetParsers_->questLogStride() == 3; + const int skipCount = isClassicLayout ? 40 : 55; - // Update quest log entry with title + for (int i = 0; i < skipCount; ++i) { + packet.readUInt32(); + } + + if (packet.getReadPos() < packet.getSize()) { + std::string title = packet.readString(); + LOG_INFO(" Quest title: '", title, "'"); + + // Only update if we got a non-empty, printable title (guards against + // landing in the middle of binary reward data on wrong layouts). + bool validTitle = !title.empty(); + if (validTitle) { + for (char c : title) { + if ((unsigned char)c < 0x20 && c != '\t') { validTitle = false; break; } + } + } + + if (validTitle) { for (auto& q : questLog_) { if (q.questId == questId) { q.title = title; @@ -1621,6 +1638,8 @@ void GameHandler::handlePacket(network::Packet& packet) { break; } } + } else { + LOG_INFO(" Skipping non-printable title (wrong layout?) for quest ", questId); } } @@ -4187,6 +4206,42 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } } } + // Scan quest log fields in VALUES updates too (server may re-send them + // after quest accept, abandon, or same-map repositions). + { + const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START); + const uint8_t qStride = packetParsers_ ? packetParsers_->questLogStride() : 5; + const uint16_t ufQuestEnd = ufQuestStart + 25 * qStride; + for (const auto& [key, val] : block.fields) { + if (key >= ufQuestStart && key < ufQuestEnd && + (key - ufQuestStart) % qStride == 0) { + uint32_t qId = val; + if (qId != 0) { + bool found = false; + for (auto& q : questLog_) { + if (q.questId == qId) { found = true; break; } + } + if (!found) { + QuestLogEntry entry; + entry.questId = qId; + entry.complete = false; + entry.title = "Quest #" + std::to_string(qId); + questLog_.push_back(entry); + LOG_INFO("Quest found in VALUES update: ", qId); + if (socket) { + network::Packet qPkt(wireOpcode(Opcode::CMSG_QUEST_QUERY)); + qPkt.writeUInt32(qId); + socket->send(qPkt); + } + } + } else { + // Quest slot cleared — remove from log if present + uint16_t slot = (key - ufQuestStart) / qStride; + (void)slot; // slot index available if needed + } + } + } + } if (applyInventoryFields(block.fields)) slotsChanged = true; if (slotsChanged) rebuildOnlineInventory(); extractSkillFields(lastPlayerFields_);