Stabilize streaming memory and parser handling; revert socket recv optimizations

This commit is contained in:
Kelsi 2026-02-22 07:26:54 -08:00
parent c914295d20
commit ae88b226b5
15 changed files with 591 additions and 161 deletions

View file

@ -1143,6 +1143,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
uint16_t opcode = packet.getOpcode();
try {
const bool allowVanillaAliases = isClassicLikeExpansion() || isActiveExpansion("tbc");
// Vanilla compatibility aliases:
// - 0x006B: can be SMSG_COMPRESSED_MOVES on some vanilla-family servers
@ -1150,7 +1153,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
// - 0x0103: SMSG_PLAY_MUSIC (some vanilla-family servers)
//
// We gate these by payload shape so expansion-native mappings remain intact.
if (opcode == 0x006B) {
if (allowVanillaAliases && opcode == 0x006B) {
// Try compressed movement batch first:
// [u8 subSize][u16 subOpcode][subPayload...] ...
// where subOpcode is typically SMSG_MONSTER_MOVE / SMSG_MONSTER_MOVE_TRANSPORT.
@ -1198,7 +1201,7 @@ void GameHandler::handlePacket(network::Packet& packet) {
// Not weather-shaped: rewind and fall through to normal opcode table handling.
packet.setReadPos(0);
}
} else if (opcode == 0x0103) {
} else if (allowVanillaAliases && opcode == 0x0103) {
// Expected play-music payload: uint32 sound/music id
if (packet.getSize() - packet.getReadPos() == 4) {
uint32_t soundId = packet.readUInt32();
@ -2858,6 +2861,23 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
break;
}
} catch (const std::bad_alloc& e) {
LOG_ERROR("OOM while handling world opcode=0x", std::hex, opcode, std::dec,
" state=", worldStateName(state),
" size=", packet.getSize(),
" readPos=", packet.getReadPos(),
" what=", e.what());
if (socket && state == WorldState::IN_WORLD) {
disconnect();
fail("Out of memory while parsing world packet");
}
} catch (const std::exception& e) {
LOG_ERROR("Exception while handling world opcode=0x", std::hex, opcode, std::dec,
" state=", worldStateName(state),
" size=", packet.getSize(),
" readPos=", packet.getReadPos(),
" what=", e.what());
}
}
void GameHandler::handleAuthChallenge(network::Packet& packet) {
@ -8659,10 +8679,30 @@ void GameHandler::handleCompressedMoves(network::Packet& packet) {
void GameHandler::handleMonsterMove(network::Packet& packet) {
MonsterMoveData data;
auto logMonsterMoveParseFailure = [&](const std::string& msg) {
static uint32_t failCount = 0;
++failCount;
if (failCount <= 10 || (failCount % 100) == 0) {
LOG_WARNING(msg, " (occurrence=", failCount, ")");
}
};
auto stripWrappedSubpacket = [&](const std::vector<uint8_t>& bytes, std::vector<uint8_t>& stripped) -> bool {
if (bytes.size() < 3) return false;
uint8_t subSize = bytes[0];
if (subSize < 2) return false;
size_t wrappedLen = static_cast<size_t>(subSize) + 1; // size byte + body
if (wrappedLen != bytes.size()) return false;
size_t payloadLen = static_cast<size_t>(subSize) - 2; // opcode(2) stripped
if (3 + payloadLen > bytes.size()) return false;
stripped.assign(bytes.begin() + 3, bytes.begin() + 3 + payloadLen);
return true;
};
// Turtle WoW (1.17+) compresses each SMSG_MONSTER_MOVE individually:
// format: uint32 decompressedSize + zlib data (zlib magic = 0x78 ??)
const auto& rawData = packet.getData();
bool isCompressed = rawData.size() >= 6 &&
const bool allowTurtleMoveCompression = isActiveExpansion("turtle");
bool isCompressed = allowTurtleMoveCompression &&
rawData.size() >= 6 &&
rawData[4] == 0x78 &&
(rawData[5] == 0x01 || rawData[5] == 0x9C ||
rawData[5] == 0xDA || rawData[5] == 0x5E);
@ -8694,36 +8734,42 @@ void GameHandler::handleMonsterMove(network::Packet& packet) {
}
LOG_INFO("MonsterMove decomp[", destLen, "]: ", hex);
}
// Some Turtle WoW compressed move payloads include an inner
// sub-packet wrapper: uint8 size + uint16 opcode + payload.
// Do not key this on expansion opcode mappings; strip by structure.
std::vector<uint8_t> parseBytes = decompressed;
if (destLen >= 3) {
uint8_t subSize = decompressed[0];
size_t wrappedLen = static_cast<size_t>(subSize) + 1; // size byte + subSize bytes
uint16_t innerOpcode = static_cast<uint16_t>(decompressed[1]) |
(static_cast<uint16_t>(decompressed[2]) << 8);
uint16_t monsterMoveWire = wireOpcode(Opcode::SMSG_MONSTER_MOVE);
bool looksLikeMonsterMoveWrapper =
(innerOpcode == 0x00DD) || (innerOpcode == monsterMoveWire);
// Strict case: one exact wrapped sub-packet in this decompressed blob.
if (subSize >= 2 && wrappedLen == destLen && looksLikeMonsterMoveWrapper) {
size_t payloadStart = 3;
size_t payloadLen = static_cast<size_t>(subSize) - 2;
parseBytes.assign(decompressed.begin() + payloadStart,
decompressed.begin() + payloadStart + payloadLen);
}
}
std::vector<uint8_t> stripped;
bool hasWrappedForm = stripWrappedSubpacket(decompressed, stripped);
network::Packet decompPacket(packet.getOpcode(), parseBytes);
// Try unwrapped payload first (common form), then wrapped-subpacket fallback.
network::Packet decompPacket(packet.getOpcode(), decompressed);
if (!packetParsers_->parseMonsterMove(decompPacket, data)) {
LOG_WARNING("Failed to parse vanilla SMSG_MONSTER_MOVE (decompressed ",
destLen, " bytes, parseBytes ", parseBytes.size(), " bytes)");
return;
if (!hasWrappedForm) {
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE (decompressed " +
std::to_string(destLen) + " bytes)");
return;
}
network::Packet wrappedPacket(packet.getOpcode(), stripped);
if (!packetParsers_->parseMonsterMove(wrappedPacket, data)) {
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE (decompressed " +
std::to_string(destLen) + " bytes, wrapped payload " +
std::to_string(stripped.size()) + " bytes)");
return;
}
LOG_WARNING("SMSG_MONSTER_MOVE parsed via wrapped-subpacket fallback");
}
} else if (!packetParsers_->parseMonsterMove(packet, data)) {
LOG_WARNING("Failed to parse SMSG_MONSTER_MOVE");
return;
// Some realms occasionally embed an extra [size|opcode] wrapper even when the
// outer packet wasn't zlib-compressed. Retry with wrapper stripped by structure.
std::vector<uint8_t> stripped;
if (stripWrappedSubpacket(rawData, stripped)) {
network::Packet wrappedPacket(packet.getOpcode(), stripped);
if (packetParsers_->parseMonsterMove(wrappedPacket, data)) {
LOG_WARNING("SMSG_MONSTER_MOVE parsed via uncompressed wrapped-subpacket fallback");
} else {
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE");
return;
}
} else {
logMonsterMoveParseFailure("Failed to parse SMSG_MONSTER_MOVE");
return;
}
}
// Update entity position in entity manager

View file

@ -1192,6 +1192,24 @@ bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBloc
return true;
}
bool TurtlePacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveData& data) {
// Turtle realms can emit both vanilla-like and WotLK-like monster move bodies.
// Try the canonical Turtle/vanilla parser first, then fall back to WotLK layout.
size_t start = packet.getReadPos();
if (MonsterMoveParser::parseVanilla(packet, data)) {
return true;
}
packet.setReadPos(start);
if (MonsterMoveParser::parse(packet, data)) {
LOG_WARNING("[Turtle] SMSG_MONSTER_MOVE parsed via WotLK fallback layout");
return true;
}
packet.setReadPos(start);
return false;
}
// ============================================================================
// Classic/Vanilla quest giver status
//

View file

@ -376,83 +376,125 @@ bool TbcPacketParsers::parseCharEnum(network::Packet& packet, CharEnumResponse&
// (WotLK removed this field)
// ============================================================================
bool TbcPacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectData& data) {
// Read block count
data.blockCount = packet.readUInt32();
constexpr uint32_t kMaxReasonableUpdateBlocks = 4096;
auto parseWithLayout = [&](bool withHasTransportByte, UpdateObjectData& out) -> bool {
out = UpdateObjectData{};
size_t start = packet.getReadPos();
if (packet.getSize() - start < 4) return false;
// TBC/Classic: has_transport byte (WotLK removed this)
/*uint8_t hasTransport =*/ packet.readUInt8();
LOG_DEBUG("[TBC] SMSG_UPDATE_OBJECT: objectCount=", data.blockCount);
// Check for out-of-range objects first
if (packet.getReadPos() + 1 <= packet.getSize()) {
uint8_t firstByte = packet.readUInt8();
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
uint32_t count = packet.readUInt32();
for (uint32_t i = 0; i < count; ++i) {
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
data.outOfRangeGuids.push_back(guid);
LOG_DEBUG(" Out of range: 0x", std::hex, guid, std::dec);
}
} else {
packet.setReadPos(packet.getReadPos() - 1);
out.blockCount = packet.readUInt32();
if (out.blockCount > kMaxReasonableUpdateBlocks) {
packet.setReadPos(start);
return false;
}
}
// Parse update blocks — dispatching movement via virtual parseMovementBlock()
data.blocks.reserve(data.blockCount);
for (uint32_t i = 0; i < data.blockCount; ++i) {
LOG_DEBUG("Parsing block ", i + 1, " / ", data.blockCount);
UpdateBlock block;
// Read update type
uint8_t updateTypeVal = packet.readUInt8();
block.updateType = static_cast<UpdateType>(updateTypeVal);
LOG_DEBUG("Update block: type=", (int)updateTypeVal);
bool ok = false;
switch (block.updateType) {
case UpdateType::VALUES: {
block.guid = UpdateObjectParser::readPackedGuid(packet);
ok = UpdateObjectParser::parseUpdateFields(packet, block);
break;
if (withHasTransportByte) {
if (packet.getReadPos() >= packet.getSize()) {
packet.setReadPos(start);
return false;
}
case UpdateType::MOVEMENT: {
block.guid = UpdateObjectParser::readPackedGuid(packet);
ok = this->parseMovementBlock(packet, block);
break;
}
case UpdateType::CREATE_OBJECT:
case UpdateType::CREATE_OBJECT2: {
block.guid = UpdateObjectParser::readPackedGuid(packet);
uint8_t objectTypeVal = packet.readUInt8();
block.objectType = static_cast<ObjectType>(objectTypeVal);
ok = this->parseMovementBlock(packet, block);
if (ok) {
ok = UpdateObjectParser::parseUpdateFields(packet, block);
/*uint8_t hasTransport =*/ packet.readUInt8();
}
if (packet.getReadPos() + 1 <= packet.getSize()) {
uint8_t firstByte = packet.readUInt8();
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
if (packet.getReadPos() + 4 > packet.getSize()) {
packet.setReadPos(start);
return false;
}
break;
uint32_t count = packet.readUInt32();
if (count > kMaxReasonableUpdateBlocks) {
packet.setReadPos(start);
return false;
}
for (uint32_t i = 0; i < count; ++i) {
if (packet.getReadPos() >= packet.getSize()) {
packet.setReadPos(start);
return false;
}
uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
out.outOfRangeGuids.push_back(guid);
}
} else {
packet.setReadPos(packet.getReadPos() - 1);
}
case UpdateType::OUT_OF_RANGE_OBJECTS:
case UpdateType::NEAR_OBJECTS:
ok = true;
break;
default:
LOG_WARNING("Unknown update type: ", (int)updateTypeVal);
ok = false;
break;
}
if (!ok) {
LOG_WARNING("Failed to parse update block ", i + 1, " of ", data.blockCount,
" — keeping ", data.blocks.size(), " parsed blocks");
break;
out.blocks.reserve(out.blockCount);
for (uint32_t i = 0; i < out.blockCount; ++i) {
if (packet.getReadPos() >= packet.getSize()) {
packet.setReadPos(start);
return false;
}
UpdateBlock block;
uint8_t updateTypeVal = packet.readUInt8();
if (updateTypeVal > static_cast<uint8_t>(UpdateType::NEAR_OBJECTS)) {
packet.setReadPos(start);
return false;
}
block.updateType = static_cast<UpdateType>(updateTypeVal);
bool ok = false;
switch (block.updateType) {
case UpdateType::VALUES: {
block.guid = UpdateObjectParser::readPackedGuid(packet);
ok = UpdateObjectParser::parseUpdateFields(packet, block);
break;
}
case UpdateType::MOVEMENT: {
block.guid = UpdateObjectParser::readPackedGuid(packet);
ok = this->parseMovementBlock(packet, block);
break;
}
case UpdateType::CREATE_OBJECT:
case UpdateType::CREATE_OBJECT2: {
block.guid = UpdateObjectParser::readPackedGuid(packet);
if (packet.getReadPos() >= packet.getSize()) {
ok = false;
break;
}
uint8_t objectTypeVal = packet.readUInt8();
block.objectType = static_cast<ObjectType>(objectTypeVal);
ok = this->parseMovementBlock(packet, block);
if (ok) ok = UpdateObjectParser::parseUpdateFields(packet, block);
break;
}
case UpdateType::OUT_OF_RANGE_OBJECTS:
case UpdateType::NEAR_OBJECTS:
ok = true;
break;
default:
ok = false;
break;
}
if (!ok) {
packet.setReadPos(start);
return false;
}
out.blocks.push_back(block);
}
data.blocks.push_back(block);
return true;
};
size_t startPos = packet.getReadPos();
UpdateObjectData parsed;
if (parseWithLayout(true, parsed)) {
data = std::move(parsed);
return true;
}
return true;
packet.setReadPos(startPos);
if (parseWithLayout(false, parsed)) {
LOG_WARNING("[TBC] SMSG_UPDATE_OBJECT parsed without has_transport byte fallback");
data = std::move(parsed);
return true;
}
packet.setReadPos(startPos);
return false;
}
network::Packet TbcPacketParsers::buildAcceptQuestPacket(uint64_t npcGuid, uint32_t questId) {

View file

@ -1131,9 +1131,16 @@ bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock&
}
bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) {
constexpr uint32_t kMaxReasonableUpdateBlocks = 4096;
constexpr uint32_t kMaxReasonableOutOfRangeGuids = 16384;
// 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);
@ -1146,6 +1153,11 @@ bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data)
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
// 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 = readPackedGuid(packet);