#include "game/world_packets.hpp" #include "game/packet_parsers.hpp" #include "game/opcodes.hpp" #include "game/character.hpp" #include "auth/crypto.hpp" #include "core/logger.hpp" #include #include #include #include #include #include #include #include namespace { inline uint32_t bswap32(uint32_t v) { return ((v & 0xFF000000u) >> 24) | ((v & 0x00FF0000u) >> 8) | ((v & 0x0000FF00u) << 8) | ((v & 0x000000FFu) << 24); } inline uint16_t bswap16(uint16_t v) { return static_cast(((v & 0xFF00u) >> 8) | ((v & 0x00FFu) << 8)); } const char* updateTypeName(wowee::game::UpdateType type) { using wowee::game::UpdateType; switch (type) { case UpdateType::VALUES: return "VALUES"; case UpdateType::MOVEMENT: return "MOVEMENT"; case UpdateType::CREATE_OBJECT: return "CREATE_OBJECT"; case UpdateType::CREATE_OBJECT2: return "CREATE_OBJECT2"; case UpdateType::OUT_OF_RANGE_OBJECTS: return "OUT_OF_RANGE_OBJECTS"; case UpdateType::NEAR_OBJECTS: return "NEAR_OBJECTS"; default: return "UNKNOWN"; } } } namespace wowee { namespace game { std::string normalizeWowTextTokens(std::string text) { if (text.empty()) return text; size_t pos = 0; while ((pos = text.find('$', pos)) != std::string::npos) { if (pos + 1 >= text.size()) break; const char code = text[pos + 1]; if (code == 'b' || code == 'B') { text.replace(pos, 2, "\n"); ++pos; } else { ++pos; } } pos = 0; while ((pos = text.find("|n", pos)) != std::string::npos) { text.replace(pos, 2, "\n"); ++pos; } pos = 0; while ((pos = text.find("|N", pos)) != std::string::npos) { text.replace(pos, 2, "\n"); ++pos; } return text; } network::Packet AuthSessionPacket::build(uint32_t build, const std::string& accountName, uint32_t clientSeed, const std::vector& sessionKey, uint32_t serverSeed, uint32_t realmId) { if (sessionKey.size() != 40) { LOG_ERROR("Invalid session key size: ", sessionKey.size(), " (expected 40)"); } // Convert account name to uppercase std::string upperAccount = accountName; std::transform(upperAccount.begin(), upperAccount.end(), upperAccount.begin(), [](unsigned char c) { return static_cast(std::toupper(c)); }); LOG_INFO("Building CMSG_AUTH_SESSION for account: ", upperAccount); // Compute authentication hash auto authHash = computeAuthHash(upperAccount, clientSeed, serverSeed, sessionKey); LOG_DEBUG(" Build: ", build); LOG_DEBUG(" Client seed: 0x", std::hex, clientSeed, std::dec); LOG_DEBUG(" Server seed: 0x", std::hex, serverSeed, std::dec); LOG_DEBUG(" Auth hash: ", authHash.size(), " bytes"); // Create packet (opcode will be added by WorldSocket) network::Packet packet(wireOpcode(Opcode::CMSG_AUTH_SESSION)); bool isTbc = (build <= 8606); // TBC 2.4.3 = 8606, WotLK starts at 11159+ if (isTbc) { // TBC 2.4.3 format (6 fields): // Build, ServerID, Account, ClientSeed, Digest, AddonInfo packet.writeUInt32(build); packet.writeUInt32(realmId); // server_id packet.writeString(upperAccount); packet.writeUInt32(clientSeed); } else { // WotLK 3.3.5a format (11 fields): // Build, LoginServerID, Account, LoginServerType, LocalChallenge, // RegionID, BattlegroupID, RealmID, DosResponse, Digest, AddonInfo packet.writeUInt32(build); packet.writeUInt32(0); // LoginServerID packet.writeString(upperAccount); packet.writeUInt32(0); // LoginServerType packet.writeUInt32(clientSeed); // AzerothCore ignores these fields; other cores may validate them. // Use 0 for maximum compatibility. packet.writeUInt32(0); // RegionID packet.writeUInt32(0); // BattlegroupID packet.writeUInt32(realmId); // RealmID LOG_DEBUG(" Realm ID: ", realmId); packet.writeUInt32(0); // DOS response (uint64) packet.writeUInt32(0); } // Authentication hash/digest (20 bytes) packet.writeBytes(authHash.data(), authHash.size()); // 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.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, " addons=", isTbc ? "12 vanilla" : "0 wotlk"); } else { LOG_ERROR("zlib compress failed with code: ", ret); packet.writeUInt32(0); } LOG_INFO("CMSG_AUTH_SESSION packet built: ", packet.getSize(), " bytes"); // Dump full packet for protocol debugging LOG_DEBUG("CMSG_AUTH_SESSION full dump:\n", core::toHexString(packet.getData().data(), packet.getData().size(), true)); return packet; } std::vector AuthSessionPacket::computeAuthHash( const std::string& accountName, uint32_t clientSeed, uint32_t serverSeed, const std::vector& sessionKey) { // Build hash input: // account_name + [0,0,0,0] + client_seed + server_seed + session_key std::vector hashInput; hashInput.reserve(accountName.size() + 4 + 4 + 4 + sessionKey.size()); // Account name (as bytes) hashInput.insert(hashInput.end(), accountName.begin(), accountName.end()); // 4 null bytes for (int i = 0; i < 4; ++i) { hashInput.push_back(0); } // Client seed (little-endian) hashInput.push_back(clientSeed & 0xFF); hashInput.push_back((clientSeed >> 8) & 0xFF); hashInput.push_back((clientSeed >> 16) & 0xFF); hashInput.push_back((clientSeed >> 24) & 0xFF); // Server seed (little-endian) hashInput.push_back(serverSeed & 0xFF); hashInput.push_back((serverSeed >> 8) & 0xFF); hashInput.push_back((serverSeed >> 16) & 0xFF); hashInput.push_back((serverSeed >> 24) & 0xFF); // Session key (40 bytes) hashInput.insert(hashInput.end(), sessionKey.begin(), sessionKey.end()); // Diagnostic: dump auth hash inputs for debugging AUTH_REJECT LOG_DEBUG("AUTH HASH: account='", accountName, "' clientSeed=0x", std::hex, clientSeed, " serverSeed=0x", serverSeed, std::dec); LOG_DEBUG("AUTH HASH: sessionKey=", core::toHexString(sessionKey.data(), sessionKey.size())); LOG_DEBUG("AUTH HASH: input(", hashInput.size(), ")=", core::toHexString(hashInput.data(), hashInput.size())); // Compute SHA1 hash auto result = auth::Crypto::sha1(hashInput); LOG_DEBUG("AUTH HASH: digest=", core::toHexString(result.data(), result.size())); return result; } bool AuthChallengeParser::parse(network::Packet& packet, AuthChallengeData& data) { // SMSG_AUTH_CHALLENGE format varies by expansion: // TBC 2.4.3: uint32 serverSeed (4 bytes) // WotLK 3.3.5a: uint32 one + uint32 serverSeed + seeds (40 bytes) if (packet.getSize() < 4) { LOG_ERROR("SMSG_AUTH_CHALLENGE packet too small: ", packet.getSize(), " bytes"); return false; } if (packet.getSize() <= 4) { // Original vanilla/TBC format: just the server seed (4 bytes) data.unknown1 = 0; data.serverSeed = packet.readUInt32(); LOG_INFO("SMSG_AUTH_CHALLENGE: TBC format (", packet.getSize(), " 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("SMSG_AUTH_CHALLENGE: Classic+seeds format (", packet.getSize(), " bytes)"); } else { // WotLK format (40+ bytes): unknown1 + serverSeed + 32 bytes encryption seeds data.unknown1 = packet.readUInt32(); data.serverSeed = packet.readUInt32(); LOG_INFO("SMSG_AUTH_CHALLENGE: WotLK format (", packet.getSize(), " bytes)"); LOG_DEBUG(" Unknown1: 0x", std::hex, data.unknown1, std::dec); } LOG_DEBUG(" Server seed: 0x", std::hex, data.serverSeed, std::dec); return true; } bool AuthResponseParser::parse(network::Packet& packet, AuthResponseData& response) { // SMSG_AUTH_RESPONSE format: // uint8 result if (packet.getSize() < 1) { LOG_ERROR("SMSG_AUTH_RESPONSE packet too small: ", packet.getSize(), " bytes"); return false; } uint8_t resultCode = packet.readUInt8(); response.result = static_cast(resultCode); LOG_INFO("Parsed SMSG_AUTH_RESPONSE: ", getAuthResultString(response.result)); return true; } const char* getAuthResultString(AuthResult result) { switch (result) { case AuthResult::OK: return "OK - Authentication successful"; case AuthResult::FAILED: return "FAILED - Authentication failed"; case AuthResult::REJECT: return "REJECT - Connection rejected"; case AuthResult::BAD_SERVER_PROOF: return "BAD_SERVER_PROOF - Invalid server proof"; case AuthResult::UNAVAILABLE: return "UNAVAILABLE - Server unavailable"; case AuthResult::SYSTEM_ERROR: return "SYSTEM_ERROR - System error occurred"; case AuthResult::BILLING_ERROR: return "BILLING_ERROR - Billing error"; case AuthResult::BILLING_EXPIRED: return "BILLING_EXPIRED - Subscription expired"; case AuthResult::VERSION_MISMATCH: return "VERSION_MISMATCH - Client version mismatch"; case AuthResult::UNKNOWN_ACCOUNT: return "UNKNOWN_ACCOUNT - Account not found"; case AuthResult::INCORRECT_PASSWORD: return "INCORRECT_PASSWORD - Wrong password"; case AuthResult::SESSION_EXPIRED: return "SESSION_EXPIRED - Session has expired"; case AuthResult::SERVER_SHUTTING_DOWN: return "SERVER_SHUTTING_DOWN - Server is shutting down"; case AuthResult::ALREADY_LOGGING_IN: return "ALREADY_LOGGING_IN - Already logging in"; case AuthResult::LOGIN_SERVER_NOT_FOUND: return "LOGIN_SERVER_NOT_FOUND - Can't contact login server"; case AuthResult::WAIT_QUEUE: return "WAIT_QUEUE - Waiting in queue"; case AuthResult::BANNED: return "BANNED - Account is banned"; case AuthResult::ALREADY_ONLINE: return "ALREADY_ONLINE - Character already logged in"; case AuthResult::NO_TIME: return "NO_TIME - No game time remaining"; case AuthResult::DB_BUSY: return "DB_BUSY - Database is busy"; case AuthResult::SUSPENDED: return "SUSPENDED - Account is suspended"; case AuthResult::PARENTAL_CONTROL: return "PARENTAL_CONTROL - Parental controls active"; case AuthResult::LOCKED_ENFORCED: return "LOCKED_ENFORCED - Account is locked"; default: return "UNKNOWN - Unknown result code"; } } // ============================================================ // Character Creation // ============================================================ network::Packet CharCreatePacket::build(const CharCreateData& data) { network::Packet packet(wireOpcode(Opcode::CMSG_CHAR_CREATE)); // Convert nonbinary gender to server-compatible value (servers only support male/female) Gender serverGender = toServerGender(data.gender); packet.writeString(data.name); // null-terminated name packet.writeUInt8(static_cast(data.race)); packet.writeUInt8(static_cast(data.characterClass)); packet.writeUInt8(static_cast(serverGender)); packet.writeUInt8(data.skin); packet.writeUInt8(data.face); packet.writeUInt8(data.hairStyle); packet.writeUInt8(data.hairColor); packet.writeUInt8(data.facialHair); packet.writeUInt8(0); // outfitId, always 0 // Turtle WoW / 1.12.1 clients send 4 extra zero bytes after outfitId. // Servers may validate packet length and silently drop undersized packets. packet.writeUInt32(0); LOG_DEBUG("Built CMSG_CHAR_CREATE: name=", data.name, " race=", static_cast(data.race), " class=", static_cast(data.characterClass), " gender=", static_cast(data.gender), " (server gender=", static_cast(serverGender), ")", " skin=", static_cast(data.skin), " face=", static_cast(data.face), " hair=", static_cast(data.hairStyle), " hairColor=", static_cast(data.hairColor), " facial=", static_cast(data.facialHair)); // Dump full packet for protocol debugging LOG_DEBUG("CMSG_CHAR_CREATE full dump: ", core::toHexString(packet.getData().data(), packet.getData().size(), true)); return packet; } bool CharCreateResponseParser::parse(network::Packet& packet, CharCreateResponseData& data) { // Validate minimum packet size: result(1) if (!packet.hasRemaining(1)) { LOG_WARNING("SMSG_CHAR_CREATE: packet too small (", packet.getSize(), " bytes)"); return false; } data.result = static_cast(packet.readUInt8()); LOG_INFO("SMSG_CHAR_CREATE result: ", static_cast(data.result)); return true; } network::Packet CharEnumPacket::build() { // CMSG_CHAR_ENUM has no body - just the opcode network::Packet packet(wireOpcode(Opcode::CMSG_CHAR_ENUM)); LOG_DEBUG("Built CMSG_CHAR_ENUM packet (no body)"); return packet; } bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response) { // Upfront validation: count(1) + at least minimal character data if (!packet.hasRemaining(1)) return false; // Read character count uint8_t count = packet.readUInt8(); LOG_INFO("Parsing SMSG_CHAR_ENUM: ", static_cast(count), " characters"); response.characters.clear(); response.characters.reserve(count); for (uint8_t i = 0; i < count; ++i) { Character character; // Validate minimum bytes for this character entry before reading: // GUID(8) + name(>=1 for empty string) + race(1) + class(1) + gender(1) + // appearanceBytes(4) + facialFeatures(1) + level(1) + zoneId(4) + mapId(4) + // x(4) + y(4) + z(4) + guildId(4) + flags(4) + customization(4) + unknown(1) + // petDisplayModel(4) + petLevel(4) + petFamily(4) + 23items*(dispModel(4)+invType(1)+enchant(4)) = 207 bytes const size_t minCharacterSize = 8 + 1 + 1 + 1 + 1 + 4 + 1 + 1 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 4 + 1 + 4 + 4 + 4 + (23 * 9); if (!packet.hasRemaining(minCharacterSize)) { LOG_WARNING("CharEnumParser: truncated character at index ", static_cast(i)); break; } // Read GUID (8 bytes, little-endian) character.guid = packet.readUInt64(); // Read name (null-terminated string) - validate before reading if (!packet.hasData()) { LOG_WARNING("CharEnumParser: no bytes for name at index ", static_cast(i)); break; } character.name = packet.readString(); // Validate remaining bytes before reading fixed-size fields if (!packet.hasRemaining(1)) { LOG_WARNING("CharEnumParser: truncated before race/class/gender at index ", static_cast(i)); character.race = Race::HUMAN; character.characterClass = Class::WARRIOR; character.gender = Gender::MALE; } else { // Read race, class, gender character.race = static_cast(packet.readUInt8()); if (!packet.hasRemaining(1)) { character.characterClass = Class::WARRIOR; character.gender = Gender::MALE; } else { character.characterClass = static_cast(packet.readUInt8()); if (!packet.hasRemaining(1)) { character.gender = Gender::MALE; } else { character.gender = static_cast(packet.readUInt8()); } } } // Validate before reading appearance data if (!packet.hasRemaining(4)) { character.appearanceBytes = 0; character.facialFeatures = 0; } else { // Read appearance data character.appearanceBytes = packet.readUInt32(); if (!packet.hasRemaining(1)) { character.facialFeatures = 0; } else { character.facialFeatures = packet.readUInt8(); } } // Read level if (!packet.hasRemaining(1)) { character.level = 1; } else { character.level = packet.readUInt8(); } // Read location if (!packet.hasRemaining(12)) { character.zoneId = 0; character.mapId = 0; character.x = 0.0f; character.y = 0.0f; character.z = 0.0f; } else { character.zoneId = packet.readUInt32(); character.mapId = packet.readUInt32(); character.x = packet.readFloat(); character.y = packet.readFloat(); character.z = packet.readFloat(); } // Read affiliations if (!packet.hasRemaining(4)) { character.guildId = 0; } else { character.guildId = packet.readUInt32(); } // Read flags if (!packet.hasRemaining(4)) { character.flags = 0; } else { character.flags = packet.readUInt32(); } // Skip customization flag (uint32) and unknown byte if (!packet.hasRemaining(4)) { // Customization missing, skip unknown } else { packet.readUInt32(); // Customization if (!packet.hasRemaining(1)) { // Unknown missing } else { packet.readUInt8(); // Unknown } } // Read pet data (always present, even if no pet) if (!packet.hasRemaining(12)) { character.pet.displayModel = 0; character.pet.level = 0; character.pet.family = 0; } else { character.pet.displayModel = packet.readUInt32(); character.pet.level = packet.readUInt32(); character.pet.family = packet.readUInt32(); } // Read equipment (23 items) character.equipment.reserve(23); for (int j = 0; j < 23; ++j) { if (!packet.hasRemaining(9)) break; EquipmentItem item; item.displayModel = packet.readUInt32(); item.inventoryType = packet.readUInt8(); item.enchantment = packet.readUInt32(); character.equipment.push_back(item); } LOG_DEBUG(" Character ", static_cast(i + 1), ": ", character.name, " (", getRaceName(character.race), " ", getClassName(character.characterClass), " level ", static_cast(character.level), " zone ", character.zoneId, ")"); response.characters.push_back(character); } LOG_INFO("Successfully parsed ", response.characters.size(), " characters"); return true; } network::Packet PlayerLoginPacket::build(uint64_t characterGuid) { network::Packet packet(wireOpcode(Opcode::CMSG_PLAYER_LOGIN)); // Write character GUID (8 bytes, little-endian) packet.writeUInt64(characterGuid); LOG_INFO("Built CMSG_PLAYER_LOGIN packet"); LOG_INFO(" Character GUID: 0x", std::hex, characterGuid, std::dec); return packet; } bool LoginVerifyWorldParser::parse(network::Packet& packet, LoginVerifyWorldData& data) { // SMSG_LOGIN_VERIFY_WORLD format (WoW 3.3.5a): // uint32 mapId // float x, y, z (position) // float orientation if (packet.getSize() < 20) { LOG_ERROR("SMSG_LOGIN_VERIFY_WORLD packet too small: ", packet.getSize(), " bytes"); return false; } data.mapId = packet.readUInt32(); data.x = packet.readFloat(); data.y = packet.readFloat(); data.z = packet.readFloat(); data.orientation = packet.readFloat(); LOG_INFO("Parsed SMSG_LOGIN_VERIFY_WORLD:"); LOG_INFO(" Map ID: ", data.mapId); LOG_INFO(" Position: (", data.x, ", ", data.y, ", ", data.z, ")"); LOG_INFO(" Orientation: ", data.orientation, " radians"); return true; } bool AccountDataTimesParser::parse(network::Packet& packet, AccountDataTimesData& data) { // Common layouts seen in the wild: // - WotLK-like: uint32 serverTime, uint8 unk, uint32 mask, uint32[up to 8] slotTimes // - Older/variant: uint32 serverTime, uint8 unk, uint32[up to 8] slotTimes // Some servers only send a subset of slots. if (packet.getSize() < 5) { LOG_ERROR("SMSG_ACCOUNT_DATA_TIMES packet too small: ", packet.getSize(), " bytes (need at least 5)"); return false; } for (uint32_t& t : data.accountDataTimes) { t = 0; } data.serverTime = packet.readUInt32(); data.unknown = packet.readUInt8(); size_t remaining = packet.getRemainingSize(); uint32_t mask = 0xFF; if (remaining >= 4 && ((remaining - 4) % 4) == 0) { // Treat first dword as slot mask when payload shape matches. mask = packet.readUInt32(); } remaining = packet.getRemainingSize(); size_t slotWords = std::min(8, remaining / 4); LOG_DEBUG("Parsed SMSG_ACCOUNT_DATA_TIMES:"); LOG_DEBUG(" Server time: ", data.serverTime); LOG_DEBUG(" Unknown: ", static_cast(data.unknown)); LOG_DEBUG(" Mask: 0x", std::hex, mask, std::dec, " slotsInPacket=", slotWords); for (size_t i = 0; i < slotWords; ++i) { data.accountDataTimes[i] = packet.readUInt32(); if (data.accountDataTimes[i] != 0 || ((mask & (1u << i)) != 0)) { LOG_DEBUG(" Data slot ", i, ": ", data.accountDataTimes[i]); } } if (packet.getReadPos() != packet.getSize()) { LOG_DEBUG(" AccountDataTimes trailing bytes: ", packet.getRemainingSize()); packet.skipAll(); } return true; } bool MotdParser::parse(network::Packet& packet, MotdData& data) { // SMSG_MOTD format (WoW 3.3.5a): // uint32 lineCount // string[lineCount] lines (null-terminated strings) if (packet.getSize() < 4) { LOG_ERROR("SMSG_MOTD packet too small: ", packet.getSize(), " bytes"); return false; } uint32_t lineCount = packet.readUInt32(); // Cap lineCount to prevent unbounded memory allocation const uint32_t MAX_MOTD_LINES = 64; if (lineCount > MAX_MOTD_LINES) { LOG_WARNING("MotdParser: lineCount capped (requested=", lineCount, ")"); lineCount = MAX_MOTD_LINES; } LOG_INFO("Parsed SMSG_MOTD: ", lineCount, " line(s)"); data.lines.clear(); data.lines.reserve(lineCount); for (uint32_t i = 0; i < lineCount; ++i) { // Validate at least 1 byte available for the string if (!packet.hasData()) { LOG_WARNING("MotdParser: truncated at line ", i + 1); break; } std::string line = packet.readString(); data.lines.push_back(line); LOG_DEBUG(" MOTD[", i + 1, "]: ", line); } return true; } network::Packet PingPacket::build(uint32_t sequence, uint32_t latency) { network::Packet packet(wireOpcode(Opcode::CMSG_PING)); // Write sequence number (uint32, little-endian) packet.writeUInt32(sequence); // Write latency (uint32, little-endian, in milliseconds) packet.writeUInt32(latency); LOG_DEBUG("Built CMSG_PING packet"); LOG_DEBUG(" Sequence: ", sequence); LOG_DEBUG(" Latency: ", latency, " ms"); return packet; } bool PongParser::parse(network::Packet& packet, PongData& data) { // SMSG_PONG format (WoW 3.3.5a): // uint32 sequence (echoed from CMSG_PING) if (packet.getSize() < 4) { LOG_ERROR("SMSG_PONG packet too small: ", packet.getSize(), " bytes"); return false; } data.sequence = packet.readUInt32(); LOG_DEBUG("Parsed SMSG_PONG:"); LOG_DEBUG(" Sequence: ", data.sequence); return true; } void MovementPacket::writeMovementPayload(network::Packet& packet, const MovementInfo& info) { // Movement packet format (WoW 3.3.5a) payload: // uint32 flags // uint16 flags2 // uint32 time // float x, y, z // float orientation // Write movement flags packet.writeUInt32(info.flags); packet.writeUInt16(info.flags2); // Write timestamp packet.writeUInt32(info.time); // Write position packet.writeFloat(info.x); packet.writeFloat(info.y); packet.writeFloat(info.z); // Write orientation packet.writeFloat(info.orientation); // Write transport data if on transport. // 3.3.5a ordering: transport block appears before pitch/fall/jump. if (info.hasFlag(MovementFlags::ONTRANSPORT)) { // Write packed transport GUID packet.writePackedGuid(info.transportGuid); // Write transport local position packet.writeFloat(info.transportX); packet.writeFloat(info.transportY); packet.writeFloat(info.transportZ); packet.writeFloat(info.transportO); // Write transport time packet.writeUInt32(info.transportTime); // Transport seat is always present in ONTRANSPORT movement info. packet.writeUInt8(static_cast(info.transportSeat)); // Optional second transport time for interpolated movement. if (info.flags2 & 0x0400) { // MOVEMENTFLAG2_INTERPOLATED_MOVEMENT packet.writeUInt32(info.transportTime2); } } // Write pitch if swimming/flying if (info.hasFlag(MovementFlags::SWIMMING) || info.hasFlag(MovementFlags::FLYING)) { packet.writeFloat(info.pitch); } // Fall time is ALWAYS present in the packet (server reads it unconditionally). // Jump velocity/angle data is only present when FALLING flag is set. packet.writeUInt32(info.fallTime); if (info.hasFlag(MovementFlags::FALLING)) { packet.writeFloat(info.jumpVelocity); packet.writeFloat(info.jumpSinAngle); packet.writeFloat(info.jumpCosAngle); packet.writeFloat(info.jumpXYSpeed); } } network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, uint64_t playerGuid) { network::Packet packet(wireOpcode(opcode)); // Movement packet format (WoW 3.3.5a): // packed GUID + movement payload packet.writePackedGuid(playerGuid); writeMovementPayload(packet, info); // Detailed hex dump for debugging static int mvLog = 5; if (mvLog-- > 0) { const auto& raw = packet.getData(); std::string hex; for (size_t i = 0; i < raw.size(); i++) { char b[4]; snprintf(b, sizeof(b), "%02x ", raw[i]); hex += b; } LOG_DEBUG("MOVEPKT opcode=0x", std::hex, wireOpcode(opcode), std::dec, " guid=0x", std::hex, playerGuid, std::dec, " payload=", raw.size(), " bytes", " flags=0x", std::hex, info.flags, std::dec, " flags2=0x", std::hex, info.flags2, std::dec, " pos=(", info.x, ",", info.y, ",", info.z, ",", info.orientation, ")", " fallTime=", info.fallTime, (info.hasFlag(MovementFlags::ONTRANSPORT) ? " ONTRANSPORT guid=0x" + std::to_string(info.transportGuid) + " localPos=(" + std::to_string(info.transportX) + "," + std::to_string(info.transportY) + "," + std::to_string(info.transportZ) + ")" : "")); LOG_DEBUG("MOVEPKT hex: ", hex); } return packet; } bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock& block) { // WoW 3.3.5a UPDATE_OBJECT movement block structure: // 1. UpdateFlags (1 byte, sometimes 2) // 2. Movement data depends on update flags auto rem = [&]() -> size_t { return packet.getRemainingSize(); }; if (rem() < 2) return false; // Update flags (3.3.5a uses 2 bytes for flags) uint16_t updateFlags = packet.readUInt16(); block.updateFlags = updateFlags; LOG_DEBUG(" UpdateFlags: 0x", std::hex, updateFlags, std::dec); // Log transport-related flag combinations if (updateFlags & 0x0002) { // UPDATEFLAG_TRANSPORT static int transportFlagLogCount = 0; if (transportFlagLogCount < 12) { LOG_INFO(" Transport flags detected: 0x", std::hex, updateFlags, std::dec, " (TRANSPORT=", !!(updateFlags & 0x0002), ", POSITION=", !!(updateFlags & 0x0100), ", ROTATION=", !!(updateFlags & 0x0200), ", STATIONARY=", !!(updateFlags & 0x0040), ")"); transportFlagLogCount++; } else { LOG_DEBUG(" Transport flags detected: 0x", std::hex, updateFlags, std::dec); } } // UpdateFlags bit meanings: // 0x0001 = UPDATEFLAG_SELF // 0x0002 = UPDATEFLAG_TRANSPORT // 0x0004 = UPDATEFLAG_HAS_TARGET // 0x0008 = UPDATEFLAG_LOWGUID // 0x0010 = UPDATEFLAG_HIGHGUID // 0x0020 = UPDATEFLAG_LIVING // 0x0040 = UPDATEFLAG_STATIONARY_POSITION // 0x0080 = UPDATEFLAG_VEHICLE // 0x0100 = UPDATEFLAG_POSITION (transport) // 0x0200 = UPDATEFLAG_ROTATION const uint16_t UPDATEFLAG_LIVING = 0x0020; const uint16_t UPDATEFLAG_STATIONARY_POSITION = 0x0040; const uint16_t UPDATEFLAG_HAS_TARGET = 0x0004; const uint16_t UPDATEFLAG_TRANSPORT = 0x0002; const uint16_t UPDATEFLAG_POSITION = 0x0100; const uint16_t UPDATEFLAG_VEHICLE = 0x0080; const uint16_t UPDATEFLAG_ROTATION = 0x0200; const uint16_t UPDATEFLAG_LOWGUID = 0x0008; const uint16_t UPDATEFLAG_HIGHGUID = 0x0010; if (updateFlags & UPDATEFLAG_LIVING) { // Minimum: moveFlags(4)+moveFlags2(2)+time(4)+position(16)+fallTime(4)+speeds(36) = 66 if (rem() < 66) return false; // Full movement block for living units uint32_t moveFlags = packet.readUInt32(); uint16_t moveFlags2 = packet.readUInt16(); /*uint32_t time =*/ packet.readUInt32(); // Position block.x = packet.readFloat(); block.y = packet.readFloat(); block.z = packet.readFloat(); block.orientation = packet.readFloat(); block.hasMovement = true; LOG_DEBUG(" LIVING movement: (", block.x, ", ", block.y, ", ", block.z, "), o=", block.orientation, " moveFlags=0x", std::hex, moveFlags, std::dec); // Transport data (if on transport) if (moveFlags & 0x00000200) { // MOVEMENTFLAG_ONTRANSPORT if (rem() < 1) return false; block.onTransport = true; block.transportGuid = packet.readPackedGuid(); if (rem() < 21) return false; // 4 floats + uint32 + uint8 block.transportX = packet.readFloat(); block.transportY = packet.readFloat(); block.transportZ = packet.readFloat(); block.transportO = packet.readFloat(); /*uint32_t tTime =*/ packet.readUInt32(); /*int8_t tSeat =*/ packet.readUInt8(); LOG_DEBUG(" OnTransport: guid=0x", std::hex, block.transportGuid, std::dec, " offset=(", block.transportX, ", ", block.transportY, ", ", block.transportZ, ")"); if (moveFlags2 & 0x0400) { // MOVEMENTFLAG2_INTERPOLATED_MOVEMENT if (rem() < 4) return false; /*uint32_t tTime2 =*/ packet.readUInt32(); } } // Swimming/flying pitch // WotLK 3.3.5a movement flags (wire format): // SWIMMING = 0x00200000 // CAN_FLY = 0x01000000 (ability to fly — no pitch field) // FLYING = 0x02000000 (actively flying — has pitch field) // SPLINE_ELEVATION = 0x04000000 (smooth vertical spline offset) // MovementFlags2: // MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING = 0x0020 // // Pitch is present when SWIMMING or FLYING are set, or the always-allow flag is set. // Note: CAN_FLY (0x01000000) does NOT gate pitch; only FLYING (0x02000000) does. // (TBC uses 0x01000000 for FLYING — see TbcMoveFlags in packet_parsers_tbc.cpp.) if ((moveFlags & 0x00200000) /* SWIMMING */ || (moveFlags & 0x02000000) /* FLYING */ || (moveFlags2 & 0x0020) /* MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING */) { if (rem() < 4) return false; /*float pitch =*/ packet.readFloat(); } // Fall time if (rem() < 4) return false; /*uint32_t fallTime =*/ packet.readUInt32(); // Jumping if (moveFlags & 0x00001000) { // MOVEMENTFLAG_FALLING if (rem() < 16) return false; /*float jumpVelocity =*/ packet.readFloat(); /*float jumpSinAngle =*/ packet.readFloat(); /*float jumpCosAngle =*/ packet.readFloat(); /*float jumpXYSpeed =*/ packet.readFloat(); } // Spline elevation if (moveFlags & 0x04000000) { // MOVEMENTFLAG_SPLINE_ELEVATION if (rem() < 4) return false; /*float splineElevation =*/ packet.readFloat(); } // Speeds (9 values in WotLK: walk/run/runBack/swim/swimBack/flight/flightBack/turn/pitch) if (rem() < 36) return false; /*float walkSpeed =*/ packet.readFloat(); float runSpeed = packet.readFloat(); /*float runBackSpeed =*/ packet.readFloat(); /*float swimSpeed =*/ packet.readFloat(); /*float swimBackSpeed =*/ packet.readFloat(); /*float flightSpeed =*/ packet.readFloat(); /*float flightBackSpeed =*/ packet.readFloat(); /*float turnRate =*/ packet.readFloat(); /*float pitchRate =*/ packet.readFloat(); block.runSpeed = runSpeed; block.moveFlags = moveFlags; // Spline data if (moveFlags & 0x08000000) { // MOVEMENTFLAG_SPLINE_ENABLED auto bytesAvailable = [&](size_t n) -> bool { return packet.hasRemaining(n); }; if (!bytesAvailable(4)) return false; uint32_t splineFlags = packet.readUInt32(); LOG_DEBUG(" Spline: flags=0x", std::hex, splineFlags, std::dec); if (splineFlags & 0x00010000) { // SPLINEFLAG_FINAL_POINT if (!bytesAvailable(12)) return false; /*float finalX =*/ packet.readFloat(); /*float finalY =*/ packet.readFloat(); /*float finalZ =*/ packet.readFloat(); } else if (splineFlags & 0x00020000) { // SPLINEFLAG_FINAL_TARGET if (!bytesAvailable(8)) return false; /*uint64_t finalTarget =*/ packet.readUInt64(); } else if (splineFlags & 0x00040000) { // SPLINEFLAG_FINAL_ANGLE if (!bytesAvailable(4)) return false; /*float finalAngle =*/ packet.readFloat(); } // WotLK spline data layout: // timePassed(4)+duration(4)+splineId(4)+durationMod(4)+durationModNext(4) // +[ANIMATION(5)]+verticalAccel(4)+effectStartTime(4)+pointCount(4)+points+mode(1)+endPoint(12) if (!bytesAvailable(12)) return false; /*uint32_t timePassed =*/ packet.readUInt32(); /*uint32_t duration =*/ packet.readUInt32(); /*uint32_t splineId =*/ packet.readUInt32(); // Helper: parse spline points + splineMode + endPoint. // WotLK uses compressed points by default (first=12 bytes, rest=4 bytes packed). auto tryParseSplinePoints = [&](bool compressed, const char* tag) -> bool { if (!bytesAvailable(4)) return false; size_t prePointCount = packet.getReadPos(); uint32_t pc = packet.readUInt32(); if (pc > 256) return false; size_t pointsBytes; if (compressed && pc > 0) { // First point = 3 floats (12 bytes), rest = packed uint32 (4 bytes each) pointsBytes = 12ull + (pc > 1 ? static_cast(pc - 1) * 4ull : 0ull); } else { // All uncompressed: 3 floats each pointsBytes = static_cast(pc) * 12ull; } size_t needed = pointsBytes + 13ull; // + splineMode(1) + endPoint(12) if (!bytesAvailable(needed)) { packet.setReadPos(prePointCount); return false; } packet.setReadPos(packet.getReadPos() + pointsBytes); uint8_t splineMode = packet.readUInt8(); if (splineMode > 3) { packet.setReadPos(prePointCount); return false; } float epX = packet.readFloat(); float epY = packet.readFloat(); float epZ = packet.readFloat(); // Validate endPoint: garbage bytes rarely produce finite world coords if (!std::isfinite(epX) || !std::isfinite(epY) || !std::isfinite(epZ) || std::fabs(epX) > 65000.0f || std::fabs(epY) > 65000.0f || std::fabs(epZ) > 65000.0f) { packet.setReadPos(prePointCount); return false; } LOG_DEBUG(" Spline pointCount=", pc, " compressed=", compressed, " endPt=(", epX, ",", epY, ",", epZ, ") (", tag, ")"); return true; }; // Save position before WotLK spline header for fallback size_t beforeSplineHeader = packet.getReadPos(); // Try 1: WotLK format (durationMod+durationModNext+[ANIMATION]+vertAccel+effectStart+points) bool splineParsed = false; if (bytesAvailable(8)) { /*float durationMod =*/ packet.readFloat(); /*float durationModNext =*/ packet.readFloat(); bool wotlkOk = true; if (splineFlags & 0x00400000) { // SPLINEFLAG_ANIMATION if (!bytesAvailable(5)) { wotlkOk = false; } else { packet.readUInt8(); packet.readUInt32(); } } // AzerothCore/ChromieCraft always writes verticalAcceleration(float) // + effectStartTime(uint32) unconditionally -- NOT gated by PARABOLIC flag. if (wotlkOk) { if (!bytesAvailable(8)) { wotlkOk = false; } else { /*float vertAccel =*/ packet.readFloat(); /*uint32_t effectStart =*/ packet.readUInt32(); } } if (wotlkOk) { // WotLK: compressed unless CYCLIC(0x80000) or ENTER_CYCLE(0x2000) set bool useCompressed = (splineFlags & (0x00080000 | 0x00002000)) == 0; splineParsed = tryParseSplinePoints(useCompressed, "wotlk-compressed"); if (!splineParsed) { splineParsed = tryParseSplinePoints(false, "wotlk-uncompressed"); } } } if (!splineParsed) { // WotLK compressed+uncompressed both failed. Try without the parabolic // fields (some cores don't send vertAccel+effectStart unconditionally). packet.setReadPos(beforeSplineHeader); if (bytesAvailable(8)) { packet.readFloat(); // durationMod packet.readFloat(); // durationModNext // Skip parabolic fields — try points directly splineParsed = tryParseSplinePoints(false, "wotlk-no-parabolic"); if (!splineParsed) { bool useComp = (splineFlags & (0x00080000 | 0x00002000)) == 0; splineParsed = tryParseSplinePoints(useComp, "wotlk-no-parabolic-compressed"); } } } // Try 3: bare points (no WotLK header at all — some spline types skip everything) if (!splineParsed) { packet.setReadPos(beforeSplineHeader); splineParsed = tryParseSplinePoints(false, "bare-uncompressed"); if (!splineParsed) { packet.setReadPos(beforeSplineHeader); bool useComp = (splineFlags & (0x00080000 | 0x00002000)) == 0; splineParsed = tryParseSplinePoints(useComp, "bare-compressed"); } } if (!splineParsed) { // Dump first 5 uint32s at beforeSplineHeader for format diagnosis packet.setReadPos(beforeSplineHeader); uint32_t d[5] = {}; for (int di = 0; di < 5 && packet.hasRemaining(4); ++di) d[di] = packet.readUInt32(); packet.setReadPos(beforeSplineHeader); LOG_WARNING("WotLK spline parse failed for guid=0x", std::hex, block.guid, std::dec, " splineFlags=0x", splineFlags, " remaining=", std::dec, packet.getRemainingSize(), " header=[0x", std::hex, d[0], " 0x", d[1], " 0x", d[2], " 0x", d[3], " 0x", d[4], "]", std::dec); return false; } } } else if (updateFlags & UPDATEFLAG_POSITION) { // Transport position update (UPDATEFLAG_POSITION = 0x0100) if (rem() < 1) return false; uint64_t transportGuid = packet.readPackedGuid(); if (rem() < 32) return false; // 8 floats block.x = packet.readFloat(); block.y = packet.readFloat(); block.z = packet.readFloat(); block.onTransport = (transportGuid != 0); block.transportGuid = transportGuid; float tx = packet.readFloat(); float ty = packet.readFloat(); float tz = packet.readFloat(); if (block.onTransport) { block.transportX = tx; block.transportY = ty; block.transportZ = tz; } else { block.transportX = 0.0f; block.transportY = 0.0f; block.transportZ = 0.0f; } block.orientation = packet.readFloat(); /*float corpseOrientation =*/ packet.readFloat(); block.hasMovement = true; if (block.onTransport) { LOG_DEBUG(" TRANSPORT POSITION UPDATE: guid=0x", std::hex, transportGuid, std::dec, " pos=(", block.x, ", ", block.y, ", ", block.z, "), o=", block.orientation, " offset=(", block.transportX, ", ", block.transportY, ", ", block.transportZ, ")"); } } else if (updateFlags & UPDATEFLAG_STATIONARY_POSITION) { if (rem() < 16) return false; block.x = packet.readFloat(); block.y = packet.readFloat(); block.z = packet.readFloat(); block.orientation = packet.readFloat(); block.hasMovement = true; LOG_DEBUG(" STATIONARY: (", block.x, ", ", block.y, ", ", block.z, "), o=", block.orientation); } // Target GUID (for units with target) if (updateFlags & UPDATEFLAG_HAS_TARGET) { if (rem() < 1) return false; /*uint64_t targetGuid =*/ packet.readPackedGuid(); } // Transport time if (updateFlags & UPDATEFLAG_TRANSPORT) { if (rem() < 4) return false; /*uint32_t transportTime =*/ packet.readUInt32(); } // Vehicle if (updateFlags & UPDATEFLAG_VEHICLE) { if (rem() < 8) return false; /*uint32_t vehicleId =*/ packet.readUInt32(); /*float vehicleOrientation =*/ packet.readFloat(); } // Rotation (GameObjects) if (updateFlags & UPDATEFLAG_ROTATION) { if (rem() < 8) return false; /*int64_t rotation =*/ packet.readUInt64(); } // Low GUID if (updateFlags & UPDATEFLAG_LOWGUID) { if (rem() < 4) return false; /*uint32_t lowGuid =*/ packet.readUInt32(); } // High GUID if (updateFlags & UPDATEFLAG_HIGHGUID) { if (rem() < 4) return false; /*uint32_t highGuid =*/ packet.readUInt32(); } return true; } bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& block) { size_t startPos = packet.getReadPos(); if (!packet.hasData()) return false; // Read number of blocks (each block is 32 fields = 32 bits) uint8_t blockCount = packet.readUInt8(); if (blockCount == 0) { return true; // No fields to update } // Sanity check: UNIT_END=148 needs 5 mask blocks, PLAYER_END=1472 needs 46. // VALUES updates don't carry objectType (defaults to 0), so allow up to 55 // for any VALUES update (could be a PLAYER). Only flag CREATE_OBJECT blocks // with genuinely excessive block counts. bool isCreateBlock = (block.updateType == UpdateType::CREATE_OBJECT || block.updateType == UpdateType::CREATE_OBJECT2); uint8_t maxExpectedBlocks = isCreateBlock ? ((block.objectType == ObjectType::PLAYER) ? 55 : 10) : 55; // VALUES: allow PLAYER-sized masks if (blockCount > maxExpectedBlocks) { LOG_WARNING("UpdateObjectParser: suspicious maskBlockCount=", static_cast(blockCount), " for objectType=", static_cast(block.objectType), " guid=0x", std::hex, block.guid, std::dec, " updateFlags=0x", std::hex, block.updateFlags, std::dec, " moveFlags=0x", std::hex, block.moveFlags, std::dec, " readPos=", packet.getReadPos(), " size=", packet.getSize()); // Movement data likely consumed wrong number of bytes, causing blockCount // to be read from a misaligned position. Bail out rather than reading garbage. if (isCreateBlock) return false; } uint32_t fieldsCapacity = blockCount * 32; LOG_DEBUG(" UPDATE MASK PARSE:"); LOG_DEBUG(" maskBlockCount = ", static_cast(blockCount)); LOG_DEBUG(" fieldsCapacity (blocks * 32) = ", fieldsCapacity); // Read update mask into a reused scratch buffer to avoid per-block allocations. static thread_local std::vector updateMask; updateMask.resize(blockCount); for (int i = 0; i < blockCount; ++i) { // Validate 4 bytes available before each block read if (!packet.hasRemaining(4)) { LOG_WARNING("UpdateObjectParser: truncated update mask at block ", i, " type=", updateTypeName(block.updateType), " objectType=", static_cast(block.objectType), " guid=0x", std::hex, block.guid, std::dec, " readPos=", packet.getReadPos(), " size=", packet.getSize(), " maskBlockCount=", static_cast(blockCount)); return false; } updateMask[i] = packet.readUInt32(); } // Find highest set bit uint16_t highestSetBit = 0; uint32_t valuesReadCount = 0; // Read only set bits in each mask block (faster than scanning all 32 bits). for (int blockIdx = 0; blockIdx < blockCount; ++blockIdx) { uint32_t mask = updateMask[blockIdx]; while (mask != 0) { const uint16_t fieldIndex = #if defined(__GNUC__) || defined(__clang__) static_cast(blockIdx * 32 + __builtin_ctz(mask)); #else static_cast(blockIdx * 32 + [] (uint32_t v) -> uint16_t { uint16_t b = 0; while ((v & 1u) == 0u) { v >>= 1u; ++b; } return b; }(mask)); #endif if (fieldIndex > highestSetBit) { highestSetBit = fieldIndex; } // Validate 4 bytes available before reading field value if (!packet.hasRemaining(4)) { LOG_WARNING("UpdateObjectParser: truncated field value at field ", fieldIndex, " type=", updateTypeName(block.updateType), " objectType=", static_cast(block.objectType), " guid=0x", std::hex, block.guid, std::dec, " readPos=", packet.getReadPos(), " size=", packet.getSize(), " maskBlockIndex=", blockIdx, " maskBlock=0x", std::hex, updateMask[blockIdx], std::dec); return false; } uint32_t value = packet.readUInt32(); // fieldIndex is monotonically increasing here, so end() is a good insertion hint. block.fields.emplace_hint(block.fields.end(), fieldIndex, value); valuesReadCount++; LOG_DEBUG(" Field[", fieldIndex, "] = 0x", std::hex, value, std::dec); mask &= (mask - 1u); } } size_t endPos = packet.getReadPos(); size_t bytesUsed = endPos - startPos; size_t bytesRemaining = packet.getSize() - endPos; LOG_DEBUG(" highestSetBitIndex = ", highestSetBit); LOG_DEBUG(" valuesReadCount = ", valuesReadCount); LOG_DEBUG(" bytesUsedForFields = ", bytesUsed); LOG_DEBUG(" bytesRemainingInPacket = ", bytesRemaining); LOG_DEBUG(" Parsed ", block.fields.size(), " fields"); return true; } bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& block) { if (!packet.hasData()) return false; // Read update type uint8_t updateTypeVal = packet.readUInt8(); block.updateType = static_cast(updateTypeVal); LOG_DEBUG("Update block: type=", static_cast(updateTypeVal)); switch (block.updateType) { case UpdateType::VALUES: { // Partial update - changed fields only if (!packet.hasData()) return false; block.guid = packet.readPackedGuid(); LOG_DEBUG(" VALUES update for GUID: 0x", std::hex, block.guid, std::dec); return parseUpdateFields(packet, block); } case UpdateType::MOVEMENT: { // Movement update if (!packet.hasRemaining(8)) return false; block.guid = packet.readUInt64(); LOG_DEBUG(" MOVEMENT update for GUID: 0x", std::hex, block.guid, std::dec); return parseMovementBlock(packet, block); } case UpdateType::CREATE_OBJECT: case UpdateType::CREATE_OBJECT2: { // Create new object with full data if (!packet.hasData()) return false; block.guid = packet.readPackedGuid(); LOG_DEBUG(" CREATE_OBJECT for GUID: 0x", std::hex, block.guid, std::dec); // Read object type if (!packet.hasData()) return false; uint8_t objectTypeVal = packet.readUInt8(); block.objectType = static_cast(objectTypeVal); LOG_DEBUG(" Object type: ", static_cast(objectTypeVal)); // Parse movement if present bool hasMovement = parseMovementBlock(packet, block); if (!hasMovement) { return false; } // Parse update fields return parseUpdateFields(packet, block); } case UpdateType::OUT_OF_RANGE_OBJECTS: { // Objects leaving view range - handled differently LOG_DEBUG(" OUT_OF_RANGE_OBJECTS (skipping in block parser)"); return true; } case UpdateType::NEAR_OBJECTS: { // Objects entering view range - handled differently LOG_DEBUG(" NEAR_OBJECTS (skipping in block parser)"); return true; } default: LOG_WARNING("Unknown update type: ", static_cast(updateTypeVal)); return false; } } bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) { // Keep worst-case packet parsing bounded. Extremely large counts are typically // malformed/desynced and can stall a frame long enough to trigger disconnects. constexpr uint32_t kMaxReasonableUpdateBlocks = 1024; constexpr uint32_t kMaxReasonableOutOfRangeGuids = 4096; // Read block count data.blockCount = packet.readUInt32(); if (data.blockCount > kMaxReasonableUpdateBlocks) { LOG_ERROR("SMSG_UPDATE_OBJECT rejected: unreasonable blockCount=", data.blockCount, " packetSize=", packet.getSize()); return false; } LOG_DEBUG("SMSG_UPDATE_OBJECT:"); LOG_DEBUG(" objectCount = ", data.blockCount); LOG_DEBUG(" packetSize = ", packet.getSize()); uint32_t remainingBlockCount = data.blockCount; // Check for out-of-range objects first if (packet.hasRemaining(1)) { uint8_t firstByte = packet.readUInt8(); if (firstByte == static_cast(UpdateType::OUT_OF_RANGE_OBJECTS)) { if (remainingBlockCount == 0) { LOG_ERROR("SMSG_UPDATE_OBJECT rejected: OUT_OF_RANGE_OBJECTS with zero blockCount"); return false; } --remainingBlockCount; // Read out-of-range GUID count uint32_t count = packet.readUInt32(); if (count > kMaxReasonableOutOfRangeGuids) { LOG_ERROR("SMSG_UPDATE_OBJECT rejected: unreasonable outOfRange count=", count, " packetSize=", packet.getSize()); return false; } for (uint32_t i = 0; i < count; ++i) { uint64_t guid = packet.readPackedGuid(); data.outOfRangeGuids.push_back(guid); LOG_DEBUG(" Out of range: 0x", std::hex, guid, std::dec); } // Done - packet may have more blocks after this // Reset read position to after the first byte if needed } else { // Not out-of-range, rewind packet.setReadPos(packet.getReadPos() - 1); } } // Parse update blocks data.blockCount = remainingBlockCount; data.blocks.reserve(data.blockCount); for (uint32_t i = 0; i < data.blockCount; ++i) { LOG_DEBUG("Parsing block ", i + 1, " / ", data.blockCount); UpdateBlock block; if (!parseUpdateBlock(packet, block)) { static int parseBlockErrors = 0; if (++parseBlockErrors <= 5) { LOG_ERROR("Failed to parse update block ", i + 1, " of ", data.blockCount, " (", i, " blocks parsed successfully before failure)"); if (parseBlockErrors == 5) LOG_ERROR("(suppressing further update block parse errors)"); } // Cannot reliably re-sync to the next block after a parse failure, // but still return true so the blocks already parsed are processed. break; } data.blocks.emplace_back(std::move(block)); } return true; } bool DestroyObjectParser::parse(network::Packet& packet, DestroyObjectData& data) { // SMSG_DESTROY_OBJECT format: // uint64 guid // uint8 isDeath (0 = despawn, 1 = death) — WotLK only; vanilla/TBC omit this if (packet.getSize() < 8) { LOG_ERROR("SMSG_DESTROY_OBJECT packet too small: ", packet.getSize(), " bytes"); return false; } data.guid = packet.readUInt64(); // WotLK adds isDeath byte; vanilla/TBC packets are exactly 8 bytes if (packet.hasData()) { data.isDeath = (packet.readUInt8() != 0); } else { data.isDeath = false; } LOG_DEBUG("Parsed SMSG_DESTROY_OBJECT:"); LOG_DEBUG(" GUID: 0x", std::hex, data.guid, std::dec); LOG_DEBUG(" Is death: ", data.isDeath ? "yes" : "no"); return true; } } // namespace game } // namespace wowee