From dd3149e3c1301ba439e447400f37e26508ffee15 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Wed, 18 Feb 2026 04:56:23 -0800 Subject: [PATCH] Fix quest system for Classic/Turtle: correct packet formats and log stride MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CMSG_QUESTGIVER_QUERY_QUEST: Classic override omits trailing unk1 byte (WotLK sends 13 bytes, Classic servers expect 12 — extra byte caused server to silently drop the packet, preventing quest details dialog) - SMSG_QUESTGIVER_QUEST_DETAILS: Classic override skips informUnit GUID (WotLK prepends 8-byte informUnit before questId; Vanilla does not) - Quest log UPDATE_OBJECT stride: use packetParsers_->questLogStride() (WotLK=5 fields/slot, Classic=3 fields/slot) - handleQuestDetails: route through packetParsers_->parseQuestDetails() - selectGossipQuest: route through packetParsers_->buildQueryQuestPacket() --- include/game/packet_parsers.hpp | 21 ++++++++++ src/game/game_handler.cpp | 11 +++-- src/game/packet_parsers_classic.cpp | 64 +++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index 46fdabcb..2f092905 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -148,6 +148,24 @@ public: return GossipMessageParser::parse(packet, data); } + // --- Quest details --- + + /** Build CMSG_QUESTGIVER_QUERY_QUEST. + * WotLK appends a trailing unk1 byte; Vanilla/Classic do not. */ + virtual network::Packet buildQueryQuestPacket(uint64_t npcGuid, uint32_t questId) { + return QuestgiverQueryQuestPacket::build(npcGuid, questId); // includes unk1 + } + + /** Parse SMSG_QUESTGIVER_QUEST_DETAILS. + * WotLK has an extra informUnit GUID before questId; Vanilla/Classic do not. */ + virtual bool parseQuestDetails(network::Packet& packet, QuestDetailsData& data) { + return QuestDetailsParser::parse(packet, data); // WotLK auto-detect + } + + /** Stride of PLAYER_QUEST_LOG fields in update-object blocks. + * WotLK: 5 fields per slot, Classic/Vanilla: 3. */ + virtual uint8_t questLogStride() const { return 5; } + // --- Quest Giver Status --- /** Read quest giver status from packet. @@ -304,6 +322,9 @@ public: network::Packet buildItemQuery(uint32_t entry, uint64_t guid) override; bool parseItemQueryResponse(network::Packet& packet, ItemQueryResponseData& data) override; uint8_t readQuestGiverStatus(network::Packet& packet) override; + network::Packet buildQueryQuestPacket(uint64_t npcGuid, uint32_t questId) override; + bool parseQuestDetails(network::Packet& packet, QuestDetailsData& data) override; + uint8_t questLogStride() const override { return 3; } }; /** diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 7aa3600c..8d965ece 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3858,7 +3858,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { const uint16_t ufPlayerLevel = fieldIndex(UF::UNIT_FIELD_LEVEL); const uint16_t ufCoinage = fieldIndex(UF::PLAYER_FIELD_COINAGE); const uint16_t ufQuestStart = fieldIndex(UF::PLAYER_QUEST_LOG_START); - const uint16_t ufQuestEnd = ufQuestStart + 25 * 5; // 25 quest slots, stride 5 + const uint8_t qStride = packetParsers_ ? packetParsers_->questLogStride() : 5; + const uint16_t ufQuestEnd = ufQuestStart + 25 * qStride; // 25 quest slots for (const auto& [key, val] : block.fields) { if (key == ufPlayerXp) { playerXp_ = val; } else if (key == ufPlayerNextXp) { playerNextLevelXp_ = val; } @@ -3872,8 +3873,8 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { playerMoneyCopper_ = val; LOG_INFO("Money set from update fields: ", val, " copper"); } - // Parse quest log fields (stride 5, 25 slots) - else if (key >= ufQuestStart && key < ufQuestEnd && (key - ufQuestStart) % 5 == 0) { + // Parse quest log fields (stride varies by expansion: 5=WotLK, 3=Classic) + else if (key >= ufQuestStart && key < ufQuestEnd && (key - ufQuestStart) % qStride == 0) { uint32_t questId = val; if (questId != 0) { // Check if quest is already in log @@ -8306,7 +8307,9 @@ void GameHandler::selectGossipQuest(uint32_t questId) { void GameHandler::handleQuestDetails(network::Packet& packet) { QuestDetailsData data; - if (!QuestDetailsParser::parse(packet, data)) { + bool ok = packetParsers_ ? packetParsers_->parseQuestDetails(packet, data) + : QuestDetailsParser::parse(packet, data); + if (!ok) { LOG_WARNING("Failed to parse SMSG_QUESTGIVER_QUEST_DETAILS"); return; } diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 8b75982f..d159d05c 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -1154,5 +1154,69 @@ uint8_t ClassicPacketParsers::readQuestGiverStatus(network::Packet& packet) { } } +// ============================================================================ +// Classic CMSG_QUESTGIVER_QUERY_QUEST — Vanilla 1.12 format +// WotLK appends a trailing unk1 byte; Vanilla servers don't expect it and +// some reject or misparse the 13-byte packet, preventing quest details from +// being sent back. Classic format: guid(8) + questId(4) = 12 bytes. +// ============================================================================ +network::Packet ClassicPacketParsers::buildQueryQuestPacket(uint64_t npcGuid, uint32_t questId) { + network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_QUERY_QUEST)); + packet.writeUInt64(npcGuid); + packet.writeUInt32(questId); + // No trailing unk byte (WotLK-only field) + return packet; +} + +// ============================================================================ +// Classic SMSG_QUESTGIVER_QUEST_DETAILS — Vanilla 1.12 format +// WotLK inserts an informUnit GUID (8 bytes) between npcGuid and questId. +// Vanilla has: npcGuid(8) + questId(4) + title + details + objectives + ... +// ============================================================================ +bool ClassicPacketParsers::parseQuestDetails(network::Packet& packet, QuestDetailsData& data) { + if (packet.getSize() < 16) return false; + + data.npcGuid = packet.readUInt64(); + // Vanilla: questId follows immediately — no informUnit GUID + data.questId = packet.readUInt32(); + data.title = packet.readString(); + data.details = packet.readString(); + data.objectives = packet.readString(); + + if (packet.getReadPos() + 5 > packet.getSize()) { + LOG_INFO("Quest details classic (short): id=", data.questId, " title='", data.title, "'"); + return !data.title.empty() || data.questId != 0; + } + + /*activateAccept*/ packet.readUInt8(); + data.suggestedPlayers = packet.readUInt32(); + + // Choice reward items: variable count + 3 uint32s each + 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 + } + } + + // Fixed reward items: variable count + 3 uint32s each + 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 + } + } + + if (packet.getReadPos() + 4 <= packet.getSize()) + data.rewardMoney = packet.readUInt32(); + + LOG_INFO("Quest details classic: id=", data.questId, " title='", data.title, "'"); + return true; +} + } // namespace game } // namespace wowee