From e8864941dc0ec9011e1006cf5ce45b7df1a6defb Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 20 Feb 2026 00:18:03 -0800 Subject: [PATCH] Fix Classic field extraction, Warden PE patches, and auth/opcode corrections Update field extraction in both CREATE_OBJECT and VALUES handlers to check specific fields (maxHealth, level, faction, etc.) before power/maxpower range checks. In Classic 1.12.1, power indices 23-27 are adjacent to maxHealth (28), and maxPower indices 29-33 are adjacent to level (34) and faction (35), so range checks like "key >= powerBase && key < powerBase+7" were incorrectly capturing those fields. Add build-aware WoW.exe selection and runtime global patching for Warden SYSTEM_INFO, EndScene, WorldEnables, and LastHardwareAction chains. Fix Classic opcodes and auth session addon data format for CMaNGOS compatibility. --- Data/expansions/classic/opcodes.json | 12 +- include/game/warden_memory.hpp | 10 +- src/game/game_handler.cpp | 51 +++++--- src/game/warden_memory.cpp | 188 ++++++++++++++++++++++++++- src/game/world_packets.cpp | 67 +++++++--- 5 files changed, 279 insertions(+), 49 deletions(-) diff --git a/Data/expansions/classic/opcodes.json b/Data/expansions/classic/opcodes.json index be864575..66804722 100644 --- a/Data/expansions/classic/opcodes.json +++ b/Data/expansions/classic/opcodes.json @@ -70,14 +70,14 @@ "CMSG_GUILD_MOTD": "0x091", "SMSG_GUILD_INFO": "0x088", "SMSG_GUILD_ROSTER": "0x08A", - "CMSG_GUILD_QUERY": "0x051", - "SMSG_GUILD_QUERY_RESPONSE": "0x052", + "CMSG_GUILD_QUERY": "0x054", + "SMSG_GUILD_QUERY_RESPONSE": "0x055", "SMSG_GUILD_INVITE": "0x083", "CMSG_GUILD_REMOVE": "0x08E", "SMSG_GUILD_EVENT": "0x092", "SMSG_GUILD_COMMAND_RESULT": "0x093", "MSG_RAID_READY_CHECK": "0x322", - "CMSG_DUEL_PROPOSED": "0x166", + "SMSG_ITEM_PUSH_RESULT": "0x166", "CMSG_DUEL_ACCEPTED": "0x16C", "CMSG_DUEL_CANCELLED": "0x16D", "SMSG_DUEL_REQUESTED": "0x167", @@ -94,7 +94,7 @@ "CMSG_BINDER_ACTIVATE": "0x1B5", "SMSG_LOG_XPGAIN": "0x1D0", "SMSG_MONSTER_MOVE": "0x0DD", - "SMSG_COMPRESSED_MOVES": "0x06B", + "SMSG_COMPRESSED_MOVES": "0x2FB", "CMSG_ATTACKSWING": "0x141", "CMSG_ATTACKSTOP": "0x142", "SMSG_ATTACKSTART": "0x143", @@ -179,7 +179,7 @@ "CMSG_SELL_ITEM": "0x1A0", "SMSG_SELL_ITEM": "0x1A1", "CMSG_BUY_ITEM": "0x1A2", - "CMSG_BUYBACK_ITEM": "0x1A6", + "CMSG_BUYBACK_ITEM": "0x290", "SMSG_BUY_FAILED": "0x1A5", "CMSG_TRAINER_LIST": "0x1B0", "SMSG_TRAINER_LIST": "0x1B1", @@ -241,5 +241,5 @@ "SMSG_CHANNEL_NOTIFY": "0x099", "CMSG_CHANNEL_LIST": "0x09A", "SMSG_CHANNEL_LIST": "0x09B", - "SMSG_INSPECT_TALENT": "0x3F4" + "SMSG_DUEL_REQUESTED": "0x167" } diff --git a/include/game/warden_memory.hpp b/include/game/warden_memory.hpp index 1c03018b..335ad7a9 100644 --- a/include/game/warden_memory.hpp +++ b/include/game/warden_memory.hpp @@ -17,8 +17,9 @@ public: WardenMemory(); ~WardenMemory(); - /** Search standard candidate dirs for WoW.exe and load it. */ - bool load(); + /** Search standard candidate dirs for WoW.exe and load it. + * @param build Client build number (e.g. 5875 for Classic 1.12.1) to select the right exe. */ + bool load(uint16_t build = 0); /** Load PE image from a specific file path. */ bool loadFromFile(const std::string& exePath); @@ -44,7 +45,10 @@ private: bool parsePE(const std::vector& fileData); void initKuserSharedData(); - std::string findWowExe() const; + void patchRuntimeGlobals(); + void writeLE32(uint32_t va, uint32_t value); + std::string findWowExe(uint16_t build) const; + static uint32_t expectedImageSizeForBuild(uint16_t build); }; } // namespace game diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index a86500ef..a6e10f3d 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3431,12 +3431,12 @@ void GameHandler::handleWardenData(network::Packet& packet) { // Lazy-load WoW.exe PE image on first MEM_CHECK if (!wardenMemory_) { wardenMemory_ = std::make_unique(); - if (!wardenMemory_->load()) { + if (!wardenMemory_->load(static_cast(build))) { LOG_WARNING("Warden: Could not load WoW.exe for MEM_CHECK"); } } - // Read real bytes from PE image (falls back to zeros if unavailable) + // Read bytes from PE image (includes patched runtime globals) std::vector memBuf(readLen, 0); if (wardenMemory_->isLoaded() && wardenMemory_->readMemory(offset, readLen, memBuf.data())) { LOG_INFO("Warden: MEM_CHECK served from PE image"); @@ -4161,6 +4161,11 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { const uint16_t ufNpcFlags = fieldIndex(UF::UNIT_NPC_FLAGS); const uint16_t ufBytes0 = fieldIndex(UF::UNIT_FIELD_BYTES_0); for (const auto& [key, val] : block.fields) { + // Check all specific fields BEFORE power/maxpower range checks. + // In Classic, power indices (23-27) are adjacent to maxHealth (28), + // and maxPower indices (29-33) are adjacent to level (34) and faction (35). + // A range check like "key >= powerBase && key < powerBase+7" would + // incorrectly capture maxHealth/level/faction in Classic's tight layout. if (key == ufHealth) { unit->setHealth(val); if (block.objectType == ObjectType::UNIT && val == 0) { @@ -4170,16 +4175,15 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { playerDead_ = true; LOG_INFO("Player logged in dead"); } - } else if (key == ufBytes0) { - unit->setPowerType(static_cast((val >> 24) & 0xFF)); - } else if (key >= ufPowerBase && key < ufPowerBase + 7) { - unit->setPowerByType(static_cast(key - ufPowerBase), val); } else if (key == ufMaxHealth) { unit->setMaxHealth(val); } - else if (key >= ufMaxPowerBase && key < ufMaxPowerBase + 7) { - unit->setMaxPowerByType(static_cast(key - ufMaxPowerBase), val); - } - else if (key == ufFaction) { unit->setFactionTemplate(val); } + else if (key == ufLevel) { + unit->setLevel(val); + } else if (key == ufFaction) { unit->setFactionTemplate(val); } else if (key == ufFlags) { unit->setUnitFlags(val); } + else if (key == ufBytes0) { + unit->setPowerType(static_cast((val >> 24) & 0xFF)); + } else if (key == ufDisplayId) { unit->setDisplayId(val); } + else if (key == ufNpcFlags) { unit->setNpcFlags(val); } else if (key == ufDynFlags) { unit->setDynamicFlags(val); if (block.objectType == ObjectType::UNIT && @@ -4187,8 +4191,12 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { unitInitiallyDead = true; } } - else if (key == ufLevel) { unit->setLevel(val); } - else if (key == ufDisplayId) { unit->setDisplayId(val); } + // Power/maxpower range checks AFTER all specific fields + else if (key >= ufPowerBase && key < ufPowerBase + 7) { + unit->setPowerByType(static_cast(key - ufPowerBase), val); + } else if (key >= ufMaxPowerBase && key < ufMaxPowerBase + 7) { + unit->setMaxPowerByType(static_cast(key - ufMaxPowerBase), val); + } else if (key == ufMountDisplayId) { if (block.guid == playerGuid) { uint32_t old = currentMountDisplayId_; @@ -4557,15 +4565,12 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { npcRespawnNotified = true; } } - } else if (key == ufBytes0) { - unit->setPowerType(static_cast((val >> 24) & 0xFF)); - } else if (key >= ufPowerBase && key < ufPowerBase + 7) { - unit->setPowerByType(static_cast(key - ufPowerBase), val); + // Specific fields checked BEFORE power/maxpower range checks + // (Classic packs maxHealth/level/faction adjacent to power indices) } else if (key == ufMaxHealth) { unit->setMaxHealth(val); } - else if (key >= ufMaxPowerBase && key < ufMaxPowerBase + 7) { - unit->setMaxPowerByType(static_cast(key - ufMaxPowerBase), val); - } - else if (key == ufFlags) { unit->setUnitFlags(val); } + else if (key == ufBytes0) { + unit->setPowerType(static_cast((val >> 24) & 0xFF)); + } else if (key == ufFlags) { unit->setUnitFlags(val); } else if (key == ufDynFlags) { uint32_t oldDyn = unit->getDynamicFlags(); unit->setDynamicFlags(val); @@ -4648,6 +4653,12 @@ void GameHandler::handleUpdateObject(network::Packet& packet) { } unit->setMountDisplayId(val); } else if (key == ufNpcFlags) { unit->setNpcFlags(val); } + // Power/maxpower range checks AFTER all specific fields + else if (key >= ufPowerBase && key < ufPowerBase + 7) { + unit->setPowerByType(static_cast(key - ufPowerBase), val); + } else if (key >= ufMaxPowerBase && key < ufMaxPowerBase + 7) { + unit->setMaxPowerByType(static_cast(key - ufMaxPowerBase), val); + } } // Some units/players are created without displayId and get it later via VALUES. diff --git a/src/game/warden_memory.cpp b/src/game/warden_memory.cpp index a612b4bd..257b3d58 100644 --- a/src/game/warden_memory.cpp +++ b/src/game/warden_memory.cpp @@ -121,6 +121,146 @@ void WardenMemory::initKuserSharedData() { std::memcpy(kuserData_ + 0x0270, &ntMinor, 4); } +void WardenMemory::writeLE32(uint32_t va, uint32_t value) { + if (va < imageBase_) return; + uint32_t rva = va - imageBase_; + if (rva + 4 > imageSize_) return; + image_[rva] = value & 0xFF; + image_[rva+1] = (value >> 8) & 0xFF; + image_[rva+2] = (value >> 16) & 0xFF; + image_[rva+3] = (value >> 24) & 0xFF; +} + +void WardenMemory::patchRuntimeGlobals() { + // Only patch Classic 1.12.1 (build 5875) WoW.exe + // Identified by: ImageBase=0x400000, ImageSize=0x906000 (unique to 1.12.1) + // Other expansions have different image sizes and different global addresses. + if (imageBase_ != 0x00400000 || imageSize_ != 0x00906000) { + LOG_INFO("WardenMemory: Not Classic 1.12.1 WoW.exe (imageSize=0x", + std::hex, imageSize_, std::dec, "), skipping runtime global patches"); + return; + } + + // Classic 1.12.1 (build 5875) runtime globals + // These are in the .data BSS region - zero on disk, populated at runtime. + // We patch them with fake but valid values so Warden checks pass. + // + // Offsets from CMaNGOS anticheat module (wardenwin.cpp): + // WardenModule = 0xCE897C + // OfsWardenSysInfo = 0x228 + // OfsWardenWinSysInfo = 0x08 + // g_theGxDevicePtr = 0xC0ED38 + // OfsDevice2 = 0x38A8 + // OfsDevice3 = 0x0 + // OfsDevice4 = 0xA8 + // WorldEnables = 0xC7B2A4 + // LastHardwareAction= 0xCF0BC8 + + // === Warden SYSTEM_INFO chain (3-level pointer chain) === + // Stage 0: [0xCE897C] → fake warden struct base + constexpr uint32_t WARDEN_MODULE_PTR = 0xCE897C; + constexpr uint32_t FAKE_WARDEN_BASE = 0xCE8000; + writeLE32(WARDEN_MODULE_PTR, FAKE_WARDEN_BASE); + + // Stage 1: [FAKE_WARDEN_BASE + 0x228] → pointer to sysinfo container + constexpr uint32_t OFS_WARDEN_SYSINFO = 0x228; + constexpr uint32_t FAKE_SYSINFO_CONTAINER = 0xCE8300; + writeLE32(FAKE_WARDEN_BASE + OFS_WARDEN_SYSINFO, FAKE_SYSINFO_CONTAINER); + + // Stage 2: [FAKE_SYSINFO_CONTAINER + 0x08] → 36-byte SYSTEM_INFO struct + constexpr uint32_t OFS_WARDEN_WIN_SYSINFO = 0x08; + uint32_t sysInfoAddr = FAKE_SYSINFO_CONTAINER + OFS_WARDEN_WIN_SYSINFO; // 0xCE8308 + // WIN_SYSTEM_INFO is 36 bytes (0x24): + // uint16 wProcessorArchitecture (must be 0 = x86) + // uint16 wReserved + // uint32 dwPageSize + // uint32 lpMinimumApplicationAddress + // uint32 lpMaximumApplicationAddress (MUST be non-zero!) + // uint32 dwActiveProcessorMask + // uint32 dwNumberOfProcessors + // uint32 dwProcessorType (must be 386, 486, or 586) + // uint32 dwAllocationGranularity + // uint16 wProcessorLevel + // uint16 wProcessorRevision + struct { + uint16_t wProcessorArchitecture; + uint16_t wReserved; + uint32_t dwPageSize; + uint32_t lpMinimumApplicationAddress; + uint32_t lpMaximumApplicationAddress; + uint32_t dwActiveProcessorMask; + uint32_t dwNumberOfProcessors; + uint32_t dwProcessorType; + uint32_t dwAllocationGranularity; + uint16_t wProcessorLevel; + uint16_t wProcessorRevision; + } __attribute__((packed)) sysInfo = { + 0, // x86 + 0, + 4096, // 4K page size + 0x00010000, // min app address + 0x7FFEFFFF, // max app address (CRITICAL: must be non-zero) + 0x0F, // 4 processors + 4, // 4 CPUs + 586, // Pentium + 65536, // 64K granularity + 6, // P6 family + 0x3A09 // revision + }; + static_assert(sizeof(sysInfo) == 36, "SYSTEM_INFO must be 36 bytes"); + uint32_t rva = sysInfoAddr - imageBase_; + if (rva + 36 <= imageSize_) { + std::memcpy(image_.data() + rva, &sysInfo, 36); + } + + LOG_INFO("WardenMemory: Patched SYSTEM_INFO chain: [0x", std::hex, + WARDEN_MODULE_PTR, "]→0x", FAKE_WARDEN_BASE, + " [0x", FAKE_WARDEN_BASE + OFS_WARDEN_SYSINFO, "]→0x", FAKE_SYSINFO_CONTAINER, + " SYSTEM_INFO@0x", sysInfoAddr, std::dec); + + // === EndScene chain (4-level pointer chain) === + // Stage 1: [0xC0ED38] → fake D3D device + constexpr uint32_t GX_DEVICE_PTR = 0xC0ED38; + constexpr uint32_t FAKE_DEVICE = 0xCE8400; + writeLE32(GX_DEVICE_PTR, FAKE_DEVICE); + + // Stage 2: [FAKE_DEVICE + 0x38A8] → fake intermediate + constexpr uint32_t OFS_DEVICE2 = 0x38A8; + constexpr uint32_t FAKE_INTERMEDIATE = 0xCE8500; + writeLE32(FAKE_DEVICE + OFS_DEVICE2, FAKE_INTERMEDIATE); + + // Stage 3: [FAKE_INTERMEDIATE + 0x0] → fake vtable + constexpr uint32_t OFS_DEVICE3 = 0x0; + constexpr uint32_t FAKE_VTABLE = 0xCE8600; + writeLE32(FAKE_INTERMEDIATE + OFS_DEVICE3, FAKE_VTABLE); + + // Stage 4: [FAKE_VTABLE + 0xA8] → address of "EndScene" function + // Point to a real .text address with normal code (not 0xE9/0xCC = not hooked) + constexpr uint32_t OFS_DEVICE4 = 0xA8; + constexpr uint32_t FAKE_ENDSCENE = 0x00401000; // Start of .text section + writeLE32(FAKE_VTABLE + OFS_DEVICE4, FAKE_ENDSCENE); + + LOG_INFO("WardenMemory: Patched EndScene chain: [0x", std::hex, + GX_DEVICE_PTR, "]→0x", FAKE_DEVICE, + " ... →EndScene@0x", FAKE_ENDSCENE, std::dec); + + // === WorldEnables (single value) === + // Required flags: TerrainDoodads|Terrain|MapObjects|MapObjectLighting|MapObjectTextures|Water + // Plus typical defaults (no Prohibited bits set) + constexpr uint32_t WORLD_ENABLES = 0xC7B2A4; + uint32_t enables = 0x1 | 0x2 | 0x10 | 0x20 | 0x40 | 0x100 | 0x200 | 0x400 | 0x800 + | 0x8000 | 0x10000 | 0x100000 | 0x1000000 | 0x2000000 + | 0x4000000 | 0x8000000 | 0x10000000; + writeLE32(WORLD_ENABLES, enables); + LOG_INFO("WardenMemory: Patched WorldEnables=0x", std::hex, enables, std::dec); + + // === LastHardwareAction (tick count) === + // Must be <= currentTime from timing check. Set to a plausible value. + constexpr uint32_t LAST_HARDWARE_ACTION = 0xCF0BC8; + writeLE32(LAST_HARDWARE_ACTION, 60000); // 1 minute + LOG_INFO("WardenMemory: Patched LastHardwareAction=60000ms"); +} + bool WardenMemory::readMemory(uint32_t va, uint8_t length, uint8_t* outBuf) const { if (length == 0) return true; @@ -139,36 +279,73 @@ bool WardenMemory::readMemory(uint32_t va, uint8_t length, uint8_t* outBuf) cons return true; } -std::string WardenMemory::findWowExe() const { +uint32_t WardenMemory::expectedImageSizeForBuild(uint16_t build) { + switch (build) { + case 5875: return 0x00906000; // Classic 1.12.1 + default: return 0; // Unknown — accept any + } +} + +std::string WardenMemory::findWowExe(uint16_t build) const { std::vector candidateDirs; if (const char* env = std::getenv("WOWEE_INTEGRITY_DIR")) { if (env && *env) candidateDirs.push_back(env); } - candidateDirs.push_back("Data/misc"); if (const char* home = std::getenv("HOME")) { if (home && *home) { + candidateDirs.push_back(std::string(home) + "/Downloads"); candidateDirs.push_back(std::string(home) + "/Downloads/twmoa_1180"); candidateDirs.push_back(std::string(home) + "/twmoa_1180"); } } + candidateDirs.push_back("Data/misc"); const char* candidateExes[] = { "WoW.exe", "TurtleWoW.exe", "Wow.exe" }; + // Collect all candidate paths + std::vector allPaths; for (const auto& dir : candidateDirs) { for (const char* exe : candidateExes) { std::string path = dir; if (!path.empty() && path.back() != '/') path += '/'; path += exe; if (std::filesystem::exists(path)) { + allPaths.push_back(path); + } + } + } + + // If we know the expected imageSize for this build, try to find a matching PE + uint32_t expectedSize = expectedImageSizeForBuild(build); + if (expectedSize != 0 && allPaths.size() > 1) { + for (const auto& path : allPaths) { + std::ifstream f(path, std::ios::binary); + if (!f.is_open()) continue; + // Read PE headers to get imageSize + f.seekg(0, std::ios::end); + auto fileSize = f.tellg(); + if (fileSize < 256) continue; + f.seekg(0x3C); + uint32_t peOfs = 0; + f.read(reinterpret_cast(&peOfs), 4); + if (peOfs + 4 + 20 + 60 > static_cast(fileSize)) continue; + f.seekg(peOfs + 4 + 20 + 56); // OptionalHeader + 56 = SizeOfImage + uint32_t imgSize = 0; + f.read(reinterpret_cast(&imgSize), 4); + if (imgSize == expectedSize) { + LOG_INFO("WardenMemory: Matched build ", build, " to ", path, + " (imageSize=0x", std::hex, imgSize, std::dec, ")"); return path; } } } - return ""; + + // Fallback: return first available + return allPaths.empty() ? "" : allPaths[0]; } -bool WardenMemory::load() { - std::string path = findWowExe(); +bool WardenMemory::load(uint16_t build) { + std::string path = findWowExe(build); if (path.empty()) { LOG_WARNING("WardenMemory: WoW.exe not found in any candidate directory"); return false; @@ -197,6 +374,7 @@ bool WardenMemory::loadFromFile(const std::string& exePath) { } initKuserSharedData(); + patchRuntimeGlobals(); loaded_ = true; LOG_INFO("WardenMemory: Loaded PE image (", fileData.size(), " bytes on disk, ", imageSize_, " bytes virtual)"); diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index fc0af398..8fa3767a 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -73,26 +73,57 @@ network::Packet AuthSessionPacket::build(uint32_t build, // Authentication hash/digest (20 bytes) packet.writeBytes(authHash.data(), authHash.size()); - // Addon info - compressed block with 0 addons - // AzerothCore format: uint32 decompressedSize + zlib compressed data - // Decompressed format: uint32 addonCount + [addons...] + uint32 clientTime - uint8_t addonData[8] = { - 0, 0, 0, 0, // addon count = 0 - 0, 0, 0, 0 // client time = 0 - }; - uint32_t decompressedSize = 8; + // Addon info - compressed block + // Format differs between expansions: + // Vanilla/TBC (CMaNGOS): while-loop of {string name, uint8 flags, uint32 modulusCRC, uint32 urlCRC} + // WotLK (AzerothCore): uint32 addonCount + {string name, uint8 enabled, uint32 crc, uint32 unk} + uint32 clientTime + std::vector addonData; + if (isTbc) { + // Vanilla/TBC: each addon entry = null-terminated name + uint8 flags + uint32 modulusCRC + uint32 urlCRC + // Send standard Blizzard addons that CMaNGOS anticheat expects for fingerprinting + static const char* vanillaAddons[] = { + "Blizzard_AuctionUI", "Blizzard_BattlefieldMinimap", "Blizzard_BindingUI", + "Blizzard_CombatText", "Blizzard_CraftUI", "Blizzard_GMSurveyUI", + "Blizzard_InspectUI", "Blizzard_MacroUI", "Blizzard_RaidUI", + "Blizzard_TalentUI", "Blizzard_TradeSkillUI", "Blizzard_TrainerUI" + }; + static const uint32_t standardModulusCRC = 0x4C1C776D; + for (const char* name : vanillaAddons) { + // string (null-terminated) + size_t len = strlen(name); + addonData.insert(addonData.end(), reinterpret_cast(name), + reinterpret_cast(name) + len + 1); + // uint8 flags = 1 (enabled) + addonData.push_back(0x01); + // uint32 modulusCRC (little-endian) + addonData.push_back(static_cast(standardModulusCRC & 0xFF)); + addonData.push_back(static_cast((standardModulusCRC >> 8) & 0xFF)); + addonData.push_back(static_cast((standardModulusCRC >> 16) & 0xFF)); + addonData.push_back(static_cast((standardModulusCRC >> 24) & 0xFF)); + // uint32 urlCRC = 0 + addonData.push_back(0); addonData.push_back(0); + addonData.push_back(0); addonData.push_back(0); + } + } else { + // WotLK: uint32 addonCount + entries + uint32 clientTime + // Send 0 addons + addonData = { 0, 0, 0, 0, // addonCount = 0 + 0, 0, 0, 0 }; // clientTime = 0 + } + uint32_t decompressedSize = static_cast(addonData.size()); // Compress with zlib uLongf compressedSize = compressBound(decompressedSize); std::vector compressed(compressedSize); - int ret = compress(compressed.data(), &compressedSize, addonData, decompressedSize); + int ret = compress(compressed.data(), &compressedSize, addonData.data(), decompressedSize); if (ret == Z_OK) { compressed.resize(compressedSize); // Write decompressedSize, then compressed bytes packet.writeUInt32(decompressedSize); packet.writeBytes(compressed.data(), compressed.size()); LOG_DEBUG("Addon info: decompressedSize=", decompressedSize, - " compressedSize=", compressedSize); + " compressedSize=", compressedSize, " addons=", + isTbc ? "12 vanilla" : "0 wotlk"); } else { LOG_ERROR("zlib compress failed with code: ", ret); packet.writeUInt32(0); @@ -191,16 +222,22 @@ bool AuthChallengeParser::parse(network::Packet& packet, AuthChallengeData& data return false; } - if (packet.getSize() < 8) { - // TBC format: just the server seed (4 bytes) + if (packet.getSize() <= 4) { + // Original vanilla/TBC format: just the server seed (4 bytes) data.unknown1 = 0; data.serverSeed = packet.readUInt32(); - LOG_INFO("Parsed SMSG_AUTH_CHALLENGE (TBC format):"); + LOG_INFO("Parsed SMSG_AUTH_CHALLENGE (TBC format, 4 bytes):"); + } else if (packet.getSize() < 40) { + // Vanilla with encryption seeds (36 bytes): serverSeed + 32 bytes seeds + // No "unknown1" prefix — first uint32 IS the server seed + data.unknown1 = 0; + data.serverSeed = packet.readUInt32(); + LOG_INFO("Parsed SMSG_AUTH_CHALLENGE (Classic+seeds format, ", packet.getSize(), " bytes):"); } else { - // WotLK format: unknown1 + serverSeed + encryption seeds + // WotLK format (40+ bytes): unknown1 + serverSeed + 32 bytes encryption seeds data.unknown1 = packet.readUInt32(); data.serverSeed = packet.readUInt32(); - LOG_INFO("Parsed SMSG_AUTH_CHALLENGE (WotLK format):"); + LOG_INFO("Parsed SMSG_AUTH_CHALLENGE (WotLK format, ", packet.getSize(), " bytes):"); LOG_INFO(" Unknown1: 0x", std::hex, data.unknown1, std::dec); }