From 8dd4bc80ec4fbaa73202935ef93367b29a1cfec2 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 10 Mar 2026 01:20:41 -0700 Subject: [PATCH] game: fix Classic 1.12 SMSG_TRAINER_LIST per-spell field layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Classic 1.12 trainer list entries lack the profDialog and profButton uint32 fields (8 bytes) that TBC/WotLK added before reqLevel. Instead, reqLevel immediately follows spellCost, and a trailing unk uint32 appears at the end of each entry. Parsing the WotLK format for Classic caused misalignment from the third field onward, corrupting state, cost, level, skill, and chain data for all trainer spells. - TrainerListParser::parse() gains a isClassic bool parameter (default false) - Classic path: cost(4) → reqLevel(1) → reqSkill... → chainNode3 → unk(4) - WotLK/TBC path: cost(4) → profDialog(4) → profButton(4) → reqLevel(1) → reqSkill... - handleTrainerList() passes isClassicLikeExpansion() as the flag --- include/game/world_packets.hpp | 4 +++- src/game/game_handler.cpp | 3 ++- src/game/world_packets.cpp | 40 ++++++++++++++++++++++++---------- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index 0809ac43..4d308028 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -2231,7 +2231,9 @@ struct TrainerListData { class TrainerListParser { public: - static bool parse(network::Packet& packet, TrainerListData& data); + // isClassic: Classic 1.12 per-spell layout has no profDialog/profButton fields + // (reqLevel immediately follows cost), plus a trailing unk uint32 per entry. + static bool parse(network::Packet& packet, TrainerListData& data, bool isClassic = false); }; class TrainerBuySpellPacket { diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index a22aa7c2..01679430 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -15012,7 +15012,8 @@ void GameHandler::handleListInventory(network::Packet& packet) { // ============================================================ void GameHandler::handleTrainerList(network::Packet& packet) { - if (!TrainerListParser::parse(packet, currentTrainerList_)) return; + const bool isClassic = isClassicLikeExpansion(); + if (!TrainerListParser::parse(packet, currentTrainerList_, isClassic)) return; trainerWindowOpen_ = true; gossipWindowOpen = false; diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 275ad5b8..c88f3750 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3833,7 +3833,11 @@ bool ListInventoryParser::parse(network::Packet& packet, ListInventoryData& data // Trainer // ============================================================ -bool TrainerListParser::parse(network::Packet& packet, TrainerListData& data) { +bool TrainerListParser::parse(network::Packet& packet, TrainerListData& data, bool isClassic) { + // WotLK per-entry: spellId(4) + state(1) + cost(4) + profDialog(4) + profButton(4) + + // reqLevel(1) + reqSkill(4) + reqSkillValue(4) + chain×3(12) = 38 bytes + // Classic per-entry: spellId(4) + state(1) + cost(4) + reqLevel(1) + + // reqSkill(4) + reqSkillValue(4) + chain×3(12) + unk(4) = 34 bytes data = TrainerListData{}; data.trainerGuid = packet.readUInt64(); data.trainerType = packet.readUInt32(); @@ -3847,23 +3851,35 @@ bool TrainerListParser::parse(network::Packet& packet, TrainerListData& data) { data.spells.reserve(spellCount); for (uint32_t i = 0; i < spellCount; ++i) { TrainerSpell spell; - spell.spellId = packet.readUInt32(); - spell.state = packet.readUInt8(); - spell.spellCost = packet.readUInt32(); - spell.profDialog = packet.readUInt32(); - spell.profButton = packet.readUInt32(); - spell.reqLevel = packet.readUInt8(); - spell.reqSkill = packet.readUInt32(); + spell.spellId = packet.readUInt32(); + spell.state = packet.readUInt8(); + spell.spellCost = packet.readUInt32(); + if (isClassic) { + // Classic 1.12: reqLevel immediately after cost; no profDialog/profButton + spell.profDialog = 0; + spell.profButton = 0; + spell.reqLevel = packet.readUInt8(); + } else { + // TBC / WotLK: profDialog + profButton before reqLevel + spell.profDialog = packet.readUInt32(); + spell.profButton = packet.readUInt32(); + spell.reqLevel = packet.readUInt8(); + } + spell.reqSkill = packet.readUInt32(); spell.reqSkillValue = packet.readUInt32(); - spell.chainNode1 = packet.readUInt32(); - spell.chainNode2 = packet.readUInt32(); - spell.chainNode3 = packet.readUInt32(); + spell.chainNode1 = packet.readUInt32(); + spell.chainNode2 = packet.readUInt32(); + spell.chainNode3 = packet.readUInt32(); + if (isClassic) { + packet.readUInt32(); // trailing unk / sort index + } data.spells.push_back(spell); } data.greeting = packet.readString(); - LOG_INFO("Trainer list: ", spellCount, " spells, type=", data.trainerType, + LOG_INFO("Trainer list (", isClassic ? "Classic" : "TBC/WotLK", "): ", + spellCount, " spells, type=", data.trainerType, ", greeting=\"", data.greeting, "\""); return true; }