diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index c6c8cc56..fbdaf5c8 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -16506,12 +16506,48 @@ void GameHandler::handleOtherPlayerMovement(network::Packet& packet) { } void GameHandler::handleCompressedMoves(network::Packet& packet) { - // Vanilla/Classic SMSG_COMPRESSED_MOVES: raw concatenated sub-packets, NOT zlib. - // Evidence: observed 1-byte "00" packets which are not valid zlib streams. - // Each sub-packet: uint8 size (of opcode[2]+payload), uint16 opcode, uint8[] payload. - // size=0 → invalid/empty, signals end of batch. - const auto& data = packet.getData(); - size_t dataLen = data.size(); + // Vanilla-family SMSG_COMPRESSED_MOVES carries concatenated movement sub-packets. + // Turtle can additionally wrap the batch in the same uint32 decompressedSize + zlib + // envelope used by other compressed world packets. + // + // Within the decompressed stream, some realms encode the leading uint8 size as: + // - opcode(2) + payload bytes + // - payload bytes only + // Try both framing modes and use the one that cleanly consumes the batch. + std::vector decompressedStorage; + const std::vector* dataPtr = &packet.getData(); + + const auto& rawData = packet.getData(); + const bool hasCompressedWrapper = + rawData.size() >= 6 && + rawData[4] == 0x78 && + (rawData[5] == 0x01 || rawData[5] == 0x9C || + rawData[5] == 0xDA || rawData[5] == 0x5E); + if (hasCompressedWrapper) { + uint32_t decompressedSize = static_cast(rawData[0]) | + (static_cast(rawData[1]) << 8) | + (static_cast(rawData[2]) << 16) | + (static_cast(rawData[3]) << 24); + if (decompressedSize == 0 || decompressedSize > 65536) { + LOG_WARNING("SMSG_COMPRESSED_MOVES: bad decompressedSize=", decompressedSize); + return; + } + + decompressedStorage.resize(decompressedSize); + uLongf destLen = decompressedSize; + int ret = uncompress(decompressedStorage.data(), &destLen, + rawData.data() + 4, rawData.size() - 4); + if (ret != Z_OK) { + LOG_WARNING("SMSG_COMPRESSED_MOVES: zlib error ", ret); + return; + } + + decompressedStorage.resize(destLen); + dataPtr = &decompressedStorage; + } + + const auto& data = *dataPtr; + const size_t dataLen = data.size(); // Wire opcodes for sub-packet routing uint16_t monsterMoveWire = wireOpcode(Opcode::SMSG_MONSTER_MOVE); @@ -16551,43 +16587,117 @@ void GameHandler::handleCompressedMoves(network::Packet& packet) { wireOpcode(Opcode::MSG_MOVE_UNROOT), }; + struct CompressedMoveSubPacket { + uint16_t opcode = 0; + std::vector payload; + }; + struct DecodeResult { + bool ok = false; + bool overrun = false; + bool usedPayloadOnlySize = false; + size_t endPos = 0; + size_t recognizedCount = 0; + size_t subPacketCount = 0; + std::vector packets; + }; + + auto isRecognizedSubOpcode = [&](uint16_t subOpcode) { + return subOpcode == monsterMoveWire || + subOpcode == monsterMoveTransportWire || + std::find(kMoveOpcodes.begin(), kMoveOpcodes.end(), subOpcode) != kMoveOpcodes.end(); + }; + + auto decodeSubPackets = [&](bool payloadOnlySize) -> DecodeResult { + DecodeResult result; + result.usedPayloadOnlySize = payloadOnlySize; + size_t pos = 0; + while (pos < dataLen) { + if (pos + 1 > dataLen) break; + uint8_t subSize = data[pos]; + if (subSize == 0) { + result.ok = true; + result.endPos = pos + 1; + return result; + } + + const size_t payloadLen = payloadOnlySize + ? static_cast(subSize) + : (subSize >= 2 ? static_cast(subSize) - 2 : 0); + if (!payloadOnlySize && subSize < 2) { + result.endPos = pos; + return result; + } + + const size_t packetLen = 1 + 2 + payloadLen; + if (pos + packetLen > dataLen) { + result.overrun = true; + result.endPos = pos; + return result; + } + + uint16_t subOpcode = static_cast(data[pos + 1]) | + (static_cast(data[pos + 2]) << 8); + size_t payloadStart = pos + 3; + + CompressedMoveSubPacket subPacket; + subPacket.opcode = subOpcode; + subPacket.payload.assign(data.begin() + payloadStart, + data.begin() + payloadStart + payloadLen); + result.packets.push_back(std::move(subPacket)); + ++result.subPacketCount; + if (isRecognizedSubOpcode(subOpcode)) { + ++result.recognizedCount; + } + + pos += packetLen; + } + result.ok = (result.endPos == 0 || result.endPos == dataLen); + result.endPos = dataLen; + return result; + }; + + DecodeResult decoded = decodeSubPackets(false); + if (!decoded.ok || decoded.overrun) { + DecodeResult payloadOnlyDecoded = decodeSubPackets(true); + const bool preferPayloadOnly = + payloadOnlyDecoded.ok && + (!decoded.ok || decoded.overrun || payloadOnlyDecoded.recognizedCount > decoded.recognizedCount); + if (preferPayloadOnly) { + decoded = std::move(payloadOnlyDecoded); + static uint32_t payloadOnlyFallbackCount = 0; + ++payloadOnlyFallbackCount; + if (payloadOnlyFallbackCount <= 10 || (payloadOnlyFallbackCount % 100) == 0) { + LOG_WARNING("SMSG_COMPRESSED_MOVES decoded via payload-only size fallback", + " (occurrence=", payloadOnlyFallbackCount, ")"); + } + } + } + + if (!decoded.ok || decoded.overrun) { + LOG_WARNING("SMSG_COMPRESSED_MOVES: sub-packet overruns buffer at pos=", decoded.endPos); + return; + } + // Track unhandled sub-opcodes once per compressed packet (avoid log spam) std::unordered_set unhandledSeen; - size_t pos = 0; - while (pos < dataLen) { - if (pos + 1 > dataLen) break; - uint8_t subSize = data[pos]; - if (subSize < 2) break; // size=0 or 1 → empty/end-of-batch sentinel - if (pos + 1 + subSize > dataLen) { - LOG_WARNING("SMSG_COMPRESSED_MOVES: sub-packet overruns buffer at pos=", pos); - break; - } - uint16_t subOpcode = static_cast(data[pos + 1]) | - (static_cast(data[pos + 2]) << 8); - size_t payloadLen = subSize - 2; - size_t payloadStart = pos + 3; + for (const auto& entry : decoded.packets) { + network::Packet subPacket(entry.opcode, entry.payload); - std::vector subPayload(data.begin() + payloadStart, - data.begin() + payloadStart + payloadLen); - network::Packet subPacket(subOpcode, subPayload); - - if (subOpcode == monsterMoveWire) { + if (entry.opcode == monsterMoveWire) { handleMonsterMove(subPacket); - } else if (subOpcode == monsterMoveTransportWire) { + } else if (entry.opcode == monsterMoveTransportWire) { handleMonsterMoveTransport(subPacket); } else if (state == WorldState::IN_WORLD && - std::find(kMoveOpcodes.begin(), kMoveOpcodes.end(), subOpcode) != kMoveOpcodes.end()) { + std::find(kMoveOpcodes.begin(), kMoveOpcodes.end(), entry.opcode) != kMoveOpcodes.end()) { // Player/NPC movement update packed in SMSG_MULTIPLE_MOVES handleOtherPlayerMovement(subPacket); } else { - if (unhandledSeen.insert(subOpcode).second) { + if (unhandledSeen.insert(entry.opcode).second) { LOG_INFO("SMSG_COMPRESSED_MOVES: unhandled sub-opcode 0x", - std::hex, subOpcode, std::dec, " payloadLen=", payloadLen); + std::hex, entry.opcode, std::dec, " payloadLen=", entry.payload.size()); } } - - pos = payloadStart + payloadLen; } } diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 59c2d0f8..4cccc4a5 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -22,6 +22,18 @@ bool hasFullPackedGuid(const network::Packet& packet) { return packet.getSize() - packet.getReadPos() >= guidBytes; } +const char* updateTypeName(UpdateType type) { + 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 // ============================================================================ @@ -63,12 +75,12 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo LOG_DEBUG(" [Classic] UpdateFlags: 0x", std::hex, (int)updateFlags, std::dec); - const uint8_t UPDATEFLAG_LIVING = 0x20; - const uint8_t UPDATEFLAG_HAS_POSITION = 0x40; - const uint8_t UPDATEFLAG_HAS_TARGET = 0x04; - const uint8_t UPDATEFLAG_TRANSPORT = 0x02; - const uint8_t UPDATEFLAG_LOWGUID = 0x08; - const uint8_t UPDATEFLAG_HIGHGUID = 0x10; + const uint8_t UPDATEFLAG_TRANSPORT = 0x02; + const uint8_t UPDATEFLAG_MELEE_ATTACKING = 0x04; + const uint8_t UPDATEFLAG_HIGHGUID = 0x08; + const uint8_t UPDATEFLAG_ALL = 0x10; + const uint8_t UPDATEFLAG_LIVING = 0x20; + const uint8_t UPDATEFLAG_HAS_POSITION = 0x40; if (updateFlags & UPDATEFLAG_LIVING) { // Movement flags (u32 only — NO extra flags byte in Classic) @@ -183,26 +195,26 @@ bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlo LOG_DEBUG(" [Classic] STATIONARY: (", block.x, ", ", block.y, ", ", block.z, ")"); } - // Target GUID - if (updateFlags & UPDATEFLAG_HAS_TARGET) { - /*uint64_t targetGuid =*/ UpdateObjectParser::readPackedGuid(packet); - } - - // Transport time - if (updateFlags & UPDATEFLAG_TRANSPORT) { - /*uint32_t transportTime =*/ packet.readUInt32(); - } - - // Low GUID - if (updateFlags & UPDATEFLAG_LOWGUID) { - /*uint32_t lowGuid =*/ packet.readUInt32(); - } - // High GUID if (updateFlags & UPDATEFLAG_HIGHGUID) { /*uint32_t highGuid =*/ packet.readUInt32(); } + // ALL/SELF extra uint32 + if (updateFlags & UPDATEFLAG_ALL) { + /*uint32_t unkAll =*/ packet.readUInt32(); + } + + // Current melee target as packed guid + if (updateFlags & UPDATEFLAG_MELEE_ATTACKING) { + /*uint64_t meleeTargetGuid =*/ UpdateObjectParser::readPackedGuid(packet); + } + + // Transport progress / world time + if (updateFlags & UPDATEFLAG_TRANSPORT) { + /*uint32_t transportTime =*/ packet.readUInt32(); + } + return true; } @@ -1690,12 +1702,12 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc LOG_DEBUG(" [Turtle] UpdateFlags: 0x", std::hex, (int)updateFlags, std::dec); - const uint8_t UPDATEFLAG_LIVING = 0x20; - const uint8_t UPDATEFLAG_HAS_POSITION = 0x40; - const uint8_t UPDATEFLAG_HAS_TARGET = 0x04; - const uint8_t UPDATEFLAG_TRANSPORT = 0x02; - const uint8_t UPDATEFLAG_LOWGUID = 0x08; - const uint8_t UPDATEFLAG_HIGHGUID = 0x10; + const uint8_t UPDATEFLAG_TRANSPORT = 0x02; + const uint8_t UPDATEFLAG_MELEE_ATTACKING = 0x04; + const uint8_t UPDATEFLAG_HIGHGUID = 0x08; + const uint8_t UPDATEFLAG_ALL = 0x10; + const uint8_t UPDATEFLAG_LIVING = 0x20; + const uint8_t UPDATEFLAG_HAS_POSITION = 0x40; if (updateFlags & UPDATEFLAG_LIVING) { size_t livingStart = packet.getReadPos(); @@ -1810,26 +1822,23 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc LOG_DEBUG(" [Turtle] STATIONARY: (", block.x, ", ", block.y, ", ", block.z, ")"); } - // Target GUID - if (updateFlags & UPDATEFLAG_HAS_TARGET) { - /*uint64_t targetGuid =*/ UpdateObjectParser::readPackedGuid(packet); - } - - // Transport time - if (updateFlags & UPDATEFLAG_TRANSPORT) { - /*uint32_t transportTime =*/ packet.readUInt32(); - } - - // Low GUID — Classic-style: 1×u32 (NOT TBC's 2×u32) - if (updateFlags & UPDATEFLAG_LOWGUID) { - /*uint32_t lowGuid =*/ packet.readUInt32(); - } - // High GUID — 1×u32 if (updateFlags & UPDATEFLAG_HIGHGUID) { /*uint32_t highGuid =*/ packet.readUInt32(); } + if (updateFlags & UPDATEFLAG_ALL) { + /*uint32_t unkAll =*/ packet.readUInt32(); + } + + if (updateFlags & UPDATEFLAG_MELEE_ATTACKING) { + /*uint64_t meleeTargetGuid =*/ UpdateObjectParser::readPackedGuid(packet); + } + + if (updateFlags & UPDATEFLAG_TRANSPORT) { + /*uint32_t transportTime =*/ packet.readUInt32(); + } + return true; } @@ -1855,9 +1864,16 @@ bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjec /*uint8_t hasTransport =*/ packet.readUInt8(); } + uint32_t remainingBlockCount = out.blockCount; + if (packet.getReadPos() + 1 <= packet.getSize()) { uint8_t firstByte = packet.readUInt8(); if (firstByte == static_cast(UpdateType::OUT_OF_RANGE_OBJECTS)) { + if (remainingBlockCount == 0) { + packet.setReadPos(start); + return false; + } + --remainingBlockCount; if (packet.getReadPos() + 4 > packet.getSize()) { packet.setReadPos(start); return false; @@ -1879,6 +1895,7 @@ bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjec } } + out.blockCount = remainingBlockCount; out.blocks.reserve(out.blockCount); for (uint32_t i = 0; i < out.blockCount; ++i) { if (packet.getReadPos() >= packet.getSize()) { @@ -1905,7 +1922,7 @@ bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjec switch (updateType) { case UpdateType::MOVEMENT: - block.guid = UpdateObjectParser::readPackedGuid(packet); + block.guid = packet.readUInt64(); if (!movementParser(packet, block)) return false; LOG_DEBUG("[Turtle] Parsed MOVEMENT block via ", layoutName, " layout"); return true; @@ -1964,6 +1981,12 @@ bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjec } if (!ok) { + LOG_WARNING("[Turtle] SMSG_UPDATE_OBJECT block parse failed", + " blockIndex=", i, + " updateType=", updateTypeName(updateType), + " readPos=", packet.getReadPos(), + " blockStart=", blockStart, + " packetSize=", packet.getSize()); packet.setReadPos(start); return false; } diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index 308873f1..30dd8e05 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -425,9 +425,16 @@ bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectDa /*uint8_t hasTransport =*/ packet.readUInt8(); } + uint32_t remainingBlockCount = out.blockCount; + if (packet.getReadPos() + 1 <= packet.getSize()) { uint8_t firstByte = packet.readUInt8(); if (firstByte == static_cast(UpdateType::OUT_OF_RANGE_OBJECTS)) { + if (remainingBlockCount == 0) { + packet.setReadPos(start); + return false; + } + --remainingBlockCount; if (packet.getReadPos() + 4 > packet.getSize()) { packet.setReadPos(start); return false; @@ -450,6 +457,7 @@ bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectDa } } + out.blockCount = remainingBlockCount; out.blocks.reserve(out.blockCount); for (uint32_t i = 0; i < out.blockCount; ++i) { if (packet.getReadPos() >= packet.getSize()) { @@ -473,7 +481,7 @@ bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectDa break; } case UpdateType::MOVEMENT: { - block.guid = UpdateObjectParser::readPackedGuid(packet); + block.guid = packet.readUInt64(); ok = this->parseMovementBlock(packet, block); break; } diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 9af7c692..2f8d5deb 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -35,6 +35,19 @@ namespace { } return packet.getSize() - packet.getReadPos() >= guidBytes; } + + 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 { @@ -1225,7 +1238,13 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& for (int i = 0; i < blockCount; ++i) { // Validate 4 bytes available before each block read if (packet.getReadPos() + 4 > packet.getSize()) { - LOG_WARNING("UpdateObjectParser: truncated update mask at block ", i); + 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(); @@ -1254,7 +1273,14 @@ bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& } // Validate 4 bytes available before reading field value if (packet.getReadPos() + 4 > packet.getSize()) { - LOG_WARNING("UpdateObjectParser: truncated field value at field ", fieldIndex); + 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(); @@ -1298,7 +1324,7 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& case UpdateType::MOVEMENT: { // Movement update - block.guid = readPackedGuid(packet); + block.guid = packet.readUInt64(); LOG_DEBUG(" MOVEMENT update for GUID: 0x", std::hex, block.guid, std::dec); return parseMovementBlock(packet, block); @@ -1361,11 +1387,18 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) LOG_DEBUG(" objectCount = ", data.blockCount); LOG_DEBUG(" packetSize = ", packet.getSize()); + uint32_t remainingBlockCount = data.blockCount; + // Check for out-of-range objects first if (packet.getReadPos() + 1 <= packet.getSize()) { 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) { @@ -1389,6 +1422,7 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) } // Parse update blocks + data.blockCount = remainingBlockCount; data.blocks.reserve(data.blockCount); for (uint32_t i = 0; i < data.blockCount; ++i) {