2026-02-02 12:24:50 -08:00
|
|
|
#include "game/world_packets.hpp"
|
2026-02-16 18:46:44 -08:00
|
|
|
#include "game/packet_parsers.hpp"
|
2026-02-02 12:24:50 -08:00
|
|
|
#include "game/opcodes.hpp"
|
|
|
|
|
#include "game/character.hpp"
|
|
|
|
|
#include "auth/crypto.hpp"
|
|
|
|
|
#include "core/logger.hpp"
|
|
|
|
|
#include <algorithm>
|
2026-03-27 18:47:35 -07:00
|
|
|
#include <array>
|
2026-02-02 12:24:50 -08:00
|
|
|
#include <cctype>
|
2026-04-03 19:36:34 -07:00
|
|
|
#include <cmath>
|
2026-02-02 12:24:50 -08:00
|
|
|
#include <cstring>
|
2026-02-10 13:16:38 -08:00
|
|
|
#include <sstream>
|
|
|
|
|
#include <iomanip>
|
2026-02-05 21:03:11 -08:00
|
|
|
#include <zlib.h>
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-23 16:30:49 +01:00
|
|
|
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<uint16_t>(((v & 0xFF00u) >> 8) | ((v & 0x00FFu) << 8));
|
|
|
|
|
}
|
2026-03-14 10:17:19 -07:00
|
|
|
|
2026-03-15 03:40:58 -07:00
|
|
|
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";
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-23 16:30:49 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
namespace wowee {
|
|
|
|
|
namespace game {
|
|
|
|
|
|
2026-02-20 23:20:02 -08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
network::Packet AuthSessionPacket::build(uint32_t build,
|
|
|
|
|
const std::string& accountName,
|
|
|
|
|
uint32_t clientSeed,
|
|
|
|
|
const std::vector<uint8_t>& sessionKey,
|
2026-02-05 21:03:11 -08:00
|
|
|
uint32_t serverSeed,
|
|
|
|
|
uint32_t realmId) {
|
2026-02-02 12:24:50 -08:00
|
|
|
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(),
|
2026-03-29 19:58:36 -07:00
|
|
|
upperAccount.begin(), [](unsigned char c) { return static_cast<char>(std::toupper(c)); });
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
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)
|
2026-02-12 22:56:36 -08:00
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_AUTH_SESSION));
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-12 22:56:36 -08:00
|
|
|
bool isTbc = (build <= 8606); // TBC 2.4.3 = 8606, WotLK starts at 11159+
|
2026-02-05 21:03:11 -08:00
|
|
|
|
2026-02-12 22:56:36 -08:00
|
|
|
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);
|
2026-02-13 16:53:28 -08:00
|
|
|
// 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
|
2026-02-12 22:56:36 -08:00
|
|
|
LOG_DEBUG(" Realm ID: ", realmId);
|
|
|
|
|
packet.writeUInt32(0); // DOS response (uint64)
|
|
|
|
|
packet.writeUInt32(0);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-05 21:03:11 -08:00
|
|
|
// Authentication hash/digest (20 bytes)
|
|
|
|
|
packet.writeBytes(authHash.data(), authHash.size());
|
|
|
|
|
|
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.
2026-02-20 00:18:03 -08:00
|
|
|
// 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<uint8_t> 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"
|
|
|
|
|
};
|
2026-04-06 22:43:13 +03:00
|
|
|
static constexpr uint32_t standardModulusCRC = 0x4C1C776D;
|
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.
2026-02-20 00:18:03 -08:00
|
|
|
for (const char* name : vanillaAddons) {
|
|
|
|
|
// string (null-terminated)
|
|
|
|
|
size_t len = strlen(name);
|
|
|
|
|
addonData.insert(addonData.end(), reinterpret_cast<const uint8_t*>(name),
|
|
|
|
|
reinterpret_cast<const uint8_t*>(name) + len + 1);
|
|
|
|
|
// uint8 flags = 1 (enabled)
|
|
|
|
|
addonData.push_back(0x01);
|
|
|
|
|
// uint32 modulusCRC (little-endian)
|
|
|
|
|
addonData.push_back(static_cast<uint8_t>(standardModulusCRC & 0xFF));
|
|
|
|
|
addonData.push_back(static_cast<uint8_t>((standardModulusCRC >> 8) & 0xFF));
|
|
|
|
|
addonData.push_back(static_cast<uint8_t>((standardModulusCRC >> 16) & 0xFF));
|
|
|
|
|
addonData.push_back(static_cast<uint8_t>((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<uint32_t>(addonData.size());
|
2026-02-05 21:03:11 -08:00
|
|
|
|
|
|
|
|
// Compress with zlib
|
|
|
|
|
uLongf compressedSize = compressBound(decompressedSize);
|
|
|
|
|
std::vector<uint8_t> compressed(compressedSize);
|
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.
2026-02-20 00:18:03 -08:00
|
|
|
int ret = compress(compressed.data(), &compressedSize, addonData.data(), decompressedSize);
|
2026-02-05 21:03:11 -08:00
|
|
|
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,
|
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.
2026-02-20 00:18:03 -08:00
|
|
|
" compressedSize=", compressedSize, " addons=",
|
|
|
|
|
isTbc ? "12 vanilla" : "0 wotlk");
|
2026-02-05 21:03:11 -08:00
|
|
|
} else {
|
|
|
|
|
LOG_ERROR("zlib compress failed with code: ", ret);
|
|
|
|
|
packet.writeUInt32(0);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
LOG_INFO("CMSG_AUTH_SESSION packet built: ", packet.getSize(), " bytes");
|
|
|
|
|
|
2026-02-05 21:03:11 -08:00
|
|
|
// Dump full packet for protocol debugging
|
2026-03-25 12:12:03 -07:00
|
|
|
LOG_DEBUG("CMSG_AUTH_SESSION full dump:\n",
|
|
|
|
|
core::toHexString(packet.getData().data(), packet.getData().size(), true));
|
2026-02-05 21:03:11 -08:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
return packet;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::vector<uint8_t> AuthSessionPacket::computeAuthHash(
|
|
|
|
|
const std::string& accountName,
|
|
|
|
|
uint32_t clientSeed,
|
|
|
|
|
uint32_t serverSeed,
|
|
|
|
|
const std::vector<uint8_t>& sessionKey) {
|
|
|
|
|
|
|
|
|
|
// Build hash input:
|
|
|
|
|
// account_name + [0,0,0,0] + client_seed + server_seed + session_key
|
|
|
|
|
|
|
|
|
|
std::vector<uint8_t> 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());
|
|
|
|
|
|
2026-02-13 16:53:28 -08:00
|
|
|
// Diagnostic: dump auth hash inputs for debugging AUTH_REJECT
|
2026-03-25 12:12:03 -07:00
|
|
|
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()));
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Compute SHA1 hash
|
2026-02-13 16:53:28 -08:00
|
|
|
auto result = auth::Crypto::sha1(hashInput);
|
2026-03-25 12:12:03 -07:00
|
|
|
LOG_DEBUG("AUTH HASH: digest=", core::toHexString(result.data(), result.size()));
|
2026-02-13 16:53:28 -08:00
|
|
|
|
|
|
|
|
return result;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool AuthChallengeParser::parse(network::Packet& packet, AuthChallengeData& data) {
|
2026-02-12 22:56:36 -08:00
|
|
|
// 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)
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-12 22:56:36 -08:00
|
|
|
if (packet.getSize() < 4) {
|
2026-02-02 12:24:50 -08:00
|
|
|
LOG_ERROR("SMSG_AUTH_CHALLENGE packet too small: ", packet.getSize(), " bytes");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
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.
2026-02-20 00:18:03 -08:00
|
|
|
if (packet.getSize() <= 4) {
|
|
|
|
|
// Original vanilla/TBC format: just the server seed (4 bytes)
|
|
|
|
|
data.unknown1 = 0;
|
|
|
|
|
data.serverSeed = packet.readUInt32();
|
2026-03-10 04:51:01 -07:00
|
|
|
LOG_INFO("SMSG_AUTH_CHALLENGE: TBC format (", packet.getSize(), " bytes)");
|
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.
2026-02-20 00:18:03 -08:00
|
|
|
} else if (packet.getSize() < 40) {
|
|
|
|
|
// Vanilla with encryption seeds (36 bytes): serverSeed + 32 bytes seeds
|
|
|
|
|
// No "unknown1" prefix — first uint32 IS the server seed
|
2026-02-12 22:56:36 -08:00
|
|
|
data.unknown1 = 0;
|
|
|
|
|
data.serverSeed = packet.readUInt32();
|
2026-03-10 04:51:01 -07:00
|
|
|
LOG_INFO("SMSG_AUTH_CHALLENGE: Classic+seeds format (", packet.getSize(), " bytes)");
|
2026-02-12 22:56:36 -08:00
|
|
|
} else {
|
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.
2026-02-20 00:18:03 -08:00
|
|
|
// WotLK format (40+ bytes): unknown1 + serverSeed + 32 bytes encryption seeds
|
2026-02-12 22:56:36 -08:00
|
|
|
data.unknown1 = packet.readUInt32();
|
|
|
|
|
data.serverSeed = packet.readUInt32();
|
2026-03-10 04:51:01 -07:00
|
|
|
LOG_INFO("SMSG_AUTH_CHALLENGE: WotLK format (", packet.getSize(), " bytes)");
|
|
|
|
|
LOG_DEBUG(" Unknown1: 0x", std::hex, data.unknown1, std::dec);
|
2026-02-12 22:56:36 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-03-10 04:51:01 -07:00
|
|
|
LOG_DEBUG(" Server seed: 0x", std::hex, data.serverSeed, std::dec);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
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<AuthResult>(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";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 14:13:48 -08:00
|
|
|
// ============================================================
|
|
|
|
|
// Character Creation
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
network::Packet CharCreatePacket::build(const CharCreateData& data) {
|
2026-02-12 22:56:36 -08:00
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_CHAR_CREATE));
|
2026-02-05 14:13:48 -08:00
|
|
|
|
2026-02-09 17:39:21 -08:00
|
|
|
// Convert nonbinary gender to server-compatible value (servers only support male/female)
|
|
|
|
|
Gender serverGender = toServerGender(data.gender);
|
|
|
|
|
|
2026-02-05 14:13:48 -08:00
|
|
|
packet.writeString(data.name); // null-terminated name
|
|
|
|
|
packet.writeUInt8(static_cast<uint8_t>(data.race));
|
|
|
|
|
packet.writeUInt8(static_cast<uint8_t>(data.characterClass));
|
2026-02-09 17:39:21 -08:00
|
|
|
packet.writeUInt8(static_cast<uint8_t>(serverGender));
|
2026-02-05 14:13:48 -08:00
|
|
|
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
|
2026-02-17 04:16:27 -08:00
|
|
|
// 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);
|
2026-02-05 14:13:48 -08:00
|
|
|
|
2026-02-05 21:03:11 -08:00
|
|
|
LOG_DEBUG("Built CMSG_CHAR_CREATE: name=", data.name,
|
|
|
|
|
" race=", static_cast<int>(data.race),
|
|
|
|
|
" class=", static_cast<int>(data.characterClass),
|
|
|
|
|
" gender=", static_cast<int>(data.gender),
|
2026-02-09 17:39:21 -08:00
|
|
|
" (server gender=", static_cast<int>(serverGender), ")",
|
2026-02-05 21:03:11 -08:00
|
|
|
" skin=", static_cast<int>(data.skin),
|
|
|
|
|
" face=", static_cast<int>(data.face),
|
|
|
|
|
" hair=", static_cast<int>(data.hairStyle),
|
|
|
|
|
" hairColor=", static_cast<int>(data.hairColor),
|
|
|
|
|
" facial=", static_cast<int>(data.facialHair));
|
|
|
|
|
|
|
|
|
|
// Dump full packet for protocol debugging
|
2026-03-25 12:12:03 -07:00
|
|
|
LOG_DEBUG("CMSG_CHAR_CREATE full dump: ",
|
|
|
|
|
core::toHexString(packet.getData().data(), packet.getData().size(), true));
|
2026-02-05 14:13:48 -08:00
|
|
|
|
|
|
|
|
return packet;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CharCreateResponseParser::parse(network::Packet& packet, CharCreateResponseData& data) {
|
2026-03-11 15:08:48 -07:00
|
|
|
// Validate minimum packet size: result(1)
|
2026-03-25 16:22:47 -07:00
|
|
|
if (!packet.hasRemaining(1)) {
|
2026-03-11 15:08:48 -07:00
|
|
|
LOG_WARNING("SMSG_CHAR_CREATE: packet too small (", packet.getSize(), " bytes)");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 14:13:48 -08:00
|
|
|
data.result = static_cast<CharCreateResult>(packet.readUInt8());
|
|
|
|
|
LOG_INFO("SMSG_CHAR_CREATE result: ", static_cast<int>(data.result));
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
network::Packet CharEnumPacket::build() {
|
|
|
|
|
// CMSG_CHAR_ENUM has no body - just the opcode
|
2026-02-12 22:56:36 -08:00
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_CHAR_ENUM));
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
LOG_DEBUG("Built CMSG_CHAR_ENUM packet (no body)");
|
|
|
|
|
|
|
|
|
|
return packet;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool CharEnumParser::parse(network::Packet& packet, CharEnumResponse& response) {
|
2026-03-11 14:40:07 -07:00
|
|
|
// Upfront validation: count(1) + at least minimal character data
|
2026-03-25 16:22:47 -07:00
|
|
|
if (!packet.hasRemaining(1)) return false;
|
2026-03-11 14:40:07 -07:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Read character count
|
|
|
|
|
uint8_t count = packet.readUInt8();
|
|
|
|
|
|
2026-03-25 11:40:49 -07:00
|
|
|
LOG_INFO("Parsing SMSG_CHAR_ENUM: ", static_cast<int>(count), " characters");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
response.characters.clear();
|
|
|
|
|
response.characters.reserve(count);
|
|
|
|
|
|
|
|
|
|
for (uint8_t i = 0; i < count; ++i) {
|
|
|
|
|
Character character;
|
|
|
|
|
|
2026-03-11 14:40:07 -07:00
|
|
|
// 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);
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(minCharacterSize)) {
|
2026-03-25 11:40:49 -07:00
|
|
|
LOG_WARNING("CharEnumParser: truncated character at index ", static_cast<int>(i));
|
2026-03-11 14:40:07 -07:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Read GUID (8 bytes, little-endian)
|
|
|
|
|
character.guid = packet.readUInt64();
|
|
|
|
|
|
2026-03-11 14:40:07 -07:00
|
|
|
// Read name (null-terminated string) - validate before reading
|
2026-03-25 14:39:01 -07:00
|
|
|
if (!packet.hasData()) {
|
2026-03-25 11:40:49 -07:00
|
|
|
LOG_WARNING("CharEnumParser: no bytes for name at index ", static_cast<int>(i));
|
2026-03-11 14:40:07 -07:00
|
|
|
break;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
character.name = packet.readString();
|
|
|
|
|
|
2026-03-11 14:40:07 -07:00
|
|
|
// Validate remaining bytes before reading fixed-size fields
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(1)) {
|
2026-03-25 11:40:49 -07:00
|
|
|
LOG_WARNING("CharEnumParser: truncated before race/class/gender at index ", static_cast<int>(i));
|
2026-03-11 14:40:07 -07:00
|
|
|
character.race = Race::HUMAN;
|
|
|
|
|
character.characterClass = Class::WARRIOR;
|
|
|
|
|
character.gender = Gender::MALE;
|
|
|
|
|
} else {
|
|
|
|
|
// Read race, class, gender
|
|
|
|
|
character.race = static_cast<Race>(packet.readUInt8());
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(1)) {
|
2026-03-11 14:40:07 -07:00
|
|
|
character.characterClass = Class::WARRIOR;
|
|
|
|
|
character.gender = Gender::MALE;
|
|
|
|
|
} else {
|
|
|
|
|
character.characterClass = static_cast<Class>(packet.readUInt8());
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(1)) {
|
2026-03-11 14:40:07 -07:00
|
|
|
character.gender = Gender::MALE;
|
|
|
|
|
} else {
|
|
|
|
|
character.gender = static_cast<Gender>(packet.readUInt8());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-03-11 14:40:07 -07:00
|
|
|
// Validate before reading appearance data
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(4)) {
|
2026-03-11 14:40:07 -07:00
|
|
|
character.appearanceBytes = 0;
|
|
|
|
|
character.facialFeatures = 0;
|
|
|
|
|
} else {
|
|
|
|
|
// Read appearance data
|
|
|
|
|
character.appearanceBytes = packet.readUInt32();
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(1)) {
|
2026-03-11 14:40:07 -07:00
|
|
|
character.facialFeatures = 0;
|
|
|
|
|
} else {
|
|
|
|
|
character.facialFeatures = packet.readUInt8();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Read level
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(1)) {
|
2026-03-11 14:40:07 -07:00
|
|
|
character.level = 1;
|
|
|
|
|
} else {
|
|
|
|
|
character.level = packet.readUInt8();
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Read location
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(12)) {
|
2026-03-11 14:40:07 -07:00
|
|
|
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();
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Read affiliations
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(4)) {
|
2026-03-11 14:40:07 -07:00
|
|
|
character.guildId = 0;
|
|
|
|
|
} else {
|
|
|
|
|
character.guildId = packet.readUInt32();
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Read flags
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(4)) {
|
2026-03-11 14:40:07 -07:00
|
|
|
character.flags = 0;
|
|
|
|
|
} else {
|
|
|
|
|
character.flags = packet.readUInt32();
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Skip customization flag (uint32) and unknown byte
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(4)) {
|
2026-03-11 14:40:07 -07:00
|
|
|
// Customization missing, skip unknown
|
|
|
|
|
} else {
|
|
|
|
|
packet.readUInt32(); // Customization
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(1)) {
|
2026-03-11 14:40:07 -07:00
|
|
|
// Unknown missing
|
|
|
|
|
} else {
|
|
|
|
|
packet.readUInt8(); // Unknown
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Read pet data (always present, even if no pet)
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(12)) {
|
2026-03-11 14:40:07 -07:00
|
|
|
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();
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Read equipment (23 items)
|
|
|
|
|
character.equipment.reserve(23);
|
|
|
|
|
for (int j = 0; j < 23; ++j) {
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(9)) break;
|
2026-02-02 12:24:50 -08:00
|
|
|
EquipmentItem item;
|
|
|
|
|
item.displayModel = packet.readUInt32();
|
|
|
|
|
item.inventoryType = packet.readUInt8();
|
|
|
|
|
item.enchantment = packet.readUInt32();
|
|
|
|
|
character.equipment.push_back(item);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-25 11:40:49 -07:00
|
|
|
LOG_DEBUG(" Character ", static_cast<int>(i + 1), ": ", character.name,
|
2026-03-10 04:25:45 -07:00
|
|
|
" (", getRaceName(character.race), " ", getClassName(character.characterClass),
|
2026-03-25 11:40:49 -07:00
|
|
|
" level ", static_cast<int>(character.level), " zone ", character.zoneId, ")");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
response.characters.push_back(character);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LOG_INFO("Successfully parsed ", response.characters.size(), " characters");
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
network::Packet PlayerLoginPacket::build(uint64_t characterGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_PLAYER_LOGIN));
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// 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) {
|
2026-02-22 07:44:32 -08:00
|
|
|
// 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)");
|
2026-02-02 12:24:50 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-22 07:44:32 -08:00
|
|
|
for (uint32_t& t : data.accountDataTimes) {
|
|
|
|
|
t = 0;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
data.serverTime = packet.readUInt32();
|
|
|
|
|
data.unknown = packet.readUInt8();
|
|
|
|
|
|
2026-03-25 12:42:56 -07:00
|
|
|
size_t remaining = packet.getRemainingSize();
|
2026-02-22 07:44:32 -08:00
|
|
|
uint32_t mask = 0xFF;
|
|
|
|
|
if (remaining >= 4 && ((remaining - 4) % 4) == 0) {
|
|
|
|
|
// Treat first dword as slot mask when payload shape matches.
|
|
|
|
|
mask = packet.readUInt32();
|
|
|
|
|
}
|
2026-03-25 12:42:56 -07:00
|
|
|
remaining = packet.getRemainingSize();
|
2026-02-22 07:44:32 -08:00
|
|
|
size_t slotWords = std::min<size_t>(8, remaining / 4);
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
LOG_DEBUG("Parsed SMSG_ACCOUNT_DATA_TIMES:");
|
|
|
|
|
LOG_DEBUG(" Server time: ", data.serverTime);
|
2026-03-25 11:40:49 -07:00
|
|
|
LOG_DEBUG(" Unknown: ", static_cast<int>(data.unknown));
|
2026-02-22 07:44:32 -08:00
|
|
|
LOG_DEBUG(" Mask: 0x", std::hex, mask, std::dec, " slotsInPacket=", slotWords);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-22 07:44:32 -08:00
|
|
|
for (size_t i = 0; i < slotWords; ++i) {
|
2026-02-02 12:24:50 -08:00
|
|
|
data.accountDataTimes[i] = packet.readUInt32();
|
2026-02-22 07:44:32 -08:00
|
|
|
if (data.accountDataTimes[i] != 0 || ((mask & (1u << i)) != 0)) {
|
2026-02-02 12:24:50 -08:00
|
|
|
LOG_DEBUG(" Data slot ", i, ": ", data.accountDataTimes[i]);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-22 07:44:32 -08:00
|
|
|
if (packet.getReadPos() != packet.getSize()) {
|
2026-03-25 12:42:56 -07:00
|
|
|
LOG_DEBUG(" AccountDataTimes trailing bytes: ", packet.getRemainingSize());
|
2026-03-25 14:27:26 -07:00
|
|
|
packet.skipAll();
|
2026-02-22 07:44:32 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
2026-03-11 14:41:25 -07:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 04:51:01 -07:00
|
|
|
LOG_INFO("Parsed SMSG_MOTD: ", lineCount, " line(s)");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
data.lines.clear();
|
|
|
|
|
data.lines.reserve(lineCount);
|
|
|
|
|
|
|
|
|
|
for (uint32_t i = 0; i < lineCount; ++i) {
|
2026-03-11 14:41:25 -07:00
|
|
|
// Validate at least 1 byte available for the string
|
2026-03-25 14:39:01 -07:00
|
|
|
if (!packet.hasData()) {
|
2026-03-11 14:41:25 -07:00
|
|
|
LOG_WARNING("MotdParser: truncated at line ", i + 1);
|
|
|
|
|
break;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
std::string line = packet.readString();
|
|
|
|
|
data.lines.push_back(line);
|
2026-03-10 04:51:01 -07:00
|
|
|
LOG_DEBUG(" MOTD[", i + 1, "]: ", line);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
network::Packet PingPacket::build(uint32_t sequence, uint32_t latency) {
|
2026-02-12 22:56:36 -08:00
|
|
|
network::Packet packet(wireOpcode(Opcode::CMSG_PING));
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-11 21:14:35 -08:00
|
|
|
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
|
2026-02-06 03:24:46 -08:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Write movement flags
|
|
|
|
|
packet.writeUInt32(info.flags);
|
|
|
|
|
packet.writeUInt16(info.flags2);
|
|
|
|
|
|
|
|
|
|
// Write timestamp
|
|
|
|
|
packet.writeUInt32(info.time);
|
|
|
|
|
|
|
|
|
|
// Write position
|
2026-03-25 13:54:10 -07:00
|
|
|
packet.writeFloat(info.x);
|
|
|
|
|
packet.writeFloat(info.y);
|
|
|
|
|
packet.writeFloat(info.z);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Write orientation
|
2026-03-25 13:54:10 -07:00
|
|
|
packet.writeFloat(info.orientation);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-11 15:24:05 -08:00
|
|
|
// Write transport data if on transport.
|
|
|
|
|
// 3.3.5a ordering: transport block appears before pitch/fall/jump.
|
2026-02-11 02:23:37 -08:00
|
|
|
if (info.hasFlag(MovementFlags::ONTRANSPORT)) {
|
|
|
|
|
// Write packed transport GUID
|
2026-03-25 14:06:42 -07:00
|
|
|
packet.writePackedGuid(info.transportGuid);
|
2026-02-11 02:23:37 -08:00
|
|
|
|
|
|
|
|
// Write transport local position
|
2026-03-25 13:54:10 -07:00
|
|
|
packet.writeFloat(info.transportX);
|
|
|
|
|
packet.writeFloat(info.transportY);
|
|
|
|
|
packet.writeFloat(info.transportZ);
|
|
|
|
|
packet.writeFloat(info.transportO);
|
2026-02-11 02:23:37 -08:00
|
|
|
|
|
|
|
|
// Write transport time
|
|
|
|
|
packet.writeUInt32(info.transportTime);
|
2026-02-11 15:24:05 -08:00
|
|
|
|
|
|
|
|
// Transport seat is always present in ONTRANSPORT movement info.
|
|
|
|
|
packet.writeUInt8(static_cast<uint8_t>(info.transportSeat));
|
|
|
|
|
|
|
|
|
|
// Optional second transport time for interpolated movement.
|
2026-03-22 21:47:12 +03:00
|
|
|
if (info.flags2 & 0x0400) { // MOVEMENTFLAG2_INTERPOLATED_MOVEMENT
|
2026-02-11 15:24:05 -08:00
|
|
|
packet.writeUInt32(info.transportTime2);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Write pitch if swimming/flying
|
|
|
|
|
if (info.hasFlag(MovementFlags::SWIMMING) || info.hasFlag(MovementFlags::FLYING)) {
|
2026-03-25 13:54:10 -07:00
|
|
|
packet.writeFloat(info.pitch);
|
2026-02-11 15:24:05 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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)) {
|
2026-03-25 13:54:10 -07:00
|
|
|
packet.writeFloat(info.jumpVelocity);
|
|
|
|
|
packet.writeFloat(info.jumpSinAngle);
|
|
|
|
|
packet.writeFloat(info.jumpCosAngle);
|
|
|
|
|
packet.writeFloat(info.jumpXYSpeed);
|
2026-02-11 02:23:37 -08:00
|
|
|
}
|
2026-02-11 21:14:35 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
network::Packet MovementPacket::build(Opcode opcode, const MovementInfo& info, uint64_t playerGuid) {
|
2026-02-12 22:56:36 -08:00
|
|
|
network::Packet packet(wireOpcode(opcode));
|
2026-02-11 21:14:35 -08:00
|
|
|
|
|
|
|
|
// Movement packet format (WoW 3.3.5a):
|
|
|
|
|
// packed GUID + movement payload
|
2026-03-25 14:06:42 -07:00
|
|
|
packet.writePackedGuid(playerGuid);
|
2026-02-11 21:14:35 -08:00
|
|
|
writeMovementPayload(packet, info);
|
2026-02-11 02:23:37 -08:00
|
|
|
|
2026-02-06 13:47:03 -08:00
|
|
|
// Detailed hex dump for debugging
|
|
|
|
|
static int mvLog = 5;
|
2026-02-06 09:14:22 -08:00
|
|
|
if (mvLog-- > 0) {
|
2026-02-06 13:47:03 -08:00
|
|
|
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;
|
|
|
|
|
}
|
2026-02-21 01:26:16 -08:00
|
|
|
LOG_DEBUG("MOVEPKT opcode=0x", std::hex, wireOpcode(opcode), std::dec,
|
2026-02-06 13:47:03 -08:00
|
|
|
" 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, ")",
|
2026-02-11 02:23:37 -08:00
|
|
|
" 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) + ")" : ""));
|
2026-02-21 01:26:16 -08:00
|
|
|
LOG_DEBUG("MOVEPKT hex: ", hex);
|
2026-02-06 09:14:22 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
return packet;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool UpdateObjectParser::parseMovementBlock(network::Packet& packet, UpdateBlock& block) {
|
2026-02-05 21:55:52 -08:00
|
|
|
// WoW 3.3.5a UPDATE_OBJECT movement block structure:
|
|
|
|
|
// 1. UpdateFlags (1 byte, sometimes 2)
|
|
|
|
|
// 2. Movement data depends on update flags
|
|
|
|
|
|
2026-03-25 12:42:56 -07:00
|
|
|
auto rem = [&]() -> size_t { return packet.getRemainingSize(); };
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 2) return false;
|
|
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
// Update flags (3.3.5a uses 2 bytes for flags)
|
|
|
|
|
uint16_t updateFlags = packet.readUInt16();
|
2026-02-08 00:59:40 -08:00
|
|
|
block.updateFlags = updateFlags;
|
2026-02-05 21:55:52 -08:00
|
|
|
|
|
|
|
|
LOG_DEBUG(" UpdateFlags: 0x", std::hex, updateFlags, std::dec);
|
|
|
|
|
|
2026-02-11 00:54:38 -08:00
|
|
|
// Log transport-related flag combinations
|
|
|
|
|
if (updateFlags & 0x0002) { // UPDATEFLAG_TRANSPORT
|
2026-02-21 01:26:16 -08:00
|
|
|
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);
|
|
|
|
|
}
|
2026-02-11 00:54:38 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
// 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) {
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
// Minimum: moveFlags(4)+moveFlags2(2)+time(4)+position(16)+fallTime(4)+speeds(36) = 66
|
|
|
|
|
if (rem() < 66) return false;
|
|
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
// 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
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 1) return false;
|
2026-02-08 00:59:40 -08:00
|
|
|
block.onTransport = true;
|
2026-03-25 14:06:42 -07:00
|
|
|
block.transportGuid = packet.readPackedGuid();
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 21) return false; // 4 floats + uint32 + uint8
|
2026-02-08 00:59:40 -08:00
|
|
|
block.transportX = packet.readFloat();
|
|
|
|
|
block.transportY = packet.readFloat();
|
|
|
|
|
block.transportZ = packet.readFloat();
|
|
|
|
|
block.transportO = packet.readFloat();
|
2026-02-05 21:55:52 -08:00
|
|
|
/*uint32_t tTime =*/ packet.readUInt32();
|
|
|
|
|
/*int8_t tSeat =*/ packet.readUInt8();
|
|
|
|
|
|
2026-02-08 00:59:40 -08:00
|
|
|
LOG_DEBUG(" OnTransport: guid=0x", std::hex, block.transportGuid, std::dec,
|
|
|
|
|
" offset=(", block.transportX, ", ", block.transportY, ", ", block.transportZ, ")");
|
|
|
|
|
|
2026-03-22 21:47:12 +03:00
|
|
|
if (moveFlags2 & 0x0400) { // MOVEMENTFLAG2_INTERPOLATED_MOVEMENT
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 4) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
/*uint32_t tTime2 =*/ packet.readUInt32();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
// Swimming/flying pitch
|
2026-03-22 21:47:12 +03:00
|
|
|
// WotLK 3.3.5a movement flags (wire format):
|
2026-03-10 11:16:40 -07:00
|
|
|
// SWIMMING = 0x00200000
|
2026-03-22 21:47:12 +03:00
|
|
|
// CAN_FLY = 0x01000000 (ability to fly — no pitch field)
|
|
|
|
|
// FLYING = 0x02000000 (actively flying — has pitch field)
|
|
|
|
|
// SPLINE_ELEVATION = 0x04000000 (smooth vertical spline offset)
|
2026-03-10 11:16:40 -07:00
|
|
|
// MovementFlags2:
|
2026-03-22 21:47:12 +03:00
|
|
|
// MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING = 0x0020
|
2026-03-10 11:16:40 -07:00
|
|
|
//
|
|
|
|
|
// Pitch is present when SWIMMING or FLYING are set, or the always-allow flag is set.
|
2026-03-22 21:47:12 +03:00
|
|
|
// Note: CAN_FLY (0x01000000) does NOT gate pitch; only FLYING (0x02000000) does.
|
|
|
|
|
// (TBC uses 0x01000000 for FLYING — see TbcMoveFlags in packet_parsers_tbc.cpp.)
|
2026-03-10 11:03:33 -07:00
|
|
|
if ((moveFlags & 0x00200000) /* SWIMMING */ ||
|
2026-03-22 21:47:12 +03:00
|
|
|
(moveFlags & 0x02000000) /* FLYING */ ||
|
|
|
|
|
(moveFlags2 & 0x0020) /* MOVEMENTFLAG2_ALWAYS_ALLOW_PITCHING */) {
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 4) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
/*float pitch =*/ packet.readFloat();
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
// Fall time
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 4) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
/*uint32_t fallTime =*/ packet.readUInt32();
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
// Jumping
|
|
|
|
|
if (moveFlags & 0x00001000) { // MOVEMENTFLAG_FALLING
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 16) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
/*float jumpVelocity =*/ packet.readFloat();
|
|
|
|
|
/*float jumpSinAngle =*/ packet.readFloat();
|
|
|
|
|
/*float jumpCosAngle =*/ packet.readFloat();
|
|
|
|
|
/*float jumpXYSpeed =*/ packet.readFloat();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Spline elevation
|
|
|
|
|
if (moveFlags & 0x04000000) { // MOVEMENTFLAG_SPLINE_ELEVATION
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 4) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
/*float splineElevation =*/ packet.readFloat();
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
fix: harden Turtle movement block parser with bounds checks
The Turtle parseMovementBlock had no bounds checking on any reads.
Since Packet::readUInt8() returns 0 past the end without failing, the
parser could "succeed" with all-zero garbage data, then subsequent
parseUpdateFields would read from wrong positions, producing
"truncated field value" and "truncated update mask" errors.
Added bounds checks before every conditional read section (transport,
swimming pitch, fall time, jumping, spline elevation, speeds, spline
data, tail flags). Also removed the WotLK movement block fallback from
the Turtle parser chain — WotLK format is fundamentally incompatible
(uint16 flags, 9 speeds) and false-positive parses corrupt NPC data.
Also changed spline pointCount > 256 from cap-to-zero to return false
so the parser correctly fails instead of silently dropping waypoints.
2026-03-18 07:39:40 -07:00
|
|
|
// Speeds (9 values in WotLK: walk/run/runBack/swim/swimBack/flight/flightBack/turn/pitch)
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 36) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
/*float walkSpeed =*/ packet.readFloat();
|
2026-02-07 20:24:25 -08:00
|
|
|
float runSpeed = packet.readFloat();
|
2026-02-05 21:55:52 -08:00
|
|
|
/*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();
|
|
|
|
|
|
2026-02-07 20:24:25 -08:00
|
|
|
block.runSpeed = runSpeed;
|
2026-03-10 11:14:58 -07:00
|
|
|
block.moveFlags = moveFlags;
|
2026-02-07 20:24:25 -08:00
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
// Spline data
|
|
|
|
|
if (moveFlags & 0x08000000) { // MOVEMENTFLAG_SPLINE_ENABLED
|
2026-03-25 16:10:48 -07:00
|
|
|
auto bytesAvailable = [&](size_t n) -> bool { return packet.hasRemaining(n); };
|
2026-02-22 08:27:17 -08:00
|
|
|
if (!bytesAvailable(4)) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
uint32_t splineFlags = packet.readUInt32();
|
2026-02-06 15:18:50 -08:00
|
|
|
LOG_DEBUG(" Spline: flags=0x", std::hex, splineFlags, std::dec);
|
2026-02-05 21:55:52 -08:00
|
|
|
|
2026-02-06 15:18:50 -08:00
|
|
|
if (splineFlags & 0x00010000) { // SPLINEFLAG_FINAL_POINT
|
2026-02-22 08:27:17 -08:00
|
|
|
if (!bytesAvailable(12)) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
/*float finalX =*/ packet.readFloat();
|
|
|
|
|
/*float finalY =*/ packet.readFloat();
|
|
|
|
|
/*float finalZ =*/ packet.readFloat();
|
2026-02-06 15:18:50 -08:00
|
|
|
} else if (splineFlags & 0x00020000) { // SPLINEFLAG_FINAL_TARGET
|
2026-02-22 08:27:17 -08:00
|
|
|
if (!bytesAvailable(8)) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
/*uint64_t finalTarget =*/ packet.readUInt64();
|
2026-02-06 15:18:50 -08:00
|
|
|
} else if (splineFlags & 0x00040000) { // SPLINEFLAG_FINAL_ANGLE
|
2026-02-22 08:27:17 -08:00
|
|
|
if (!bytesAvailable(4)) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
/*float finalAngle =*/ packet.readFloat();
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 16:58:39 -07:00
|
|
|
// 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;
|
2026-02-05 21:55:52 -08:00
|
|
|
/*uint32_t timePassed =*/ packet.readUInt32();
|
|
|
|
|
/*uint32_t duration =*/ packet.readUInt32();
|
|
|
|
|
/*uint32_t splineId =*/ packet.readUInt32();
|
2026-03-18 07:23:51 -07:00
|
|
|
|
2026-03-23 16:32:59 -07:00
|
|
|
// 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 {
|
2026-03-18 07:23:51 -07:00
|
|
|
if (!bytesAvailable(4)) return false;
|
2026-03-23 11:09:15 -07:00
|
|
|
size_t prePointCount = packet.getReadPos();
|
2026-03-18 07:23:51 -07:00
|
|
|
uint32_t pc = packet.readUInt32();
|
|
|
|
|
if (pc > 256) return false;
|
2026-03-23 16:32:59 -07:00
|
|
|
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<size_t>(pc - 1) * 4ull : 0ull);
|
|
|
|
|
} else {
|
|
|
|
|
// All uncompressed: 3 floats each
|
|
|
|
|
pointsBytes = static_cast<size_t>(pc) * 12ull;
|
2026-02-22 08:27:17 -08:00
|
|
|
}
|
2026-03-23 16:32:59 -07:00
|
|
|
size_t needed = pointsBytes + 13ull; // + splineMode(1) + endPoint(12)
|
|
|
|
|
if (!bytesAvailable(needed)) {
|
|
|
|
|
packet.setReadPos(prePointCount);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
packet.setReadPos(packet.getReadPos() + pointsBytes);
|
2026-03-23 11:09:15 -07:00
|
|
|
uint8_t splineMode = packet.readUInt8();
|
|
|
|
|
if (splineMode > 3) {
|
|
|
|
|
packet.setReadPos(prePointCount);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-04-03 19:36:34 -07:00
|
|
|
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, ")");
|
2026-03-18 07:23:51 -07:00
|
|
|
return true;
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-28 10:52:26 -07:00
|
|
|
// Save position before WotLK spline header for fallback
|
|
|
|
|
size_t beforeSplineHeader = packet.getReadPos();
|
|
|
|
|
|
2026-04-10 23:30:55 +03:00
|
|
|
// AzerothCore MoveSplineFlag constants:
|
|
|
|
|
// CATMULLROM = 0x00080000 — uncompressed Catmull-Rom interpolation
|
|
|
|
|
// CYCLIC = 0x00100000 — cyclic path
|
|
|
|
|
// ENTER_CYCLE = 0x00200000 — entering cyclic path
|
|
|
|
|
// ANIMATION = 0x00400000 — animation spline with animType+effectStart
|
|
|
|
|
// PARABOLIC = 0x00000008 — vertical_acceleration+effectStartTime
|
|
|
|
|
constexpr uint32_t SF_PARABOLIC = 0x00000008;
|
|
|
|
|
constexpr uint32_t SF_CATMULLROM = 0x00080000;
|
|
|
|
|
constexpr uint32_t SF_CYCLIC = 0x00100000;
|
|
|
|
|
constexpr uint32_t SF_ENTER_CYCLE = 0x00200000;
|
|
|
|
|
constexpr uint32_t SF_ANIMATION = 0x00400000;
|
|
|
|
|
constexpr uint32_t SF_UNCOMPRESSED_MASK = SF_CATMULLROM | SF_CYCLIC | SF_ENTER_CYCLE;
|
|
|
|
|
|
2026-03-28 10:52:26 -07:00
|
|
|
// Try 1: WotLK format (durationMod+durationModNext+[ANIMATION]+vertAccel+effectStart+points)
|
2026-04-10 23:30:55 +03:00
|
|
|
// Some servers (ChromieCraft) always write vertAccel+effectStart unconditionally.
|
2026-03-28 10:52:26 -07:00
|
|
|
bool splineParsed = false;
|
|
|
|
|
if (bytesAvailable(8)) {
|
|
|
|
|
/*float durationMod =*/ packet.readFloat();
|
|
|
|
|
/*float durationModNext =*/ packet.readFloat();
|
|
|
|
|
bool wotlkOk = true;
|
2026-04-10 23:30:55 +03:00
|
|
|
if (splineFlags & SF_ANIMATION) {
|
2026-03-28 10:52:26 -07:00
|
|
|
if (!bytesAvailable(5)) { wotlkOk = false; }
|
|
|
|
|
else { packet.readUInt8(); packet.readUInt32(); }
|
|
|
|
|
}
|
2026-04-10 23:30:55 +03:00
|
|
|
// Unconditional vertAccel+effectStart (ChromieCraft/some AzerothCore builds)
|
2026-03-28 10:52:26 -07:00
|
|
|
if (wotlkOk) {
|
|
|
|
|
if (!bytesAvailable(8)) { wotlkOk = false; }
|
|
|
|
|
else { /*float vertAccel =*/ packet.readFloat(); /*uint32_t effectStart =*/ packet.readUInt32(); }
|
|
|
|
|
}
|
|
|
|
|
if (wotlkOk) {
|
2026-04-10 23:30:55 +03:00
|
|
|
bool useCompressed = (splineFlags & SF_UNCOMPRESSED_MASK) == 0;
|
2026-03-28 10:52:26 -07:00
|
|
|
splineParsed = tryParseSplinePoints(useCompressed, "wotlk-compressed");
|
|
|
|
|
if (!splineParsed) {
|
|
|
|
|
splineParsed = tryParseSplinePoints(false, "wotlk-uncompressed");
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-05 21:55:52 -08:00
|
|
|
}
|
2026-03-28 10:52:26 -07:00
|
|
|
|
2026-04-10 23:30:55 +03:00
|
|
|
// Try 2: ANIMATION present but vertAccel+effectStart gated by PARABOLIC
|
|
|
|
|
// (standard AzerothCore: only writes vertAccel+effectStart when PARABOLIC is set)
|
|
|
|
|
if (!splineParsed && (splineFlags & SF_ANIMATION)) {
|
|
|
|
|
packet.setReadPos(beforeSplineHeader);
|
|
|
|
|
if (bytesAvailable(8)) {
|
|
|
|
|
packet.readFloat(); // durationMod
|
|
|
|
|
packet.readFloat(); // durationModNext
|
|
|
|
|
bool ok = true;
|
|
|
|
|
if (!bytesAvailable(5)) { ok = false; }
|
|
|
|
|
else { packet.readUInt8(); packet.readUInt32(); } // animType + effectStart
|
|
|
|
|
if (ok && (splineFlags & SF_PARABOLIC)) {
|
|
|
|
|
if (!bytesAvailable(8)) { ok = false; }
|
|
|
|
|
else { packet.readFloat(); packet.readUInt32(); }
|
|
|
|
|
}
|
|
|
|
|
if (ok) {
|
|
|
|
|
bool useCompressed = (splineFlags & SF_UNCOMPRESSED_MASK) == 0;
|
|
|
|
|
splineParsed = tryParseSplinePoints(useCompressed, "wotlk-anim-conditional");
|
|
|
|
|
if (!splineParsed) {
|
|
|
|
|
splineParsed = tryParseSplinePoints(false, "wotlk-anim-conditional-uncomp");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try 3: No ANIMATION — vertAccel+effectStart only when PARABOLIC set
|
|
|
|
|
if (!splineParsed) {
|
|
|
|
|
packet.setReadPos(beforeSplineHeader);
|
|
|
|
|
if (bytesAvailable(8)) {
|
|
|
|
|
packet.readFloat(); // durationMod
|
|
|
|
|
packet.readFloat(); // durationModNext
|
|
|
|
|
bool ok = true;
|
|
|
|
|
if (splineFlags & SF_PARABOLIC) {
|
|
|
|
|
if (!bytesAvailable(8)) { ok = false; }
|
|
|
|
|
else { packet.readFloat(); packet.readUInt32(); }
|
|
|
|
|
}
|
|
|
|
|
if (ok) {
|
|
|
|
|
bool useCompressed = (splineFlags & SF_UNCOMPRESSED_MASK) == 0;
|
|
|
|
|
splineParsed = tryParseSplinePoints(useCompressed, "wotlk-parabolic-gated");
|
|
|
|
|
if (!splineParsed) {
|
|
|
|
|
splineParsed = tryParseSplinePoints(false, "wotlk-parabolic-gated-uncomp");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try 4: No header at all — just durationMod+durationModNext then points
|
2026-03-27 16:51:13 -07:00
|
|
|
if (!splineParsed) {
|
2026-03-28 10:52:26 -07:00
|
|
|
packet.setReadPos(beforeSplineHeader);
|
2026-03-28 10:55:46 -07:00
|
|
|
if (bytesAvailable(8)) {
|
|
|
|
|
packet.readFloat(); // durationMod
|
|
|
|
|
packet.readFloat(); // durationModNext
|
|
|
|
|
splineParsed = tryParseSplinePoints(false, "wotlk-no-parabolic");
|
|
|
|
|
if (!splineParsed) {
|
2026-04-10 23:30:55 +03:00
|
|
|
bool useComp = (splineFlags & SF_UNCOMPRESSED_MASK) == 0;
|
2026-03-28 10:55:46 -07:00
|
|
|
splineParsed = tryParseSplinePoints(useComp, "wotlk-no-parabolic-compressed");
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-27 16:51:13 -07:00
|
|
|
}
|
2026-03-28 10:52:26 -07:00
|
|
|
|
2026-04-10 23:30:55 +03:00
|
|
|
// Try 5: bare points (no WotLK header at all — some spline types skip everything)
|
2026-03-28 12:47:37 -07:00
|
|
|
if (!splineParsed) {
|
|
|
|
|
packet.setReadPos(beforeSplineHeader);
|
|
|
|
|
splineParsed = tryParseSplinePoints(false, "bare-uncompressed");
|
|
|
|
|
if (!splineParsed) {
|
|
|
|
|
packet.setReadPos(beforeSplineHeader);
|
2026-04-10 23:30:55 +03:00
|
|
|
bool useComp = (splineFlags & SF_UNCOMPRESSED_MASK) == 0;
|
2026-03-28 12:47:37 -07:00
|
|
|
splineParsed = tryParseSplinePoints(useComp, "bare-compressed");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-28 10:31:53 -07:00
|
|
|
if (!splineParsed) {
|
2026-04-05 20:24:28 -07:00
|
|
|
// 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);
|
2026-03-28 10:55:46 -07:00
|
|
|
LOG_WARNING("WotLK spline parse failed for guid=0x", std::hex, block.guid, std::dec,
|
2026-04-10 23:30:55 +03:00
|
|
|
" splineFlags=0x", std::hex, splineFlags, std::dec,
|
|
|
|
|
" remaining=", packet.getRemainingSize(),
|
2026-04-05 20:24:28 -07:00
|
|
|
" header=[0x", std::hex, d[0], " 0x", d[1], " 0x", d[2],
|
|
|
|
|
" 0x", d[3], " 0x", d[4], "]", std::dec);
|
2026-03-28 10:31:53 -07:00
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-05 21:55:52 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (updateFlags & UPDATEFLAG_POSITION) {
|
2026-02-11 00:54:38 -08:00
|
|
|
// Transport position update (UPDATEFLAG_POSITION = 0x0100)
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 1) return false;
|
2026-03-25 14:06:42 -07:00
|
|
|
uint64_t transportGuid = packet.readPackedGuid();
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 32) return false; // 8 floats
|
2026-02-05 21:55:52 -08:00
|
|
|
block.x = packet.readFloat();
|
|
|
|
|
block.y = packet.readFloat();
|
|
|
|
|
block.z = packet.readFloat();
|
2026-02-11 15:24:05 -08:00
|
|
|
block.onTransport = (transportGuid != 0);
|
|
|
|
|
block.transportGuid = transportGuid;
|
2026-02-11 21:14:35 -08:00
|
|
|
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;
|
|
|
|
|
}
|
2026-02-05 21:55:52 -08:00
|
|
|
block.orientation = packet.readFloat();
|
|
|
|
|
/*float corpseOrientation =*/ packet.readFloat();
|
|
|
|
|
block.hasMovement = true;
|
|
|
|
|
|
2026-02-11 21:14:35 -08:00
|
|
|
if (block.onTransport) {
|
2026-03-10 04:51:01 -07:00
|
|
|
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, ")");
|
2026-02-11 21:14:35 -08:00
|
|
|
}
|
2026-02-05 21:55:52 -08:00
|
|
|
}
|
|
|
|
|
else if (updateFlags & UPDATEFLAG_STATIONARY_POSITION) {
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 16) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
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);
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
// Target GUID (for units with target)
|
|
|
|
|
if (updateFlags & UPDATEFLAG_HAS_TARGET) {
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 1) return false;
|
2026-03-25 14:06:42 -07:00
|
|
|
/*uint64_t targetGuid =*/ packet.readPackedGuid();
|
2026-02-05 21:55:52 -08:00
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-05 21:55:52 -08:00
|
|
|
// Transport time
|
|
|
|
|
if (updateFlags & UPDATEFLAG_TRANSPORT) {
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 4) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
/*uint32_t transportTime =*/ packet.readUInt32();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Vehicle
|
|
|
|
|
if (updateFlags & UPDATEFLAG_VEHICLE) {
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 8) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
/*uint32_t vehicleId =*/ packet.readUInt32();
|
|
|
|
|
/*float vehicleOrientation =*/ packet.readFloat();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Rotation (GameObjects)
|
|
|
|
|
if (updateFlags & UPDATEFLAG_ROTATION) {
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 8) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
/*int64_t rotation =*/ packet.readUInt64();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Low GUID
|
|
|
|
|
if (updateFlags & UPDATEFLAG_LOWGUID) {
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 4) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
/*uint32_t lowGuid =*/ packet.readUInt32();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// High GUID
|
|
|
|
|
if (updateFlags & UPDATEFLAG_HIGHGUID) {
|
fix: add bounds checks to WotLK movement block parser
Complete the parser hardening across all expansions. Check remaining
bytes before every conditional read in the WotLK base
UpdateObjectParser::parseMovementBlock: LIVING entry (66-byte minimum),
transport, pitch, fall time, jumping, spline elevation, speeds,
POSITION, STATIONARY, and all tail flags (HAS_TARGET, TRANSPORT,
VEHICLE, ROTATION, LOWGUID, HIGHGUID). Prevents silent garbage reads
when Packet::readUInt8/readFloat return 0 past EOF.
2026-03-18 08:04:00 -07:00
|
|
|
if (rem() < 4) return false;
|
2026-02-05 21:55:52 -08:00
|
|
|
/*uint32_t highGuid =*/ packet.readUInt32();
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool UpdateObjectParser::parseUpdateFields(network::Packet& packet, UpdateBlock& block) {
|
2026-02-10 01:24:37 -08:00
|
|
|
size_t startPos = packet.getReadPos();
|
|
|
|
|
|
2026-03-25 14:39:01 -07:00
|
|
|
if (!packet.hasData()) return false;
|
2026-03-18 08:08:08 -07:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-23 16:32:59 -07:00
|
|
|
// Sanity check: UNIT_END=148 needs 5 mask blocks, PLAYER_END=1472 needs 46.
|
2026-03-24 08:23:00 -07:00
|
|
|
// 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
|
2026-03-23 16:32:59 -07:00
|
|
|
if (blockCount > maxExpectedBlocks) {
|
2026-03-25 11:40:49 -07:00
|
|
|
LOG_WARNING("UpdateObjectParser: suspicious maskBlockCount=", static_cast<int>(blockCount),
|
|
|
|
|
" for objectType=", static_cast<int>(block.objectType),
|
2026-03-23 16:32:59 -07:00
|
|
|
" 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());
|
2026-04-05 20:12:17 -07:00
|
|
|
// 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;
|
2026-03-23 16:32:59 -07:00
|
|
|
}
|
|
|
|
|
|
2026-02-10 01:24:37 -08:00
|
|
|
uint32_t fieldsCapacity = blockCount * 32;
|
2026-02-11 21:14:35 -08:00
|
|
|
LOG_DEBUG(" UPDATE MASK PARSE:");
|
2026-03-25 11:40:49 -07:00
|
|
|
LOG_DEBUG(" maskBlockCount = ", static_cast<int>(blockCount));
|
2026-02-11 21:14:35 -08:00
|
|
|
LOG_DEBUG(" fieldsCapacity (blocks * 32) = ", fieldsCapacity);
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-22 08:30:18 -08:00
|
|
|
// Read update mask into a reused scratch buffer to avoid per-block allocations.
|
|
|
|
|
static thread_local std::vector<uint32_t> updateMask;
|
|
|
|
|
updateMask.resize(blockCount);
|
2026-02-02 12:24:50 -08:00
|
|
|
for (int i = 0; i < blockCount; ++i) {
|
2026-03-11 14:41:25 -07:00
|
|
|
// Validate 4 bytes available before each block read
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(4)) {
|
2026-03-15 03:40:58 -07:00
|
|
|
LOG_WARNING("UpdateObjectParser: truncated update mask at block ", i,
|
|
|
|
|
" type=", updateTypeName(block.updateType),
|
|
|
|
|
" objectType=", static_cast<int>(block.objectType),
|
|
|
|
|
" guid=0x", std::hex, block.guid, std::dec,
|
|
|
|
|
" readPos=", packet.getReadPos(),
|
|
|
|
|
" size=", packet.getSize(),
|
|
|
|
|
" maskBlockCount=", static_cast<int>(blockCount));
|
2026-03-11 14:41:25 -07:00
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
updateMask[i] = packet.readUInt32();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 01:24:37 -08:00
|
|
|
// Find highest set bit
|
|
|
|
|
uint16_t highestSetBit = 0;
|
|
|
|
|
uint32_t valuesReadCount = 0;
|
|
|
|
|
|
2026-02-22 08:30:18 -08:00
|
|
|
// Read only set bits in each mask block (faster than scanning all 32 bits).
|
2026-02-02 12:24:50 -08:00
|
|
|
for (int blockIdx = 0; blockIdx < blockCount; ++blockIdx) {
|
|
|
|
|
uint32_t mask = updateMask[blockIdx];
|
2026-02-22 08:30:18 -08:00
|
|
|
while (mask != 0) {
|
|
|
|
|
const uint16_t fieldIndex =
|
|
|
|
|
#if defined(__GNUC__) || defined(__clang__)
|
|
|
|
|
static_cast<uint16_t>(blockIdx * 32 + __builtin_ctz(mask));
|
|
|
|
|
#else
|
|
|
|
|
static_cast<uint16_t>(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;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
2026-03-11 14:41:25 -07:00
|
|
|
// Validate 4 bytes available before reading field value
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(4)) {
|
2026-03-15 03:40:58 -07:00
|
|
|
LOG_WARNING("UpdateObjectParser: truncated field value at field ", fieldIndex,
|
|
|
|
|
" type=", updateTypeName(block.updateType),
|
|
|
|
|
" objectType=", static_cast<int>(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);
|
2026-03-11 14:41:25 -07:00
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-22 08:30:18 -08:00
|
|
|
uint32_t value = packet.readUInt32();
|
2026-02-22 08:37:02 -08:00
|
|
|
// fieldIndex is monotonically increasing here, so end() is a good insertion hint.
|
|
|
|
|
block.fields.emplace_hint(block.fields.end(), fieldIndex, value);
|
2026-02-22 08:30:18 -08:00
|
|
|
valuesReadCount++;
|
|
|
|
|
|
|
|
|
|
LOG_DEBUG(" Field[", fieldIndex, "] = 0x", std::hex, value, std::dec);
|
|
|
|
|
mask &= (mask - 1u);
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-10 01:24:37 -08:00
|
|
|
size_t endPos = packet.getReadPos();
|
|
|
|
|
size_t bytesUsed = endPos - startPos;
|
|
|
|
|
size_t bytesRemaining = packet.getSize() - endPos;
|
|
|
|
|
|
2026-02-11 21:14:35 -08:00
|
|
|
LOG_DEBUG(" highestSetBitIndex = ", highestSetBit);
|
|
|
|
|
LOG_DEBUG(" valuesReadCount = ", valuesReadCount);
|
|
|
|
|
LOG_DEBUG(" bytesUsedForFields = ", bytesUsed);
|
|
|
|
|
LOG_DEBUG(" bytesRemainingInPacket = ", bytesRemaining);
|
|
|
|
|
LOG_DEBUG(" Parsed ", block.fields.size(), " fields");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool UpdateObjectParser::parseUpdateBlock(network::Packet& packet, UpdateBlock& block) {
|
2026-03-25 14:39:01 -07:00
|
|
|
if (!packet.hasData()) return false;
|
2026-03-18 08:08:08 -07:00
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Read update type
|
|
|
|
|
uint8_t updateTypeVal = packet.readUInt8();
|
|
|
|
|
block.updateType = static_cast<UpdateType>(updateTypeVal);
|
|
|
|
|
|
2026-03-25 11:40:49 -07:00
|
|
|
LOG_DEBUG("Update block: type=", static_cast<int>(updateTypeVal));
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
switch (block.updateType) {
|
|
|
|
|
case UpdateType::VALUES: {
|
|
|
|
|
// Partial update - changed fields only
|
2026-03-25 14:39:01 -07:00
|
|
|
if (!packet.hasData()) return false;
|
2026-03-25 14:06:42 -07:00
|
|
|
block.guid = packet.readPackedGuid();
|
2026-02-02 12:24:50 -08:00
|
|
|
LOG_DEBUG(" VALUES update for GUID: 0x", std::hex, block.guid, std::dec);
|
|
|
|
|
|
|
|
|
|
return parseUpdateFields(packet, block);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case UpdateType::MOVEMENT: {
|
|
|
|
|
// Movement update
|
2026-03-25 16:10:48 -07:00
|
|
|
if (!packet.hasRemaining(8)) return false;
|
2026-03-15 03:40:58 -07:00
|
|
|
block.guid = packet.readUInt64();
|
2026-02-02 12:24:50 -08:00
|
|
|
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
|
2026-03-25 14:39:01 -07:00
|
|
|
if (!packet.hasData()) return false;
|
2026-03-25 14:06:42 -07:00
|
|
|
block.guid = packet.readPackedGuid();
|
2026-02-02 12:24:50 -08:00
|
|
|
LOG_DEBUG(" CREATE_OBJECT for GUID: 0x", std::hex, block.guid, std::dec);
|
|
|
|
|
|
|
|
|
|
// Read object type
|
2026-03-25 14:39:01 -07:00
|
|
|
if (!packet.hasData()) return false;
|
2026-02-02 12:24:50 -08:00
|
|
|
uint8_t objectTypeVal = packet.readUInt8();
|
|
|
|
|
block.objectType = static_cast<ObjectType>(objectTypeVal);
|
2026-03-25 11:40:49 -07:00
|
|
|
LOG_DEBUG(" Object type: ", static_cast<int>(objectTypeVal));
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// 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:
|
2026-03-25 11:40:49 -07:00
|
|
|
LOG_WARNING("Unknown update type: ", static_cast<int>(updateTypeVal));
|
2026-02-02 12:24:50 -08:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool UpdateObjectParser::parse(network::Packet& packet, UpdateObjectData& data) {
|
2026-03-15 01:21:23 -07:00
|
|
|
// 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;
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
// Read block count
|
|
|
|
|
data.blockCount = packet.readUInt32();
|
2026-02-22 07:26:54 -08:00
|
|
|
if (data.blockCount > kMaxReasonableUpdateBlocks) {
|
|
|
|
|
LOG_ERROR("SMSG_UPDATE_OBJECT rejected: unreasonable blockCount=", data.blockCount,
|
|
|
|
|
" packetSize=", packet.getSize());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-11 21:14:35 -08:00
|
|
|
LOG_DEBUG("SMSG_UPDATE_OBJECT:");
|
|
|
|
|
LOG_DEBUG(" objectCount = ", data.blockCount);
|
|
|
|
|
LOG_DEBUG(" packetSize = ", packet.getSize());
|
2026-02-10 01:24:37 -08:00
|
|
|
|
2026-03-15 03:40:58 -07:00
|
|
|
uint32_t remainingBlockCount = data.blockCount;
|
|
|
|
|
|
2026-02-02 12:24:50 -08:00
|
|
|
// Check for out-of-range objects first
|
2026-03-25 16:10:48 -07:00
|
|
|
if (packet.hasRemaining(1)) {
|
2026-02-02 12:24:50 -08:00
|
|
|
uint8_t firstByte = packet.readUInt8();
|
|
|
|
|
|
|
|
|
|
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
|
2026-03-15 03:40:58 -07:00
|
|
|
if (remainingBlockCount == 0) {
|
|
|
|
|
LOG_ERROR("SMSG_UPDATE_OBJECT rejected: OUT_OF_RANGE_OBJECTS with zero blockCount");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
--remainingBlockCount;
|
2026-02-02 12:24:50 -08:00
|
|
|
// Read out-of-range GUID count
|
|
|
|
|
uint32_t count = packet.readUInt32();
|
2026-02-22 07:26:54 -08:00
|
|
|
if (count > kMaxReasonableOutOfRangeGuids) {
|
|
|
|
|
LOG_ERROR("SMSG_UPDATE_OBJECT rejected: unreasonable outOfRange count=", count,
|
|
|
|
|
" packetSize=", packet.getSize());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
for (uint32_t i = 0; i < count; ++i) {
|
2026-03-25 14:06:42 -07:00
|
|
|
uint64_t guid = packet.readPackedGuid();
|
2026-02-02 12:24:50 -08:00
|
|
|
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
|
2026-03-15 03:40:58 -07:00
|
|
|
data.blockCount = remainingBlockCount;
|
2026-02-02 12:24:50 -08:00
|
|
|
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)) {
|
2026-02-23 04:32:58 -08:00
|
|
|
static int parseBlockErrors = 0;
|
2026-04-10 23:30:55 +03:00
|
|
|
const uint32_t lostBlocks = data.blockCount - i;
|
|
|
|
|
if (++parseBlockErrors <= 10) {
|
2026-03-10 08:38:39 -07:00
|
|
|
LOG_ERROR("Failed to parse update block ", i + 1, " of ", data.blockCount,
|
2026-04-10 23:30:55 +03:00
|
|
|
" (", i, " blocks parsed, ", lostBlocks, " blocks LOST",
|
|
|
|
|
", remaining=", packet.getRemainingSize(), " bytes)");
|
|
|
|
|
if (parseBlockErrors == 10)
|
2026-02-23 04:32:58 -08:00
|
|
|
LOG_ERROR("(suppressing further update block parse errors)");
|
|
|
|
|
}
|
2026-03-10 08:38:39 -07:00
|
|
|
// Cannot reliably re-sync to the next block after a parse failure,
|
|
|
|
|
// but still return true so the blocks already parsed are processed.
|
|
|
|
|
break;
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
2026-02-22 08:37:02 -08:00
|
|
|
data.blocks.emplace_back(std::move(block));
|
2026-02-02 12:24:50 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool DestroyObjectParser::parse(network::Packet& packet, DestroyObjectData& data) {
|
|
|
|
|
// SMSG_DESTROY_OBJECT format:
|
|
|
|
|
// uint64 guid
|
2026-02-13 18:59:09 -08:00
|
|
|
// uint8 isDeath (0 = despawn, 1 = death) — WotLK only; vanilla/TBC omit this
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-13 18:59:09 -08:00
|
|
|
if (packet.getSize() < 8) {
|
2026-02-02 12:24:50 -08:00
|
|
|
LOG_ERROR("SMSG_DESTROY_OBJECT packet too small: ", packet.getSize(), " bytes");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data.guid = packet.readUInt64();
|
2026-02-13 18:59:09 -08:00
|
|
|
// WotLK adds isDeath byte; vanilla/TBC packets are exactly 8 bytes
|
2026-03-25 14:39:01 -07:00
|
|
|
if (packet.hasData()) {
|
2026-02-13 18:59:09 -08:00
|
|
|
data.isDeath = (packet.readUInt8() != 0);
|
|
|
|
|
} else {
|
|
|
|
|
data.isDeath = false;
|
|
|
|
|
}
|
2026-02-02 12:24:50 -08:00
|
|
|
|
2026-02-15 14:00:41 -08:00
|
|
|
LOG_DEBUG("Parsed SMSG_DESTROY_OBJECT:");
|
|
|
|
|
LOG_DEBUG(" GUID: 0x", std::hex, data.guid, std::dec);
|
|
|
|
|
LOG_DEBUG(" Is death: ", data.isDeath ? "yes" : "no");
|
2026-02-02 12:24:50 -08:00
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace game
|
|
|
|
|
} // namespace wowee
|