mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-03-22 23:30:14 +00:00
Mirror the Turtle parser hardening: check remaining bytes before every conditional read in ClassicPacketParsers::parseMovementBlock. Prevents silent garbage reads (readUInt8 returns 0 past EOF) that corrupt subsequent update fields and lose NPC data in multi-block packets.
2436 lines
100 KiB
C++
2436 lines
100 KiB
C++
#include "game/packet_parsers.hpp"
|
||
#include "core/logger.hpp"
|
||
#include <cstdio>
|
||
#include <functional>
|
||
#include <string>
|
||
|
||
namespace wowee {
|
||
namespace game {
|
||
|
||
namespace {
|
||
|
||
bool hasFullPackedGuid(const network::Packet& packet) {
|
||
if (packet.getReadPos() >= packet.getSize()) {
|
||
return false;
|
||
}
|
||
|
||
const auto& rawData = packet.getData();
|
||
const uint8_t mask = rawData[packet.getReadPos()];
|
||
size_t guidBytes = 1;
|
||
for (int bit = 0; bit < 8; ++bit) {
|
||
if ((mask & (1u << bit)) != 0) {
|
||
++guidBytes;
|
||
}
|
||
}
|
||
return packet.getSize() - packet.getReadPos() >= guidBytes;
|
||
}
|
||
|
||
std::string formatPacketBytes(const network::Packet& packet, size_t startPos) {
|
||
const auto& rawData = packet.getData();
|
||
if (startPos >= rawData.size()) {
|
||
return {};
|
||
}
|
||
|
||
std::string hex;
|
||
hex.reserve((rawData.size() - startPos) * 3);
|
||
for (size_t i = startPos; i < rawData.size(); ++i) {
|
||
char buf[4];
|
||
std::snprintf(buf, sizeof(buf), "%02x ", rawData[i]);
|
||
hex += buf;
|
||
}
|
||
if (!hex.empty()) {
|
||
hex.pop_back();
|
||
}
|
||
return hex;
|
||
}
|
||
|
||
bool skipClassicSpellCastTargets(network::Packet& packet, uint64_t* primaryTargetGuid = nullptr) {
|
||
if (packet.getSize() - packet.getReadPos() < 2) {
|
||
return false;
|
||
}
|
||
|
||
const uint16_t targetFlags = packet.readUInt16();
|
||
|
||
const auto readPackedTargetGuid = [&](bool capture) -> bool {
|
||
if (!hasFullPackedGuid(packet)) {
|
||
return false;
|
||
}
|
||
const uint64_t guid = UpdateObjectParser::readPackedGuid(packet);
|
||
if (capture && primaryTargetGuid && *primaryTargetGuid == 0) {
|
||
*primaryTargetGuid = guid;
|
||
}
|
||
return true;
|
||
};
|
||
|
||
// Common Classic/Turtle SpellCastTargets payloads.
|
||
if ((targetFlags & 0x0002) != 0 && !readPackedTargetGuid(true)) { // UNIT
|
||
return false;
|
||
}
|
||
if ((targetFlags & 0x0004) != 0 && !readPackedTargetGuid(false)) { // UNIT_MINIPET/extra guid
|
||
return false;
|
||
}
|
||
if ((targetFlags & 0x0010) != 0 && !readPackedTargetGuid(false)) { // ITEM
|
||
return false;
|
||
}
|
||
if ((targetFlags & 0x0800) != 0 && !readPackedTargetGuid(true)) { // OBJECT
|
||
return false;
|
||
}
|
||
if ((targetFlags & 0x8000) != 0 && !readPackedTargetGuid(false)) { // CORPSE
|
||
return false;
|
||
}
|
||
|
||
if ((targetFlags & 0x0020) != 0) { // SOURCE_LOCATION
|
||
if (packet.getSize() - packet.getReadPos() < 12) {
|
||
return false;
|
||
}
|
||
(void)packet.readFloat();
|
||
(void)packet.readFloat();
|
||
(void)packet.readFloat();
|
||
}
|
||
if ((targetFlags & 0x0040) != 0) { // DEST_LOCATION
|
||
if (packet.getSize() - packet.getReadPos() < 12) {
|
||
return false;
|
||
}
|
||
(void)packet.readFloat();
|
||
(void)packet.readFloat();
|
||
(void)packet.readFloat();
|
||
}
|
||
|
||
if ((targetFlags & 0x1000) != 0) { // TRADE_ITEM
|
||
if (packet.getSize() - packet.getReadPos() < 1) {
|
||
return false;
|
||
}
|
||
(void)packet.readUInt8();
|
||
}
|
||
|
||
if ((targetFlags & 0x2000) != 0) { // STRING
|
||
const auto& rawData = packet.getData();
|
||
size_t pos = packet.getReadPos();
|
||
while (pos < rawData.size() && rawData[pos] != 0) {
|
||
++pos;
|
||
}
|
||
if (pos >= rawData.size()) {
|
||
return false;
|
||
}
|
||
packet.setReadPos(pos + 1);
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
const char* updateTypeName(UpdateType type) {
|
||
switch (type) {
|
||
case UpdateType::VALUES: return "VALUES";
|
||
case UpdateType::MOVEMENT: return "MOVEMENT";
|
||
case UpdateType::CREATE_OBJECT: return "CREATE_OBJECT";
|
||
case UpdateType::CREATE_OBJECT2: return "CREATE_OBJECT2";
|
||
case UpdateType::OUT_OF_RANGE_OBJECTS: return "OUT_OF_RANGE_OBJECTS";
|
||
case UpdateType::NEAR_OBJECTS: return "NEAR_OBJECTS";
|
||
default: return "UNKNOWN";
|
||
}
|
||
}
|
||
|
||
} // namespace
|
||
|
||
// ============================================================================
|
||
// Classic 1.12.1 movement flag constants
|
||
// Key differences from TBC:
|
||
// - SPLINE_ENABLED at 0x00400000 (TBC/WotLK: 0x08000000)
|
||
// - No FLYING flag (flight was added in TBC)
|
||
// - ONTRANSPORT at 0x02000000 (not used for pitch in Classic)
|
||
// Same as TBC: ON_TRANSPORT=0x200, JUMPING=0x2000, SWIMMING=0x200000,
|
||
// SPLINE_ELEVATION=0x04000000
|
||
// ============================================================================
|
||
namespace ClassicMoveFlags {
|
||
constexpr uint32_t ONTRANSPORT = 0x02000000; // Gates transport data (vmangos authoritative)
|
||
constexpr uint32_t JUMPING = 0x00002000; // Gates jump data
|
||
constexpr uint32_t SWIMMING = 0x00200000; // Gates pitch
|
||
constexpr uint32_t SPLINE_ENABLED = 0x00400000; // TBC/WotLK: 0x08000000
|
||
constexpr uint32_t SPLINE_ELEVATION = 0x04000000; // Same as TBC
|
||
}
|
||
|
||
uint32_t classicWireMoveFlags(uint32_t internalFlags) {
|
||
uint32_t wireFlags = internalFlags;
|
||
|
||
// Internal movement state is tracked with WotLK-era bits. Classic/Turtle
|
||
// movement packets still use the older transport/jump flag layout.
|
||
const uint32_t kInternalOnTransport = static_cast<uint32_t>(MovementFlags::ONTRANSPORT);
|
||
const uint32_t kInternalFalling =
|
||
static_cast<uint32_t>(MovementFlags::FALLING) |
|
||
static_cast<uint32_t>(MovementFlags::FALLINGFAR);
|
||
const uint32_t kClassicConflicts =
|
||
static_cast<uint32_t>(MovementFlags::ASCENDING) |
|
||
static_cast<uint32_t>(MovementFlags::CAN_FLY) |
|
||
static_cast<uint32_t>(MovementFlags::FLYING) |
|
||
static_cast<uint32_t>(MovementFlags::HOVER);
|
||
|
||
wireFlags &= ~kClassicConflicts;
|
||
|
||
if ((internalFlags & kInternalOnTransport) != 0) {
|
||
wireFlags &= ~kInternalOnTransport;
|
||
wireFlags |= ClassicMoveFlags::ONTRANSPORT;
|
||
}
|
||
|
||
if ((internalFlags & kInternalFalling) != 0) {
|
||
wireFlags &= ~kInternalFalling;
|
||
wireFlags |= ClassicMoveFlags::JUMPING;
|
||
}
|
||
|
||
return wireFlags;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic parseMovementBlock
|
||
// Key differences from TBC:
|
||
// - NO moveFlags2 (TBC reads u8, WotLK reads u16)
|
||
// - SPLINE_ENABLED at 0x00400000 (not 0x08000000)
|
||
// - Transport data: NO timestamp (TBC adds u32 timestamp)
|
||
// - Pitch: only SWIMMING (no ONTRANSPORT secondary pitch, no FLYING)
|
||
// Same as TBC: u8 UpdateFlags, JUMPING=0x2000, 8 speeds, no pitchRate
|
||
// ============================================================================
|
||
bool ClassicPacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) {
|
||
auto rem = [&]() -> size_t { return packet.getSize() - packet.getReadPos(); };
|
||
if (rem() < 1) return false;
|
||
|
||
// Classic: UpdateFlags is uint8 (same as TBC)
|
||
uint8_t updateFlags = packet.readUInt8();
|
||
block.updateFlags = static_cast<uint16_t>(updateFlags);
|
||
|
||
LOG_DEBUG(" [Classic] UpdateFlags: 0x", std::hex, (int)updateFlags, std::dec);
|
||
|
||
const uint8_t UPDATEFLAG_TRANSPORT = 0x02;
|
||
const uint8_t UPDATEFLAG_MELEE_ATTACKING = 0x04;
|
||
const uint8_t UPDATEFLAG_HIGHGUID = 0x08;
|
||
const uint8_t UPDATEFLAG_ALL = 0x10;
|
||
const uint8_t UPDATEFLAG_LIVING = 0x20;
|
||
const uint8_t UPDATEFLAG_HAS_POSITION = 0x40;
|
||
|
||
if (updateFlags & UPDATEFLAG_LIVING) {
|
||
// Minimum: moveFlags(4)+time(4)+position(16)+fallTime(4)+speeds(24) = 52 bytes
|
||
if (rem() < 52) return false;
|
||
|
||
// Movement flags (u32 only — NO extra flags byte in Classic)
|
||
uint32_t moveFlags = packet.readUInt32();
|
||
/*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(" [Classic] LIVING: (", block.x, ", ", block.y, ", ", block.z,
|
||
"), o=", block.orientation, " moveFlags=0x", std::hex, moveFlags, std::dec);
|
||
|
||
// Transport data (Classic: ONTRANSPORT=0x02000000, no timestamp)
|
||
if (moveFlags & ClassicMoveFlags::ONTRANSPORT) {
|
||
if (rem() < 1) return false;
|
||
block.onTransport = true;
|
||
block.transportGuid = UpdateObjectParser::readPackedGuid(packet);
|
||
if (rem() < 16) return false; // 4 floats
|
||
block.transportX = packet.readFloat();
|
||
block.transportY = packet.readFloat();
|
||
block.transportZ = packet.readFloat();
|
||
block.transportO = packet.readFloat();
|
||
}
|
||
|
||
// Pitch (Classic: only SWIMMING, no FLYING or ONTRANSPORT pitch)
|
||
if (moveFlags & ClassicMoveFlags::SWIMMING) {
|
||
if (rem() < 4) return false;
|
||
/*float pitch =*/ packet.readFloat();
|
||
}
|
||
|
||
// Fall time (always present)
|
||
if (rem() < 4) return false;
|
||
/*uint32_t fallTime =*/ packet.readUInt32();
|
||
|
||
// Jumping (Classic: JUMPING=0x2000, same as TBC)
|
||
if (moveFlags & ClassicMoveFlags::JUMPING) {
|
||
if (rem() < 16) return false;
|
||
/*float jumpVelocity =*/ packet.readFloat();
|
||
/*float jumpSinAngle =*/ packet.readFloat();
|
||
/*float jumpCosAngle =*/ packet.readFloat();
|
||
/*float jumpXYSpeed =*/ packet.readFloat();
|
||
}
|
||
|
||
// Spline elevation
|
||
if (moveFlags & ClassicMoveFlags::SPLINE_ELEVATION) {
|
||
if (rem() < 4) return false;
|
||
/*float splineElevation =*/ packet.readFloat();
|
||
}
|
||
|
||
// Speeds (Classic: 6 values — no flight speeds, no pitchRate)
|
||
if (rem() < 24) return false;
|
||
/*float walkSpeed =*/ packet.readFloat();
|
||
float runSpeed = packet.readFloat();
|
||
/*float runBackSpeed =*/ packet.readFloat();
|
||
/*float swimSpeed =*/ packet.readFloat();
|
||
/*float swimBackSpeed =*/ packet.readFloat();
|
||
/*float turnRate =*/ packet.readFloat();
|
||
|
||
block.runSpeed = runSpeed;
|
||
block.moveFlags = moveFlags;
|
||
|
||
// Spline data (Classic: SPLINE_ENABLED=0x00400000)
|
||
if (moveFlags & ClassicMoveFlags::SPLINE_ENABLED) {
|
||
if (rem() < 4) return false;
|
||
uint32_t splineFlags = packet.readUInt32();
|
||
LOG_DEBUG(" [Classic] Spline: flags=0x", std::hex, splineFlags, std::dec);
|
||
|
||
if (splineFlags & 0x00010000) { // FINAL_POINT
|
||
if (rem() < 12) return false;
|
||
/*float finalX =*/ packet.readFloat();
|
||
/*float finalY =*/ packet.readFloat();
|
||
/*float finalZ =*/ packet.readFloat();
|
||
} else if (splineFlags & 0x00020000) { // FINAL_TARGET
|
||
if (rem() < 8) return false;
|
||
/*uint64_t finalTarget =*/ packet.readUInt64();
|
||
} else if (splineFlags & 0x00040000) { // FINAL_ANGLE
|
||
if (rem() < 4) return false;
|
||
/*float finalAngle =*/ packet.readFloat();
|
||
}
|
||
|
||
// Classic spline: timePassed, duration, id, pointCount
|
||
if (rem() < 16) return false;
|
||
/*uint32_t timePassed =*/ packet.readUInt32();
|
||
/*uint32_t duration =*/ packet.readUInt32();
|
||
/*uint32_t splineId =*/ packet.readUInt32();
|
||
|
||
uint32_t pointCount = packet.readUInt32();
|
||
if (pointCount > 256) return false;
|
||
|
||
// points + endPoint (no splineMode in Classic)
|
||
if (rem() < static_cast<size_t>(pointCount) * 12 + 12) return false;
|
||
for (uint32_t i = 0; i < pointCount; i++) {
|
||
/*float px =*/ packet.readFloat();
|
||
/*float py =*/ packet.readFloat();
|
||
/*float pz =*/ packet.readFloat();
|
||
}
|
||
|
||
// Classic: NO splineMode byte
|
||
/*float endPointX =*/ packet.readFloat();
|
||
/*float endPointY =*/ packet.readFloat();
|
||
/*float endPointZ =*/ packet.readFloat();
|
||
}
|
||
}
|
||
else if (updateFlags & UPDATEFLAG_HAS_POSITION) {
|
||
if (rem() < 16) return false;
|
||
block.x = packet.readFloat();
|
||
block.y = packet.readFloat();
|
||
block.z = packet.readFloat();
|
||
block.orientation = packet.readFloat();
|
||
block.hasMovement = true;
|
||
|
||
LOG_DEBUG(" [Classic] STATIONARY: (", block.x, ", ", block.y, ", ", block.z, ")");
|
||
}
|
||
|
||
// High GUID
|
||
if (updateFlags & UPDATEFLAG_HIGHGUID) {
|
||
if (rem() < 4) return false;
|
||
/*uint32_t highGuid =*/ packet.readUInt32();
|
||
}
|
||
|
||
// ALL/SELF extra uint32
|
||
if (updateFlags & UPDATEFLAG_ALL) {
|
||
if (rem() < 4) return false;
|
||
/*uint32_t unkAll =*/ packet.readUInt32();
|
||
}
|
||
|
||
// Current melee target as packed guid
|
||
if (updateFlags & UPDATEFLAG_MELEE_ATTACKING) {
|
||
if (rem() < 1) return false;
|
||
/*uint64_t meleeTargetGuid =*/ UpdateObjectParser::readPackedGuid(packet);
|
||
}
|
||
|
||
// Transport progress / world time
|
||
if (updateFlags & UPDATEFLAG_TRANSPORT) {
|
||
if (rem() < 4) return false;
|
||
/*uint32_t transportTime =*/ packet.readUInt32();
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic writeMovementPayload
|
||
// Key differences from TBC:
|
||
// - NO flags2 byte (TBC writes u8)
|
||
// - Transport data: NO timestamp
|
||
// - Pitch: only SWIMMING (no ONTRANSPORT pitch)
|
||
// ============================================================================
|
||
void ClassicPacketParsers::writeMovementPayload(network::Packet& packet, const MovementInfo& info) {
|
||
const uint32_t wireFlags = classicWireMoveFlags(info.flags);
|
||
|
||
// Movement flags (uint32)
|
||
packet.writeUInt32(wireFlags);
|
||
|
||
// Classic: NO flags2 byte (TBC has u8, WotLK has u16)
|
||
|
||
// Timestamp
|
||
packet.writeUInt32(info.time);
|
||
|
||
// Position
|
||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.x), sizeof(float));
|
||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.y), sizeof(float));
|
||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.z), sizeof(float));
|
||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.orientation), sizeof(float));
|
||
|
||
// Transport data (Classic ONTRANSPORT = 0x02000000, no timestamp)
|
||
if (wireFlags & ClassicMoveFlags::ONTRANSPORT) {
|
||
// Packed transport GUID
|
||
uint8_t transMask = 0;
|
||
uint8_t transGuidBytes[8];
|
||
int transGuidByteCount = 0;
|
||
for (int i = 0; i < 8; i++) {
|
||
uint8_t byte = static_cast<uint8_t>((info.transportGuid >> (i * 8)) & 0xFF);
|
||
if (byte != 0) {
|
||
transMask |= (1 << i);
|
||
transGuidBytes[transGuidByteCount++] = byte;
|
||
}
|
||
}
|
||
packet.writeUInt8(transMask);
|
||
for (int i = 0; i < transGuidByteCount; i++) {
|
||
packet.writeUInt8(transGuidBytes[i]);
|
||
}
|
||
|
||
// Transport local position
|
||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportX), sizeof(float));
|
||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportY), sizeof(float));
|
||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportZ), sizeof(float));
|
||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.transportO), sizeof(float));
|
||
|
||
// Classic: NO transport timestamp
|
||
// Classic: NO transport seat byte
|
||
}
|
||
|
||
// Pitch (Classic: only SWIMMING)
|
||
if (wireFlags & ClassicMoveFlags::SWIMMING) {
|
||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.pitch), sizeof(float));
|
||
}
|
||
|
||
// Fall time (always present)
|
||
packet.writeUInt32(info.fallTime);
|
||
|
||
// Jump data (Classic JUMPING = 0x2000)
|
||
if (wireFlags & ClassicMoveFlags::JUMPING) {
|
||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpVelocity), sizeof(float));
|
||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpSinAngle), sizeof(float));
|
||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpCosAngle), sizeof(float));
|
||
packet.writeBytes(reinterpret_cast<const uint8_t*>(&info.jumpXYSpeed), sizeof(float));
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic buildMovementPacket
|
||
// Classic/TBC: client movement packets do NOT include PackedGuid prefix
|
||
// (WotLK added PackedGuid to client packets)
|
||
// ============================================================================
|
||
network::Packet ClassicPacketParsers::buildMovementPacket(LogicalOpcode opcode,
|
||
const MovementInfo& info,
|
||
uint64_t /*playerGuid*/) {
|
||
network::Packet packet(wireOpcode(opcode));
|
||
|
||
// Classic: NO PackedGuid prefix for client packets
|
||
writeMovementPayload(packet, info);
|
||
|
||
return packet;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic buildCastSpell
|
||
// Vanilla 1.12.x: NO castCount prefix, NO castFlags byte
|
||
// Format: uint32 spellId + uint16 targetFlags + [PackedGuid if unit target]
|
||
// ============================================================================
|
||
network::Packet ClassicPacketParsers::buildCastSpell(uint32_t spellId, uint64_t targetGuid, uint8_t /*castCount*/) {
|
||
network::Packet packet(wireOpcode(LogicalOpcode::CMSG_CAST_SPELL));
|
||
|
||
packet.writeUInt32(spellId);
|
||
|
||
// SpellCastTargets — vanilla/CMaNGOS uses uint16 target mask (WotLK uses uint32)
|
||
if (targetGuid != 0) {
|
||
packet.writeUInt16(0x02); // TARGET_FLAG_UNIT
|
||
|
||
// Write packed GUID
|
||
uint8_t mask = 0;
|
||
uint8_t bytes[8];
|
||
int byteCount = 0;
|
||
uint64_t g = targetGuid;
|
||
for (int i = 0; i < 8; ++i) {
|
||
uint8_t b = g & 0xFF;
|
||
if (b != 0) {
|
||
mask |= (1 << i);
|
||
bytes[byteCount++] = b;
|
||
}
|
||
g >>= 8;
|
||
}
|
||
packet.writeUInt8(mask);
|
||
for (int i = 0; i < byteCount; ++i) {
|
||
packet.writeUInt8(bytes[i]);
|
||
}
|
||
} else {
|
||
packet.writeUInt16(0x00); // TARGET_FLAG_SELF
|
||
}
|
||
|
||
return packet;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic CMSG_USE_ITEM
|
||
// Vanilla 1.12.x: bag(u8) + slot(u8) + spellIndex(u8) + SpellCastTargets(u16)
|
||
// NO spellId, itemGuid, glyphIndex, or castFlags fields (those are WotLK)
|
||
// ============================================================================
|
||
network::Packet ClassicPacketParsers::buildUseItem(uint8_t bagIndex, uint8_t slotIndex, uint64_t /*itemGuid*/, uint32_t /*spellId*/) {
|
||
network::Packet packet(wireOpcode(LogicalOpcode::CMSG_USE_ITEM));
|
||
packet.writeUInt8(bagIndex);
|
||
packet.writeUInt8(slotIndex);
|
||
packet.writeUInt8(0); // spell_index (which item spell to trigger, usually 0)
|
||
packet.writeUInt16(0x0000); // SpellCastTargets: TARGET_FLAG_SELF
|
||
return packet;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic parseSpellStart — Vanilla 1.12 SMSG_SPELL_START
|
||
//
|
||
// Key differences from TBC:
|
||
// - GUIDs are PackedGuid (variable-length byte mask + non-zero bytes),
|
||
// NOT full uint64 as in TBC/WotLK.
|
||
// - castFlags is uint16 (NOT uint32 as in TBC/WotLK).
|
||
// - SpellCastTargets uses uint16 targetFlags (NOT uint32 as in TBC).
|
||
//
|
||
// Format: PackedGuid(casterObj) + PackedGuid(casterUnit)
|
||
// + uint32(spellId) + uint16(castFlags) + uint32(castTime)
|
||
// + uint16(targetFlags) [+ PackedGuid(unitTarget) if TARGET_FLAG_UNIT]
|
||
// ============================================================================
|
||
bool ClassicPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& data) {
|
||
data = SpellStartData{};
|
||
|
||
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||
const size_t startPos = packet.getReadPos();
|
||
if (rem() < 2) return false;
|
||
|
||
if (!hasFullPackedGuid(packet)) {
|
||
packet.setReadPos(startPos);
|
||
return false;
|
||
}
|
||
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
|
||
if (!hasFullPackedGuid(packet)) {
|
||
packet.setReadPos(startPos);
|
||
return false;
|
||
}
|
||
data.casterUnit = UpdateObjectParser::readPackedGuid(packet);
|
||
|
||
// Vanilla/Turtle SMSG_SPELL_START does not include castCount here.
|
||
// Layout after the two packed GUIDs is spellId(u32) + castFlags(u16) + castTime(u32).
|
||
if (rem() < 10) return false;
|
||
data.castCount = 0;
|
||
data.spellId = packet.readUInt32();
|
||
data.castFlags = packet.readUInt16(); // uint16 in Vanilla (uint32 in TBC/WotLK)
|
||
data.castTime = packet.readUInt32();
|
||
|
||
// SpellCastTargets: consume ALL target payload types so subsequent reads stay aligned.
|
||
// Previously only UNIT(0x02)/OBJECT(0x800) were handled; DEST_LOCATION(0x40),
|
||
// SOURCE_LOCATION(0x20), and ITEM(0x10) bytes were silently skipped, corrupting
|
||
// castFlags/castTime for every AOE/ground-targeted spell (Rain of Fire, Blizzard, etc.).
|
||
{
|
||
uint64_t targetGuid = 0;
|
||
// skipClassicSpellCastTargets reads uint16 targetFlags and all payloads.
|
||
// Non-fatal on truncation: self-cast spells have zero-byte targets.
|
||
skipClassicSpellCastTargets(packet, &targetGuid);
|
||
data.targetGuid = targetGuid;
|
||
}
|
||
|
||
LOG_DEBUG("[Classic] Spell start: spell=", data.spellId, " castTime=", data.castTime, "ms",
|
||
" targetGuid=0x", std::hex, data.targetGuid, std::dec);
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic parseSpellGo — Vanilla 1.12 SMSG_SPELL_GO
|
||
//
|
||
// Same GUID and castFlags format differences as parseSpellStart:
|
||
// - GUIDs are PackedGuid (not full uint64)
|
||
// - castFlags is uint16 (not uint32)
|
||
// - Hit/miss target GUIDs are also PackedGuid in Vanilla
|
||
//
|
||
// Format: PackedGuid(casterObj) + PackedGuid(casterUnit)
|
||
// + uint32(spellId) + uint16(castFlags)
|
||
// + uint8(hitCount) + [PackedGuid(hitTarget) × hitCount]
|
||
// + uint8(missCount) + [PackedGuid(missTarget) + uint8(missType)] × missCount
|
||
// ============================================================================
|
||
bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) {
|
||
// Always reset output to avoid stale targets when callers reuse buffers.
|
||
data = SpellGoData{};
|
||
|
||
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||
const size_t startPos = packet.getReadPos();
|
||
const bool traceSmallSpellGo = (packet.getSize() - startPos) <= 48;
|
||
const auto traceFailure = [&](const char* stage, size_t pos, uint32_t value = 0) {
|
||
if (!traceSmallSpellGo) {
|
||
return;
|
||
}
|
||
static uint32_t smallSpellGoTraceCount = 0;
|
||
++smallSpellGoTraceCount;
|
||
if (smallSpellGoTraceCount > 12 && (smallSpellGoTraceCount % 50) != 0) {
|
||
return;
|
||
}
|
||
LOG_WARNING("[Classic] Spell go trace: stage=", stage,
|
||
" pos=", pos,
|
||
" size=", packet.getSize() - startPos,
|
||
" spell=", data.spellId,
|
||
" castFlags=0x", std::hex, data.castFlags, std::dec,
|
||
" value=", value,
|
||
" bytes=[", formatPacketBytes(packet, startPos), "]");
|
||
};
|
||
if (rem() < 2) return false;
|
||
|
||
if (!hasFullPackedGuid(packet)) return false;
|
||
data.casterGuid = UpdateObjectParser::readPackedGuid(packet);
|
||
if (!hasFullPackedGuid(packet)) return false;
|
||
data.casterUnit = UpdateObjectParser::readPackedGuid(packet);
|
||
|
||
// Vanilla/Turtle SMSG_SPELL_GO does not include castCount here.
|
||
// Layout after the two packed GUIDs is spellId(u32) + castFlags(u16).
|
||
if (rem() < 6) return false;
|
||
data.castCount = 0;
|
||
data.spellId = packet.readUInt32();
|
||
data.castFlags = packet.readUInt16(); // uint16 in Vanilla (uint32 in TBC/WotLK)
|
||
|
||
const size_t countsPos = packet.getReadPos();
|
||
uint64_t ignoredTargetGuid = 0;
|
||
std::function<bool(bool)> parseHitAndMissLists = [&](bool allowTargetsFallback) -> bool {
|
||
packet.setReadPos(countsPos);
|
||
data.hitTargets.clear();
|
||
data.missTargets.clear();
|
||
ignoredTargetGuid = 0;
|
||
|
||
// hitCount is mandatory in SMSG_SPELL_GO. Missing byte means truncation.
|
||
if (rem() < 1) {
|
||
LOG_WARNING("[Classic] Spell go: missing hitCount after fixed fields");
|
||
traceFailure("missing_hit_count", packet.getReadPos());
|
||
packet.setReadPos(startPos);
|
||
return false;
|
||
}
|
||
const uint8_t rawHitCount = packet.readUInt8();
|
||
if (rawHitCount > 128) {
|
||
LOG_WARNING("[Classic] Spell go: hitCount capped (requested=", (int)rawHitCount, ")");
|
||
}
|
||
if (rem() < static_cast<size_t>(rawHitCount) + 1u) {
|
||
static uint32_t badHitCountTrunc = 0;
|
||
++badHitCountTrunc;
|
||
if (badHitCountTrunc <= 10 || (badHitCountTrunc % 100) == 0) {
|
||
LOG_WARNING("[Classic] Spell go: invalid hitCount/remaining (hits=", (int)rawHitCount,
|
||
" remaining=", rem(), " occurrence=", badHitCountTrunc, ")");
|
||
}
|
||
traceFailure("invalid_hit_count", packet.getReadPos(), rawHitCount);
|
||
packet.setReadPos(startPos);
|
||
return false;
|
||
}
|
||
|
||
const auto parseHitList = [&](bool usePackedGuids) -> bool {
|
||
packet.setReadPos(countsPos + 1); // after hitCount
|
||
data.hitTargets.clear();
|
||
const uint8_t storedHitLimit = std::min<uint8_t>(rawHitCount, 128);
|
||
data.hitTargets.reserve(storedHitLimit);
|
||
for (uint16_t i = 0; i < rawHitCount; ++i) {
|
||
uint64_t targetGuid = 0;
|
||
if (usePackedGuids) {
|
||
if (!hasFullPackedGuid(packet)) {
|
||
return false;
|
||
}
|
||
targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
||
} else {
|
||
if (rem() < 8) {
|
||
return false;
|
||
}
|
||
targetGuid = packet.readUInt64();
|
||
}
|
||
if (i < storedHitLimit) {
|
||
data.hitTargets.push_back(targetGuid);
|
||
}
|
||
}
|
||
data.hitCount = static_cast<uint8_t>(data.hitTargets.size());
|
||
return true;
|
||
};
|
||
|
||
if (!parseHitList(false) && !parseHitList(true)) {
|
||
LOG_WARNING("[Classic] Spell go: truncated hit targets at index 0/", (int)rawHitCount);
|
||
traceFailure("truncated_hit_target", packet.getReadPos(), rawHitCount);
|
||
packet.setReadPos(startPos);
|
||
return false;
|
||
}
|
||
|
||
std::function<bool(size_t, bool)> parseMissListFrom = [&](size_t missStartPos,
|
||
bool allowMidTargetsFallback) -> bool {
|
||
packet.setReadPos(missStartPos);
|
||
data.missTargets.clear();
|
||
|
||
if (rem() < 1) {
|
||
LOG_WARNING("[Classic] Spell go: missing missCount after hit target list");
|
||
traceFailure("missing_miss_count", packet.getReadPos());
|
||
packet.setReadPos(startPos);
|
||
return false;
|
||
}
|
||
const uint8_t rawMissCount = packet.readUInt8();
|
||
if (rawMissCount > 128) {
|
||
LOG_WARNING("[Classic] Spell go: missCount capped (requested=", (int)rawMissCount, ")");
|
||
traceFailure("miss_count_capped", packet.getReadPos() - 1, rawMissCount);
|
||
}
|
||
if (rem() < static_cast<size_t>(rawMissCount) * 2u) {
|
||
if (allowMidTargetsFallback) {
|
||
packet.setReadPos(missStartPos);
|
||
if (skipClassicSpellCastTargets(packet, &ignoredTargetGuid)) {
|
||
traceFailure("mid_targets_fallback", missStartPos, ignoredTargetGuid != 0 ? 1u : 0u);
|
||
return parseMissListFrom(packet.getReadPos(), false);
|
||
}
|
||
}
|
||
if (allowTargetsFallback) {
|
||
packet.setReadPos(countsPos);
|
||
if (skipClassicSpellCastTargets(packet, &ignoredTargetGuid)) {
|
||
traceFailure("pre_targets_fallback", countsPos, ignoredTargetGuid != 0 ? 1u : 0u);
|
||
return parseHitAndMissLists(false);
|
||
}
|
||
}
|
||
|
||
static uint32_t badMissCountTrunc = 0;
|
||
++badMissCountTrunc;
|
||
if (badMissCountTrunc <= 10 || (badMissCountTrunc % 100) == 0) {
|
||
LOG_WARNING("[Classic] Spell go: invalid missCount/remaining (misses=", (int)rawMissCount,
|
||
" remaining=", rem(), " occurrence=", badMissCountTrunc, ")");
|
||
}
|
||
traceFailure("invalid_miss_count", packet.getReadPos(), rawMissCount);
|
||
packet.setReadPos(startPos);
|
||
return false;
|
||
}
|
||
|
||
const uint8_t storedMissLimit = std::min<uint8_t>(rawMissCount, 128);
|
||
data.missTargets.reserve(storedMissLimit);
|
||
bool truncatedMissTargets = false;
|
||
const auto parseMissEntry = [&](SpellGoMissEntry& m, bool usePackedGuid) -> bool {
|
||
if (usePackedGuid) {
|
||
if (!hasFullPackedGuid(packet)) {
|
||
return false;
|
||
}
|
||
m.targetGuid = UpdateObjectParser::readPackedGuid(packet);
|
||
} else {
|
||
if (rem() < 8) {
|
||
return false;
|
||
}
|
||
m.targetGuid = packet.readUInt64();
|
||
}
|
||
return true;
|
||
};
|
||
for (uint16_t i = 0; i < rawMissCount; ++i) {
|
||
SpellGoMissEntry m;
|
||
const size_t missEntryPos = packet.getReadPos();
|
||
if (!parseMissEntry(m, false)) {
|
||
packet.setReadPos(missEntryPos);
|
||
if (!parseMissEntry(m, true)) {
|
||
LOG_WARNING("[Classic] Spell go: truncated miss targets at index ", i,
|
||
"/", (int)rawMissCount);
|
||
traceFailure("truncated_miss_target", packet.getReadPos(), i);
|
||
truncatedMissTargets = true;
|
||
break;
|
||
}
|
||
}
|
||
if (rem() < 1) {
|
||
LOG_WARNING("[Classic] Spell go: missing missType at miss index ", i,
|
||
"/", (int)rawMissCount);
|
||
traceFailure("missing_miss_type", packet.getReadPos(), i);
|
||
truncatedMissTargets = true;
|
||
break;
|
||
}
|
||
m.missType = packet.readUInt8();
|
||
if (m.missType == 11) {
|
||
if (rem() < 1) {
|
||
LOG_WARNING("[Classic] Spell go: truncated reflect payload at miss index ", i,
|
||
"/", (int)rawMissCount);
|
||
traceFailure("truncated_reflect", packet.getReadPos(), i);
|
||
truncatedMissTargets = true;
|
||
break;
|
||
}
|
||
(void)packet.readUInt8();
|
||
}
|
||
if (i < storedMissLimit) {
|
||
data.missTargets.push_back(m);
|
||
}
|
||
}
|
||
if (truncatedMissTargets) {
|
||
packet.setReadPos(startPos);
|
||
return false;
|
||
}
|
||
data.missCount = static_cast<uint8_t>(data.missTargets.size());
|
||
return true;
|
||
};
|
||
|
||
return parseMissListFrom(packet.getReadPos(), true);
|
||
};
|
||
|
||
if (!parseHitAndMissLists(true)) {
|
||
return false;
|
||
}
|
||
|
||
// SpellCastTargets follows the miss list — consume all target bytes so that
|
||
// any subsequent fields (e.g. castFlags extras) are not misaligned.
|
||
skipClassicSpellCastTargets(packet, &data.targetGuid);
|
||
|
||
LOG_DEBUG("[Classic] Spell go: spell=", data.spellId, " hits=", (int)data.hitCount,
|
||
" misses=", (int)data.missCount);
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic parseAttackerStateUpdate — Vanilla 1.12 SMSG_ATTACKERSTATEUPDATE
|
||
//
|
||
// Identical to TBC format except GUIDs are PackedGuid (not full uint64).
|
||
// Format: uint32(hitInfo) + PackedGuid(attacker) + PackedGuid(target)
|
||
// + int32(totalDamage) + uint8(subDamageCount)
|
||
// + [per sub: uint32(schoolMask) + float(damage) + uint32(intDamage)
|
||
// + uint32(absorbed) + uint32(resisted)]
|
||
// + uint32(victimState) + int32(overkill) [+ uint32(blocked)]
|
||
// ============================================================================
|
||
bool ClassicPacketParsers::parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) {
|
||
data = AttackerStateUpdateData{};
|
||
|
||
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||
if (rem() < 5) return false; // hitInfo(4) + at least GUID mask byte(1)
|
||
|
||
const size_t startPos = packet.getReadPos();
|
||
data.hitInfo = packet.readUInt32();
|
||
if (!hasFullPackedGuid(packet)) {
|
||
packet.setReadPos(startPos);
|
||
return false;
|
||
}
|
||
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
|
||
if (!hasFullPackedGuid(packet)) {
|
||
packet.setReadPos(startPos);
|
||
return false;
|
||
}
|
||
data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
|
||
|
||
if (rem() < 5) {
|
||
packet.setReadPos(startPos);
|
||
return false;
|
||
} // int32 totalDamage + uint8 subDamageCount
|
||
data.totalDamage = static_cast<int32_t>(packet.readUInt32());
|
||
data.subDamageCount = packet.readUInt8();
|
||
|
||
const uint8_t maxSubDamageCount = static_cast<uint8_t>(std::min<size_t>(rem() / 20, 64));
|
||
if (data.subDamageCount > maxSubDamageCount) {
|
||
data.subDamageCount = maxSubDamageCount;
|
||
}
|
||
|
||
data.subDamages.reserve(data.subDamageCount);
|
||
for (uint8_t i = 0; i < data.subDamageCount; ++i) {
|
||
if (rem() < 20) {
|
||
packet.setReadPos(startPos);
|
||
return false;
|
||
}
|
||
SubDamage sub;
|
||
sub.schoolMask = packet.readUInt32();
|
||
sub.damage = packet.readFloat();
|
||
sub.intDamage = packet.readUInt32();
|
||
sub.absorbed = packet.readUInt32();
|
||
sub.resisted = packet.readUInt32();
|
||
data.subDamages.push_back(sub);
|
||
}
|
||
data.subDamageCount = static_cast<uint8_t>(data.subDamages.size());
|
||
|
||
if (rem() < 8) {
|
||
packet.setReadPos(startPos);
|
||
return false;
|
||
}
|
||
data.victimState = packet.readUInt32();
|
||
data.overkill = static_cast<int32_t>(packet.readUInt32());
|
||
|
||
if (rem() >= 4) {
|
||
data.blocked = packet.readUInt32();
|
||
}
|
||
|
||
LOG_DEBUG("[Classic] Melee hit: ", data.totalDamage, " damage",
|
||
data.isCrit() ? " (CRIT)" : "",
|
||
data.isMiss() ? " (MISS)" : "");
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic parseSpellDamageLog — Vanilla 1.12 SMSG_SPELLNONMELEEDAMAGELOG
|
||
//
|
||
// Identical to TBC except GUIDs are PackedGuid (not full uint64).
|
||
// Format: PackedGuid(target) + PackedGuid(caster) + uint32(spellId)
|
||
// + uint32(damage) + uint8(schoolMask) + uint32(absorbed) + uint32(resisted)
|
||
// + uint8(periodicLog) + uint8(unused) + uint32(blocked) + uint32(flags)
|
||
// ============================================================================
|
||
bool ClassicPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageLogData& data) {
|
||
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||
if (rem() < 2 || !hasFullPackedGuid(packet)) return false;
|
||
|
||
data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
|
||
if (rem() < 1 || !hasFullPackedGuid(packet)) return false;
|
||
data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
|
||
|
||
// uint32(spellId) + uint32(damage) + uint8(schoolMask) + uint32(absorbed)
|
||
// + uint32(resisted) + uint8 + uint8 + uint32(blocked) + uint32(flags) = 21 bytes
|
||
if (rem() < 21) return false;
|
||
data.spellId = packet.readUInt32();
|
||
data.damage = packet.readUInt32();
|
||
data.schoolMask = packet.readUInt8();
|
||
data.absorbed = packet.readUInt32();
|
||
data.resisted = packet.readUInt32();
|
||
packet.readUInt8(); // periodicLog
|
||
packet.readUInt8(); // unused
|
||
packet.readUInt32(); // blocked
|
||
uint32_t flags = packet.readUInt32();
|
||
data.isCrit = (flags & 0x02) != 0;
|
||
data.overkill = 0; // no overkill field in Vanilla (same as TBC)
|
||
|
||
LOG_DEBUG("[Classic] Spell damage: spellId=", data.spellId, " dmg=", data.damage,
|
||
data.isCrit ? " CRIT" : "");
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic parseSpellHealLog — Vanilla 1.12 SMSG_SPELLHEALLOG
|
||
//
|
||
// Identical to TBC except GUIDs are PackedGuid (not full uint64).
|
||
// Format: PackedGuid(target) + PackedGuid(caster) + uint32(spellId)
|
||
// + uint32(heal) + uint32(overheal) + uint8(crit)
|
||
// ============================================================================
|
||
bool ClassicPacketParsers::parseSpellHealLog(network::Packet& packet, SpellHealLogData& data) {
|
||
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||
if (rem() < 2 || !hasFullPackedGuid(packet)) return false;
|
||
|
||
data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
|
||
if (rem() < 1 || !hasFullPackedGuid(packet)) return false;
|
||
data.casterGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla
|
||
|
||
if (rem() < 13) return false; // uint32 + uint32 + uint32 + uint8 = 13 bytes
|
||
data.spellId = packet.readUInt32();
|
||
data.heal = packet.readUInt32();
|
||
data.overheal = packet.readUInt32();
|
||
data.isCrit = (packet.readUInt8() != 0);
|
||
|
||
LOG_DEBUG("[Classic] Spell heal: spellId=", data.spellId, " heal=", data.heal,
|
||
data.isCrit ? " CRIT" : "");
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic parseAuraUpdate — Vanilla 1.12 SMSG_AURA_UPDATE / SMSG_AURA_UPDATE_ALL
|
||
//
|
||
// Classic has SMSG_AURA_UPDATE (TBC does not — TBC uses a different aura system
|
||
// and the TBC override returns false with a warning). Classic inherits TBC's
|
||
// override by default, so this override is needed to restore aura tracking.
|
||
//
|
||
// Classic aura flags differ from WotLK:
|
||
// 0x01/0x02/0x04 = effect indices active (same as WotLK)
|
||
// 0x08 = CANCELABLE / NOT-NEGATIVE (WotLK: 0x08 = NOT_CASTER)
|
||
// 0x10 = DURATION (WotLK: 0x20 = DURATION)
|
||
// 0x20 = NOT_CASTER (WotLK: no caster GUID at all if 0x08)
|
||
// 0x40 = POSITIVE (WotLK: 0x40 = EFFECT_AMOUNTS)
|
||
//
|
||
// Key differences from WotLK parser:
|
||
// - No caster GUID field in Classic SMSG_AURA_UPDATE packets
|
||
// - DURATION bit is 0x10, not 0x20
|
||
// - No effect amounts field (WotLK 0x40 = EFFECT_AMOUNTS does not exist here)
|
||
//
|
||
// Format: PackedGuid(entity) + [uint8(slot) + uint32(spellId)
|
||
// [+ uint8(flags) + uint8(level) + uint8(charges)
|
||
// + [uint32(maxDuration) + uint32(duration) if flags & 0x10]]*
|
||
// ============================================================================
|
||
bool ClassicPacketParsers::parseAuraUpdate(network::Packet& packet, AuraUpdateData& data, bool isAll) {
|
||
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||
if (rem() < 1) return false;
|
||
|
||
data.guid = UpdateObjectParser::readPackedGuid(packet);
|
||
|
||
while (rem() > 0) {
|
||
if (rem() < 1) break;
|
||
uint8_t slot = packet.readUInt8();
|
||
if (rem() < 4) break;
|
||
uint32_t spellId = packet.readUInt32();
|
||
|
||
AuraSlot aura;
|
||
if (spellId != 0) {
|
||
aura.spellId = spellId;
|
||
if (rem() < 3) { data.updates.push_back({slot, aura}); break; }
|
||
aura.flags = packet.readUInt8();
|
||
aura.level = packet.readUInt8();
|
||
aura.charges = packet.readUInt8();
|
||
|
||
// Classic DURATION flag is 0x10 (WotLK uses 0x20)
|
||
if ((aura.flags & 0x10) && rem() >= 8) {
|
||
aura.maxDurationMs = static_cast<int32_t>(packet.readUInt32());
|
||
aura.durationMs = static_cast<int32_t>(packet.readUInt32());
|
||
}
|
||
// No caster GUID field in Classic (WotLK added it gated by 0x08 NOT_CASTER)
|
||
// No effect amounts field in Classic (WotLK added it gated by 0x40)
|
||
}
|
||
|
||
data.updates.push_back({slot, aura});
|
||
if (!isAll) break;
|
||
}
|
||
|
||
LOG_DEBUG("[Classic] Aura update for 0x", std::hex, data.guid, std::dec,
|
||
": ", data.updates.size(), " slots");
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic SMSG_NAME_QUERY_RESPONSE format (1.12 / vmangos):
|
||
// uint64 guid (full, GetObjectGuid)
|
||
// CString name
|
||
// CString realmName (usually empty = single \0 byte)
|
||
// uint32 race
|
||
// uint32 gender
|
||
// uint32 class
|
||
//
|
||
// TBC Variant A (inherited from TbcPacketParsers) skips the realmName CString,
|
||
// causing it to misread the uint32 race field (absorbs the realmName \0 byte
|
||
// as the low byte), producing race=0 and shifted gender/class values.
|
||
// ============================================================================
|
||
bool ClassicPacketParsers::parseNameQueryResponse(network::Packet& packet, NameQueryResponseData& data) {
|
||
data = NameQueryResponseData{};
|
||
|
||
auto rem = [&]() { return packet.getSize() - packet.getReadPos(); };
|
||
if (rem() < 8) return false;
|
||
|
||
data.guid = packet.readUInt64(); // full uint64, not PackedGuid
|
||
data.name = packet.readString(); // null-terminated name
|
||
if (rem() == 0) return !data.name.empty();
|
||
|
||
data.realmName = packet.readString(); // null-terminated realm name (usually "")
|
||
if (rem() < 12) return !data.name.empty();
|
||
|
||
uint32_t race = packet.readUInt32();
|
||
uint32_t gender = packet.readUInt32();
|
||
uint32_t cls = packet.readUInt32();
|
||
data.race = static_cast<uint8_t>(race & 0xFF);
|
||
data.gender = static_cast<uint8_t>(gender & 0xFF);
|
||
data.classId = static_cast<uint8_t>(cls & 0xFF);
|
||
data.found = 0;
|
||
|
||
LOG_DEBUG("[Classic] Name query response: ", data.name,
|
||
" (race=", (int)data.race, " gender=", (int)data.gender,
|
||
" class=", (int)data.classId, ")");
|
||
return !data.name.empty();
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic SMSG_CAST_FAILED: no castCount byte (added in TBC/WotLK)
|
||
// Format: spellId(u32) + result(u8)
|
||
// ============================================================================
|
||
bool ClassicPacketParsers::parseCastFailed(network::Packet& packet, CastFailedData& data) {
|
||
data.castCount = 0;
|
||
data.spellId = packet.readUInt32();
|
||
uint8_t vanillaResult = packet.readUInt8();
|
||
// Vanilla enum starts at 0=AFFECTING_COMBAT (no SUCCESS entry).
|
||
// WotLK enum starts at 0=SUCCESS, 1=AFFECTING_COMBAT.
|
||
// Shift +1 to align with WotLK result strings.
|
||
data.result = vanillaResult + 1;
|
||
LOG_DEBUG("[Classic] Cast failed: spell=", data.spellId, " vanillaResult=", (int)vanillaResult);
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic SMSG_CAST_RESULT: same layout as parseCastFailed (spellId + result),
|
||
// but the result enum starts at 0=AFFECTING_COMBAT (no SUCCESS entry).
|
||
// Apply the same +1 shift used in parseCastFailed so the result codes
|
||
// align with WotLK's getSpellCastResultString table.
|
||
// ============================================================================
|
||
bool ClassicPacketParsers::parseCastResult(network::Packet& packet, uint32_t& spellId, uint8_t& result) {
|
||
if (packet.getSize() - packet.getReadPos() < 5) return false;
|
||
spellId = packet.readUInt32();
|
||
uint8_t vanillaResult = packet.readUInt8();
|
||
// Shift +1: Vanilla result 0=AFFECTING_COMBAT maps to WotLK result 1=AFFECTING_COMBAT
|
||
result = vanillaResult + 1;
|
||
LOG_DEBUG("[Classic] Cast result: spell=", spellId, " vanillaResult=", (int)vanillaResult);
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic 1.12.1 parseCharEnum
|
||
// Differences from TBC:
|
||
// - Equipment: 20 items, but NO enchantment field per slot
|
||
// Classic: displayId(u32) + inventoryType(u8) = 5 bytes/slot
|
||
// TBC/WotLK: displayId(u32) + inventoryType(u8) + enchant(u32) = 9 bytes/slot
|
||
// - After flags: uint8 firstLogin (same as TBC)
|
||
// ============================================================================
|
||
bool ClassicPacketParsers::parseCharEnum(network::Packet& packet, CharEnumResponse& response) {
|
||
// Validate minimum packet size for count byte
|
||
if (packet.getSize() < 1) {
|
||
LOG_ERROR("[Classic] SMSG_CHAR_ENUM packet too small: ", packet.getSize(), " bytes");
|
||
return false;
|
||
}
|
||
|
||
uint8_t count = packet.readUInt8();
|
||
|
||
// Cap count to prevent excessive memory allocation
|
||
constexpr uint8_t kMaxCharacters = 32;
|
||
if (count > kMaxCharacters) {
|
||
LOG_WARNING("[Classic] Character count ", (int)count, " exceeds max ", (int)kMaxCharacters,
|
||
", capping");
|
||
count = kMaxCharacters;
|
||
}
|
||
|
||
LOG_INFO("[Classic] Parsing SMSG_CHAR_ENUM: ", (int)count, " characters");
|
||
|
||
response.characters.clear();
|
||
response.characters.reserve(count);
|
||
|
||
for (uint8_t i = 0; i < count; ++i) {
|
||
// Sanity check: ensure we have at least minimal data before reading next character
|
||
// Minimum: guid(8) + name(1) + race(1) + class(1) + gender(1) + appearance(4)
|
||
// + facialFeatures(1) + level(1) + zone(4) + map(4) + pos(12) + guild(4)
|
||
// + flags(4) + firstLogin(1) + pet(12) + equipment(20*5)
|
||
constexpr size_t kMinCharacterSize = 8 + 1 + 1 + 1 + 1 + 4 + 1 + 1 + 4 + 4 + 12 + 4 + 4 + 1 + 12 + 100;
|
||
if (packet.getReadPos() + kMinCharacterSize > packet.getSize()) {
|
||
LOG_WARNING("[Classic] Character enum packet truncated at character ", (int)(i + 1),
|
||
", pos=", packet.getReadPos(), " needed=", kMinCharacterSize,
|
||
" size=", packet.getSize());
|
||
break;
|
||
}
|
||
|
||
Character character;
|
||
|
||
// GUID (8 bytes)
|
||
character.guid = packet.readUInt64();
|
||
|
||
// Name (null-terminated string)
|
||
character.name = packet.readString();
|
||
|
||
// Race, class, gender
|
||
character.race = static_cast<Race>(packet.readUInt8());
|
||
character.characterClass = static_cast<Class>(packet.readUInt8());
|
||
character.gender = static_cast<Gender>(packet.readUInt8());
|
||
|
||
// Appearance (5 bytes: skin, face, hairStyle, hairColor packed + facialFeatures)
|
||
character.appearanceBytes = packet.readUInt32();
|
||
character.facialFeatures = packet.readUInt8();
|
||
|
||
// Level
|
||
character.level = packet.readUInt8();
|
||
|
||
// Location
|
||
character.zoneId = packet.readUInt32();
|
||
character.mapId = packet.readUInt32();
|
||
character.x = packet.readFloat();
|
||
character.y = packet.readFloat();
|
||
character.z = packet.readFloat();
|
||
|
||
// Guild ID
|
||
character.guildId = packet.readUInt32();
|
||
|
||
// Flags
|
||
character.flags = packet.readUInt32();
|
||
|
||
// Classic: uint8 firstLogin (same as TBC)
|
||
/*uint8_t firstLogin =*/ packet.readUInt8();
|
||
|
||
// Pet data (always present)
|
||
character.pet.displayModel = packet.readUInt32();
|
||
character.pet.level = packet.readUInt32();
|
||
character.pet.family = packet.readUInt32();
|
||
|
||
// Equipment (Classic: 20 items, NO enchantment field)
|
||
character.equipment.reserve(20);
|
||
for (int j = 0; j < 20; ++j) {
|
||
EquipmentItem item;
|
||
item.displayModel = packet.readUInt32();
|
||
item.inventoryType = packet.readUInt8();
|
||
item.enchantment = 0; // Classic has no enchant field in char enum
|
||
character.equipment.push_back(item);
|
||
}
|
||
|
||
LOG_DEBUG(" Character ", (int)(i + 1), ": ", character.name,
|
||
" (", getRaceName(character.race), " ", getClassName(character.characterClass),
|
||
" level ", (int)character.level, " zone ", character.zoneId, ")");
|
||
|
||
response.characters.push_back(character);
|
||
}
|
||
|
||
LOG_INFO("[Classic] Parsed ", response.characters.size(), " characters");
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic 1.12.1 parseMessageChat
|
||
// Differences from WotLK:
|
||
// - NO uint32 unknown field after senderGuid
|
||
// - CHANNEL type: channelName + rank(u32) + senderGuid (not just channelName)
|
||
// - No ACHIEVEMENT/GUILD_ACHIEVEMENT types
|
||
// ============================================================================
|
||
bool ClassicPacketParsers::parseMessageChat(network::Packet& packet, MessageChatData& data) {
|
||
if (packet.getSize() < 10) {
|
||
LOG_ERROR("[Classic] SMSG_MESSAGECHAT packet too small: ", packet.getSize(), " bytes");
|
||
return false;
|
||
}
|
||
|
||
// Read chat type
|
||
uint8_t typeVal = packet.readUInt8();
|
||
data.type = static_cast<ChatType>(typeVal);
|
||
|
||
// Read language
|
||
uint32_t langVal = packet.readUInt32();
|
||
data.language = static_cast<ChatLanguage>(langVal);
|
||
|
||
// Classic: NO uint32 unknown field here (WotLK has one)
|
||
|
||
// Type-specific data (matches CMaNGOS-Classic BuildChatPacket)
|
||
switch (data.type) {
|
||
case ChatType::MONSTER_EMOTE: {
|
||
// nameLen(u32) + name + targetGuid(u64)
|
||
uint32_t nameLen = packet.readUInt32();
|
||
if (nameLen > 0 && nameLen < 256) {
|
||
data.senderName.resize(nameLen);
|
||
for (uint32_t i = 0; i < nameLen; ++i) {
|
||
data.senderName[i] = static_cast<char>(packet.readUInt8());
|
||
}
|
||
if (!data.senderName.empty() && data.senderName.back() == '\0') {
|
||
data.senderName.pop_back();
|
||
}
|
||
}
|
||
data.receiverGuid = packet.readUInt64();
|
||
break;
|
||
}
|
||
|
||
case ChatType::SAY:
|
||
case ChatType::PARTY:
|
||
case ChatType::YELL:
|
||
case ChatType::WHISPER:
|
||
case ChatType::WHISPER_INFORM:
|
||
case ChatType::GUILD:
|
||
case ChatType::OFFICER:
|
||
case ChatType::RAID:
|
||
case ChatType::RAID_LEADER:
|
||
case ChatType::RAID_WARNING:
|
||
case ChatType::EMOTE:
|
||
case ChatType::TEXT_EMOTE: {
|
||
// senderGuid(u64) + senderGuid(u64) — written twice by server
|
||
data.senderGuid = packet.readUInt64();
|
||
/*duplicateGuid*/ packet.readUInt64();
|
||
break;
|
||
}
|
||
|
||
case ChatType::MONSTER_SAY:
|
||
case ChatType::MONSTER_YELL: {
|
||
// senderGuid(u64) + nameLen(u32) + name + targetGuid(u64)
|
||
data.senderGuid = packet.readUInt64();
|
||
uint32_t nameLen = packet.readUInt32();
|
||
if (nameLen > 0 && nameLen < 256) {
|
||
data.senderName.resize(nameLen);
|
||
for (uint32_t i = 0; i < nameLen; ++i) {
|
||
data.senderName[i] = static_cast<char>(packet.readUInt8());
|
||
}
|
||
if (!data.senderName.empty() && data.senderName.back() == '\0') {
|
||
data.senderName.pop_back();
|
||
}
|
||
}
|
||
data.receiverGuid = packet.readUInt64();
|
||
break;
|
||
}
|
||
|
||
case ChatType::CHANNEL: {
|
||
// channelName(string) + rank(u32) + senderGuid(u64)
|
||
data.channelName = packet.readString();
|
||
/*uint32_t rank =*/ packet.readUInt32();
|
||
data.senderGuid = packet.readUInt64();
|
||
break;
|
||
}
|
||
|
||
default: {
|
||
// All other types: senderGuid(u64) + senderGuid(u64) — written twice
|
||
data.senderGuid = packet.readUInt64();
|
||
/*duplicateGuid*/ packet.readUInt64();
|
||
break;
|
||
}
|
||
}
|
||
|
||
// Read message length
|
||
uint32_t messageLen = packet.readUInt32();
|
||
|
||
// Read message
|
||
if (messageLen > 0 && messageLen < 8192) {
|
||
data.message.resize(messageLen);
|
||
for (uint32_t i = 0; i < messageLen; ++i) {
|
||
data.message[i] = static_cast<char>(packet.readUInt8());
|
||
}
|
||
// Remove null terminator if present
|
||
if (!data.message.empty() && data.message.back() == '\0') {
|
||
data.message.pop_back();
|
||
}
|
||
}
|
||
|
||
// Read chat tag
|
||
if (packet.getReadPos() < packet.getSize()) {
|
||
data.chatTag = packet.readUInt8();
|
||
}
|
||
|
||
LOG_DEBUG("[Classic] SMSG_MESSAGECHAT: type=", getChatTypeString(data.type),
|
||
" sender=", data.senderName.empty() ? std::to_string(data.senderGuid) : data.senderName);
|
||
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic CMSG_JOIN_CHANNEL / CMSG_LEAVE_CHANNEL
|
||
// Classic format: just string channelName + string password (no channelId/hasVoice/joinedByZone)
|
||
// ============================================================================
|
||
|
||
network::Packet ClassicPacketParsers::buildJoinChannel(const std::string& channelName, const std::string& password) {
|
||
network::Packet packet(wireOpcode(Opcode::CMSG_JOIN_CHANNEL));
|
||
packet.writeString(channelName);
|
||
packet.writeString(password);
|
||
LOG_DEBUG("[Classic] Built CMSG_JOIN_CHANNEL: channel=", channelName);
|
||
return packet;
|
||
}
|
||
|
||
network::Packet ClassicPacketParsers::buildLeaveChannel(const std::string& channelName) {
|
||
network::Packet packet(wireOpcode(Opcode::CMSG_LEAVE_CHANNEL));
|
||
packet.writeString(channelName);
|
||
LOG_DEBUG("[Classic] Built CMSG_LEAVE_CHANNEL: channel=", channelName);
|
||
return packet;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic guild roster parser
|
||
// Differences from WotLK:
|
||
// - No rankCount field (fixed 10 ranks, read rights only)
|
||
// - No per-rank bank tab data
|
||
// - No gender byte per member
|
||
// ============================================================================
|
||
|
||
bool ClassicPacketParsers::parseGuildRoster(network::Packet& packet, GuildRosterData& data) {
|
||
if (packet.getSize() < 4) {
|
||
LOG_ERROR("Classic SMSG_GUILD_ROSTER too small: ", packet.getSize());
|
||
return false;
|
||
}
|
||
uint32_t numMembers = packet.readUInt32();
|
||
data.motd = packet.readString();
|
||
data.guildInfo = packet.readString();
|
||
|
||
// Classic: fixed 10 ranks, just uint32 rights each (no goldLimit, no bank tabs)
|
||
data.ranks.resize(10);
|
||
for (int i = 0; i < 10; ++i) {
|
||
data.ranks[i].rights = packet.readUInt32();
|
||
data.ranks[i].goldLimit = 0;
|
||
}
|
||
|
||
data.members.resize(numMembers);
|
||
for (uint32_t i = 0; i < numMembers; ++i) {
|
||
auto& m = data.members[i];
|
||
m.guid = packet.readUInt64();
|
||
m.online = (packet.readUInt8() != 0);
|
||
m.name = packet.readString();
|
||
m.rankIndex = packet.readUInt32();
|
||
m.level = packet.readUInt8();
|
||
m.classId = packet.readUInt8();
|
||
// Classic: NO gender byte
|
||
m.gender = 0;
|
||
m.zoneId = packet.readUInt32();
|
||
if (!m.online) {
|
||
m.lastOnline = packet.readFloat();
|
||
}
|
||
m.publicNote = packet.readString();
|
||
m.officerNote = packet.readString();
|
||
}
|
||
LOG_INFO("Parsed Classic SMSG_GUILD_ROSTER: ", numMembers, " members");
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic guild query response parser
|
||
// Differences from WotLK:
|
||
// - No trailing rankCount uint32
|
||
// ============================================================================
|
||
|
||
bool ClassicPacketParsers::parseGuildQueryResponse(network::Packet& packet, GuildQueryResponseData& data) {
|
||
if (packet.getSize() < 8) {
|
||
LOG_ERROR("Classic SMSG_GUILD_QUERY_RESPONSE too small: ", packet.getSize());
|
||
return false;
|
||
}
|
||
data.guildId = packet.readUInt32();
|
||
data.guildName = packet.readString();
|
||
for (int i = 0; i < 10; ++i) {
|
||
data.rankNames[i] = packet.readString();
|
||
}
|
||
data.emblemStyle = packet.readUInt32();
|
||
data.emblemColor = packet.readUInt32();
|
||
data.borderStyle = packet.readUInt32();
|
||
data.borderColor = packet.readUInt32();
|
||
data.backgroundColor = packet.readUInt32();
|
||
// Classic: NO trailing rankCount
|
||
data.rankCount = 10;
|
||
LOG_INFO("Parsed Classic SMSG_GUILD_QUERY_RESPONSE: guild=", data.guildName);
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// GameObject Query — Classic has no extra strings before data[]
|
||
// WotLK has iconName + castBarCaption + unk1 between names and data[].
|
||
// Vanilla: entry, type, displayId, name[4], data[24]
|
||
// ============================================================================
|
||
|
||
bool ClassicPacketParsers::parseGameObjectQueryResponse(network::Packet& packet, GameObjectQueryResponseData& data) {
|
||
// Validate minimum packet size: entry(4)
|
||
if (packet.getSize() < 4) {
|
||
LOG_ERROR("Classic SMSG_GAMEOBJECT_QUERY_RESPONSE: packet too small (", packet.getSize(), " bytes)");
|
||
return false;
|
||
}
|
||
|
||
data.entry = packet.readUInt32();
|
||
|
||
// High bit set means gameobject not found
|
||
if (data.entry & 0x80000000) {
|
||
data.entry &= ~0x80000000;
|
||
data.name = "";
|
||
return true;
|
||
}
|
||
|
||
// Validate minimum size for fixed fields: type(4) + displayId(4)
|
||
if (packet.getSize() - packet.getReadPos() < 8) {
|
||
LOG_ERROR("Classic SMSG_GAMEOBJECT_QUERY_RESPONSE: truncated before names (entry=", data.entry, ")");
|
||
return false;
|
||
}
|
||
|
||
data.type = packet.readUInt32();
|
||
data.displayId = packet.readUInt32();
|
||
// 4 name strings
|
||
data.name = packet.readString();
|
||
packet.readString();
|
||
packet.readString();
|
||
packet.readString();
|
||
|
||
// Classic: data[24] comes immediately after names (no extra strings)
|
||
size_t remaining = packet.getSize() - packet.getReadPos();
|
||
if (remaining >= 24 * 4) {
|
||
for (int i = 0; i < 24; i++) {
|
||
data.data[i] = packet.readUInt32();
|
||
}
|
||
data.hasData = true;
|
||
} else if (remaining > 0) {
|
||
// Partial data field; read what we can
|
||
uint32_t fieldsToRead = remaining / 4;
|
||
for (uint32_t i = 0; i < fieldsToRead && i < 24; i++) {
|
||
data.data[i] = packet.readUInt32();
|
||
}
|
||
if (fieldsToRead < 24) {
|
||
LOG_WARNING("Classic SMSG_GAMEOBJECT_QUERY_RESPONSE: truncated in data fields (", fieldsToRead,
|
||
" of 24 read, entry=", data.entry, ")");
|
||
}
|
||
}
|
||
|
||
if (data.type == 15) { // MO_TRANSPORT
|
||
LOG_DEBUG("Classic GO query: MO_TRANSPORT entry=", data.entry,
|
||
" name=\"", data.name, "\" displayId=", data.displayId,
|
||
" taxiPathId=", data.data[0], " moveSpeed=", data.data[1]);
|
||
} else {
|
||
LOG_DEBUG("Classic GO query: ", data.name, " type=", data.type, " entry=", data.entry);
|
||
}
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Gossip — Classic has no menuId, and quest items lack questFlags + isRepeatable
|
||
// ============================================================================
|
||
|
||
bool ClassicPacketParsers::parseGossipMessage(network::Packet& packet, GossipMessageData& data) {
|
||
size_t remaining = packet.getSize() - packet.getReadPos();
|
||
if (remaining < 8 + 4 + 4) {
|
||
LOG_ERROR("Classic SMSG_GOSSIP_MESSAGE too small: ", remaining, " bytes");
|
||
return false;
|
||
}
|
||
|
||
data.npcGuid = packet.readUInt64();
|
||
// Classic: NO menuId field (WotLK adds uint32 menuId here)
|
||
data.menuId = 0;
|
||
data.titleTextId = packet.readUInt32();
|
||
uint32_t optionCount = packet.readUInt32();
|
||
|
||
// Cap option count to reasonable maximum
|
||
constexpr uint32_t kMaxGossipOptions = 256;
|
||
if (optionCount > kMaxGossipOptions) {
|
||
LOG_WARNING("Classic SMSG_GOSSIP_MESSAGE optionCount=", optionCount, " exceeds max ",
|
||
kMaxGossipOptions, ", capping");
|
||
optionCount = kMaxGossipOptions;
|
||
}
|
||
|
||
data.options.clear();
|
||
data.options.reserve(optionCount);
|
||
for (uint32_t i = 0; i < optionCount; ++i) {
|
||
// Sanity check: ensure minimum bytes available for option (id(4)+icon(1)+isCoded(1)+text(1))
|
||
remaining = packet.getSize() - packet.getReadPos();
|
||
if (remaining < 7) {
|
||
LOG_WARNING("Classic gossip option ", i, " truncated (", remaining, " bytes left)");
|
||
break;
|
||
}
|
||
|
||
GossipOption opt;
|
||
opt.id = packet.readUInt32();
|
||
opt.icon = packet.readUInt8();
|
||
opt.isCoded = (packet.readUInt8() != 0);
|
||
// Classic/Vanilla: NO boxMoney or boxText fields (commented out in mangoszero)
|
||
opt.boxMoney = 0;
|
||
opt.text = packet.readString();
|
||
opt.boxText = "";
|
||
data.options.push_back(opt);
|
||
}
|
||
|
||
// Ensure we have at least 4 bytes for questCount
|
||
remaining = packet.getSize() - packet.getReadPos();
|
||
if (remaining < 4) {
|
||
LOG_WARNING("Classic SMSG_GOSSIP_MESSAGE truncated before questCount");
|
||
return data.options.size() > 0; // Return true if we got at least some options
|
||
}
|
||
|
||
uint32_t questCount = packet.readUInt32();
|
||
|
||
// Cap quest count to reasonable maximum
|
||
constexpr uint32_t kMaxGossipQuests = 256;
|
||
if (questCount > kMaxGossipQuests) {
|
||
LOG_WARNING("Classic SMSG_GOSSIP_MESSAGE questCount=", questCount, " exceeds max ",
|
||
kMaxGossipQuests, ", capping");
|
||
questCount = kMaxGossipQuests;
|
||
}
|
||
|
||
data.quests.clear();
|
||
data.quests.reserve(questCount);
|
||
for (uint32_t i = 0; i < questCount; ++i) {
|
||
// Sanity check: ensure minimum bytes available for quest (id(4)+icon(4)+level(4)+title(1))
|
||
remaining = packet.getSize() - packet.getReadPos();
|
||
if (remaining < 13) {
|
||
LOG_WARNING("Classic gossip quest ", i, " truncated (", remaining, " bytes left)");
|
||
break;
|
||
}
|
||
|
||
GossipQuestItem quest;
|
||
quest.questId = packet.readUInt32();
|
||
quest.questIcon = packet.readUInt32();
|
||
quest.questLevel = static_cast<int32_t>(packet.readUInt32());
|
||
// Classic: NO questFlags, NO isRepeatable
|
||
quest.questFlags = 0;
|
||
quest.isRepeatable = 0;
|
||
quest.title = normalizeWowTextTokens(packet.readString());
|
||
data.quests.push_back(quest);
|
||
}
|
||
|
||
LOG_DEBUG("Classic Gossip: ", optionCount, " options, ", questCount, " quests");
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic CMSG_SEND_MAIL — Vanilla 1.12 format
|
||
// Differences from WotLK:
|
||
// - Single uint64 itemGuid instead of uint8 attachmentCount + item array
|
||
// - Trailing uint64 unk3 + uint8 unk4 (clients > 1.9.4)
|
||
// ============================================================================
|
||
network::Packet ClassicPacketParsers::buildSendMail(uint64_t mailboxGuid,
|
||
const std::string& recipient,
|
||
const std::string& subject,
|
||
const std::string& body,
|
||
uint32_t money, uint32_t cod,
|
||
const std::vector<uint64_t>& itemGuids) {
|
||
network::Packet packet(wireOpcode(Opcode::CMSG_SEND_MAIL));
|
||
packet.writeUInt64(mailboxGuid);
|
||
packet.writeString(recipient);
|
||
packet.writeString(subject);
|
||
packet.writeString(body);
|
||
packet.writeUInt32(0); // stationery
|
||
packet.writeUInt32(0); // unknown
|
||
// Vanilla supports only one item attachment (single uint64 GUID)
|
||
uint64_t singleItemGuid = itemGuids.empty() ? 0 : itemGuids[0];
|
||
packet.writeUInt64(singleItemGuid);
|
||
packet.writeUInt32(money);
|
||
packet.writeUInt32(cod);
|
||
packet.writeUInt64(0); // unk3 (clients > 1.9.4)
|
||
packet.writeUInt8(0); // unk4 (clients > 1.9.4)
|
||
return packet;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic SMSG_MAIL_LIST_RESULT — Vanilla 1.12 format (per vmangos)
|
||
// Key differences from WotLK:
|
||
// - uint8 count (not uint32 totalCount + uint8 shownCount)
|
||
// - No msgSize prefix per entry
|
||
// - Subject comes before item data
|
||
// - Single inline item (not attachment count + array)
|
||
// - uint8 stackCount (not uint32)
|
||
// - No enchantment array (single permanentEnchant uint32)
|
||
// ============================================================================
|
||
bool ClassicPacketParsers::parseMailList(network::Packet& packet,
|
||
std::vector<MailMessage>& inbox) {
|
||
size_t remaining = packet.getSize() - packet.getReadPos();
|
||
if (remaining < 1) return false;
|
||
|
||
uint8_t count = packet.readUInt8();
|
||
LOG_INFO("SMSG_MAIL_LIST_RESULT (Classic): count=", (int)count);
|
||
|
||
inbox.clear();
|
||
inbox.reserve(count);
|
||
|
||
for (uint8_t i = 0; i < count; ++i) {
|
||
remaining = packet.getSize() - packet.getReadPos();
|
||
if (remaining < 5) {
|
||
LOG_WARNING("Classic mail entry ", i, " truncated (", remaining, " bytes left)");
|
||
break;
|
||
}
|
||
|
||
MailMessage msg;
|
||
|
||
// vmangos HandleGetMailList format:
|
||
// u32 messageId, u8 messageType, sender (guid or u32),
|
||
// string subject, u32 itemTextId, u32 package, u32 stationery,
|
||
// item fields (entry, enchant, randomProp, suffixFactor,
|
||
// u8 stackCount, u32 charges, u32 maxDur, u32 dur),
|
||
// u32 money, u32 cod, u32 flags, float expirationTime,
|
||
// u32 mailTemplateId (build-dependent)
|
||
msg.messageId = packet.readUInt32();
|
||
msg.messageType = packet.readUInt8();
|
||
|
||
switch (msg.messageType) {
|
||
case 0: msg.senderGuid = packet.readUInt64(); break;
|
||
default: msg.senderEntry = packet.readUInt32(); break;
|
||
}
|
||
|
||
msg.subject = packet.readString();
|
||
|
||
uint32_t itemTextId = packet.readUInt32();
|
||
(void)itemTextId;
|
||
packet.readUInt32(); // package (unused)
|
||
msg.stationeryId = packet.readUInt32();
|
||
|
||
// Single inline item (Vanilla: one item per mail)
|
||
uint32_t itemEntry = packet.readUInt32();
|
||
uint32_t permanentEnchant = packet.readUInt32();
|
||
uint32_t randomPropertyId = packet.readUInt32();
|
||
uint32_t suffixFactor = packet.readUInt32();
|
||
uint8_t stackCount = packet.readUInt8();
|
||
packet.readUInt32(); // charges
|
||
uint32_t maxDurability = packet.readUInt32();
|
||
uint32_t durability = packet.readUInt32();
|
||
|
||
if (itemEntry != 0) {
|
||
MailAttachment att;
|
||
att.slot = 0;
|
||
att.itemGuidLow = 0; // Not provided in Vanilla list
|
||
att.itemId = itemEntry;
|
||
att.enchantId = permanentEnchant;
|
||
att.randomPropertyId = randomPropertyId;
|
||
att.randomSuffix = suffixFactor;
|
||
att.stackCount = stackCount;
|
||
att.chargesOrDurability = durability;
|
||
att.maxDurability = maxDurability;
|
||
msg.attachments.push_back(att);
|
||
}
|
||
|
||
msg.money = packet.readUInt32();
|
||
msg.cod = packet.readUInt32();
|
||
msg.flags = packet.readUInt32();
|
||
msg.expirationTime = packet.readFloat();
|
||
msg.mailTemplateId = packet.readUInt32();
|
||
|
||
msg.read = (msg.flags & 0x01) != 0;
|
||
inbox.push_back(std::move(msg));
|
||
}
|
||
|
||
LOG_INFO("Parsed ", inbox.size(), " mail messages");
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic CMSG_MAIL_TAKE_ITEM — Vanilla only sends mailboxGuid + mailId
|
||
// (no itemSlot — Vanilla only supports 1 item per mail)
|
||
// ============================================================================
|
||
network::Packet ClassicPacketParsers::buildMailTakeItem(uint64_t mailboxGuid,
|
||
uint32_t mailId,
|
||
uint32_t /*itemSlot*/) {
|
||
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_TAKE_ITEM));
|
||
packet.writeUInt64(mailboxGuid);
|
||
packet.writeUInt32(mailId);
|
||
return packet;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic CMSG_MAIL_DELETE — Vanilla only sends mailboxGuid + mailId
|
||
// (no mailTemplateId field)
|
||
// ============================================================================
|
||
network::Packet ClassicPacketParsers::buildMailDelete(uint64_t mailboxGuid,
|
||
uint32_t mailId,
|
||
uint32_t /*mailTemplateId*/) {
|
||
network::Packet packet(wireOpcode(Opcode::CMSG_MAIL_DELETE));
|
||
packet.writeUInt64(mailboxGuid);
|
||
packet.writeUInt32(mailId);
|
||
return packet;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE
|
||
// Vanilla has NO SoundOverrideSubclass, NO Flags2, NO ScalingStatDistribution,
|
||
// NO ScalingStatValue, and only 2 damage types (not 5).
|
||
// ============================================================================
|
||
network::Packet ClassicPacketParsers::buildItemQuery(uint32_t entry, uint64_t guid) {
|
||
// Vanilla CMSG_ITEM_QUERY_SINGLE: uint32 entry + uint64 guid (same as WotLK)
|
||
network::Packet packet(wireOpcode(Opcode::CMSG_ITEM_QUERY_SINGLE));
|
||
packet.writeUInt32(entry);
|
||
packet.writeUInt64(guid);
|
||
LOG_DEBUG("[Classic] Built CMSG_ITEM_QUERY_SINGLE: entry=", entry, " guid=0x", std::hex, guid, std::dec);
|
||
return packet;
|
||
}
|
||
|
||
bool ClassicPacketParsers::parseItemQueryResponse(network::Packet& packet, ItemQueryResponseData& data) {
|
||
// Validate minimum packet size: entry(4)
|
||
if (packet.getSize() < 4) {
|
||
LOG_ERROR("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: packet too small (", packet.getSize(), " bytes)");
|
||
return false;
|
||
}
|
||
|
||
data.entry = packet.readUInt32();
|
||
|
||
// High bit set means item not found
|
||
if (data.entry & 0x80000000) {
|
||
data.entry &= ~0x80000000;
|
||
return true;
|
||
}
|
||
|
||
// Validate minimum size for fixed fields: itemClass(4) + subClass(4) + 4 name strings + displayInfoId(4) + quality(4)
|
||
if (packet.getSize() - packet.getReadPos() < 8) {
|
||
LOG_ERROR("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before names (entry=", data.entry, ")");
|
||
return false;
|
||
}
|
||
|
||
uint32_t itemClass = packet.readUInt32();
|
||
uint32_t subClass = packet.readUInt32();
|
||
// Vanilla: NO SoundOverrideSubclass
|
||
|
||
data.itemClass = itemClass;
|
||
data.subClass = subClass;
|
||
data.subclassName = "";
|
||
if (itemClass == 2) { // Weapon
|
||
switch (subClass) {
|
||
case 0: data.subclassName = "Axe"; break;
|
||
case 1: data.subclassName = "Axe"; break;
|
||
case 2: data.subclassName = "Bow"; break;
|
||
case 3: data.subclassName = "Gun"; break;
|
||
case 4: data.subclassName = "Mace"; break;
|
||
case 5: data.subclassName = "Mace"; break;
|
||
case 6: data.subclassName = "Polearm"; break;
|
||
case 7: data.subclassName = "Sword"; break;
|
||
case 8: data.subclassName = "Sword"; break;
|
||
case 10: data.subclassName = "Staff"; break;
|
||
case 13: data.subclassName = "Fist Weapon"; break;
|
||
case 15: data.subclassName = "Dagger"; break;
|
||
case 16: data.subclassName = "Thrown"; break;
|
||
case 18: data.subclassName = "Crossbow"; break;
|
||
case 19: data.subclassName = "Wand"; break;
|
||
case 20: data.subclassName = "Fishing Pole"; break;
|
||
default: data.subclassName = "Weapon"; break;
|
||
}
|
||
} else if (itemClass == 4) { // Armor
|
||
switch (subClass) {
|
||
case 0: data.subclassName = "Miscellaneous"; break;
|
||
case 1: data.subclassName = "Cloth"; break;
|
||
case 2: data.subclassName = "Leather"; break;
|
||
case 3: data.subclassName = "Mail"; break;
|
||
case 4: data.subclassName = "Plate"; break;
|
||
case 6: data.subclassName = "Shield"; break;
|
||
default: data.subclassName = "Armor"; break;
|
||
}
|
||
}
|
||
|
||
// 4 name strings
|
||
data.name = packet.readString();
|
||
packet.readString(); // name2
|
||
packet.readString(); // name3
|
||
packet.readString(); // name4
|
||
|
||
data.displayInfoId = packet.readUInt32();
|
||
data.quality = packet.readUInt32();
|
||
|
||
// Validate minimum size for fixed fields: Flags(4) + BuyPrice(4) + SellPrice(4) + inventoryType(4)
|
||
if (packet.getSize() - packet.getReadPos() < 16) {
|
||
LOG_ERROR("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before inventoryType (entry=", data.entry, ")");
|
||
return false;
|
||
}
|
||
|
||
data.itemFlags = packet.readUInt32(); // Flags
|
||
// Vanilla: NO Flags2
|
||
packet.readUInt32(); // BuyPrice
|
||
data.sellPrice = packet.readUInt32(); // SellPrice
|
||
|
||
data.inventoryType = packet.readUInt32();
|
||
|
||
// Validate minimum size for remaining fixed fields: 13×4 = 52 bytes
|
||
if (packet.getSize() - packet.getReadPos() < 52) {
|
||
LOG_ERROR("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before stats (entry=", data.entry, ")");
|
||
return false;
|
||
}
|
||
|
||
data.allowableClass = packet.readUInt32(); // AllowableClass
|
||
data.allowableRace = packet.readUInt32(); // AllowableRace
|
||
data.itemLevel = packet.readUInt32();
|
||
data.requiredLevel = packet.readUInt32();
|
||
data.requiredSkill = packet.readUInt32(); // RequiredSkill
|
||
data.requiredSkillRank = packet.readUInt32(); // RequiredSkillRank
|
||
packet.readUInt32(); // RequiredSpell
|
||
packet.readUInt32(); // RequiredHonorRank
|
||
packet.readUInt32(); // RequiredCityRank
|
||
data.requiredReputationFaction = packet.readUInt32(); // RequiredReputationFaction
|
||
data.requiredReputationRank = packet.readUInt32(); // RequiredReputationRank
|
||
data.maxCount = static_cast<int32_t>(packet.readUInt32()); // MaxCount (1 = Unique)
|
||
data.maxStack = static_cast<int32_t>(packet.readUInt32()); // Stackable
|
||
data.containerSlots = packet.readUInt32();
|
||
|
||
// Vanilla: 10 stat pairs, NO statsCount prefix (10×8 = 80 bytes)
|
||
if (packet.getSize() - packet.getReadPos() < 80) {
|
||
LOG_WARNING("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated in stats section (entry=", data.entry, ")");
|
||
// Read what we can
|
||
}
|
||
for (uint32_t i = 0; i < 10; i++) {
|
||
if (packet.getSize() - packet.getReadPos() < 8) {
|
||
LOG_WARNING("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: stat ", i, " truncated (entry=", data.entry, ")");
|
||
break;
|
||
}
|
||
uint32_t statType = packet.readUInt32();
|
||
int32_t statValue = static_cast<int32_t>(packet.readUInt32());
|
||
if (statType != 0) {
|
||
switch (statType) {
|
||
case 3: data.agility = statValue; break;
|
||
case 4: data.strength = statValue; break;
|
||
case 5: data.intellect = statValue; break;
|
||
case 6: data.spirit = statValue; break;
|
||
case 7: data.stamina = statValue; break;
|
||
default:
|
||
if (statValue != 0)
|
||
data.extraStats.push_back({statType, statValue});
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Vanilla: NO ScalingStatDistribution, NO ScalingStatValue
|
||
|
||
// Vanilla: 5 damage types (same count as WotLK)
|
||
bool haveWeaponDamage = false;
|
||
for (int i = 0; i < 5; i++) {
|
||
// Each damage entry is dmgMin(4) + dmgMax(4) + damageType(4) = 12 bytes
|
||
if (packet.getSize() - packet.getReadPos() < 12) {
|
||
LOG_WARNING("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: damage ", i, " truncated (entry=", data.entry, ")");
|
||
break;
|
||
}
|
||
float dmgMin = packet.readFloat();
|
||
float dmgMax = packet.readFloat();
|
||
uint32_t damageType = packet.readUInt32();
|
||
if (!haveWeaponDamage && dmgMax > 0.0f) {
|
||
// Prefer physical damage (type 0) when present.
|
||
if (damageType == 0 || data.damageMax <= 0.0f) {
|
||
data.damageMin = dmgMin;
|
||
data.damageMax = dmgMax;
|
||
haveWeaponDamage = (damageType == 0);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Validate minimum size for armor field (4 bytes)
|
||
if (packet.getSize() - packet.getReadPos() < 4) {
|
||
LOG_WARNING("Classic SMSG_ITEM_QUERY_SINGLE_RESPONSE: truncated before armor (entry=", data.entry, ")");
|
||
return true; // Have core fields; armor is important but optional
|
||
}
|
||
data.armor = static_cast<int32_t>(packet.readUInt32());
|
||
|
||
// Remaining tail can vary by core. Read resistances + delay when present.
|
||
if (packet.getSize() - packet.getReadPos() >= 28) {
|
||
data.holyRes = static_cast<int32_t>(packet.readUInt32()); // HolyRes
|
||
data.fireRes = static_cast<int32_t>(packet.readUInt32()); // FireRes
|
||
data.natureRes = static_cast<int32_t>(packet.readUInt32()); // NatureRes
|
||
data.frostRes = static_cast<int32_t>(packet.readUInt32()); // FrostRes
|
||
data.shadowRes = static_cast<int32_t>(packet.readUInt32()); // ShadowRes
|
||
data.arcaneRes = static_cast<int32_t>(packet.readUInt32()); // ArcaneRes
|
||
data.delayMs = packet.readUInt32();
|
||
}
|
||
|
||
// AmmoType + RangedModRange (2 fields, 8 bytes)
|
||
if (packet.getSize() - packet.getReadPos() >= 8) {
|
||
packet.readUInt32(); // AmmoType
|
||
packet.readFloat(); // RangedModRange
|
||
}
|
||
|
||
// 2 item spells in Vanilla (3 fields each: SpellId, Trigger, Charges)
|
||
// Actually vanilla has 5 spells: SpellId, Trigger, Charges, Cooldown, Category, CatCooldown = 24 bytes each
|
||
for (int i = 0; i < 5; i++) {
|
||
if (packet.getReadPos() + 24 > packet.getSize()) break;
|
||
data.spells[i].spellId = packet.readUInt32();
|
||
data.spells[i].spellTrigger = packet.readUInt32();
|
||
packet.readUInt32(); // SpellCharges
|
||
packet.readUInt32(); // SpellCooldown
|
||
packet.readUInt32(); // SpellCategory
|
||
packet.readUInt32(); // SpellCategoryCooldown
|
||
}
|
||
|
||
// Bonding type
|
||
if (packet.getReadPos() + 4 <= packet.getSize())
|
||
data.bindType = packet.readUInt32();
|
||
|
||
// Description (flavor/lore text)
|
||
if (packet.getReadPos() < packet.getSize())
|
||
data.description = packet.readString();
|
||
|
||
// Post-description: PageText, LanguageID, PageMaterial, StartQuest
|
||
if (packet.getReadPos() + 16 <= packet.getSize()) {
|
||
packet.readUInt32(); // PageText
|
||
packet.readUInt32(); // LanguageID
|
||
packet.readUInt32(); // PageMaterial
|
||
data.startQuestId = packet.readUInt32(); // StartQuest
|
||
}
|
||
|
||
data.valid = !data.name.empty();
|
||
LOG_DEBUG("[Classic] Item query response: ", data.name, " (quality=", data.quality,
|
||
" invType=", data.inventoryType, " stack=", data.maxStack, ")");
|
||
return true;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Turtle WoW (build 7234) parseMovementBlock
|
||
//
|
||
// Turtle WoW is a heavily modified vanilla (1.12.1) server. Through hex dump
|
||
// analysis the wire format is nearly identical to Classic with one key addition:
|
||
//
|
||
// LIVING section:
|
||
// moveFlags u32 (NO moveFlags2 — confirmed by position alignment)
|
||
// time u32
|
||
// position 4×float
|
||
// transport guarded by moveFlags & 0x02000000 (Classic flag)
|
||
// packed GUID + 4 floats + u32 timestamp (TBC-style addition)
|
||
// pitch guarded by SWIMMING (0x200000)
|
||
// fallTime u32
|
||
// jump data guarded by JUMPING (0x2000)
|
||
// splineElev guarded by 0x04000000
|
||
// speeds 6 floats (walk/run/runBack/swim/swimBack/turnRate)
|
||
// spline guarded by 0x00400000 (Classic flag) OR 0x08000000 (TBC flag)
|
||
//
|
||
// Tail (same as Classic):
|
||
// LOWGUID → 1×u32
|
||
// HIGHGUID → 1×u32
|
||
//
|
||
// The ONLY confirmed difference from pure Classic is:
|
||
// Transport data includes a u32 timestamp after the 4 transport floats
|
||
// (Classic omits this; TBC/WotLK include it). Without this, entities on
|
||
// transports cause a 4-byte desync that cascades to later blocks.
|
||
// ============================================================================
|
||
namespace TurtleMoveFlags {
|
||
constexpr uint32_t ONTRANSPORT = 0x02000000; // Classic transport flag
|
||
constexpr uint32_t JUMPING = 0x00002000;
|
||
constexpr uint32_t SWIMMING = 0x00200000;
|
||
constexpr uint32_t SPLINE_ELEVATION = 0x04000000;
|
||
constexpr uint32_t SPLINE_CLASSIC = 0x00400000; // Classic spline enabled
|
||
constexpr uint32_t SPLINE_TBC = 0x08000000; // TBC spline enabled
|
||
}
|
||
|
||
bool TurtlePacketParsers::parseMovementBlock(network::Packet& packet, UpdateBlock& block) {
|
||
auto rem = [&]() -> size_t { return packet.getSize() - packet.getReadPos(); };
|
||
if (rem() < 1) return false;
|
||
|
||
uint8_t updateFlags = packet.readUInt8();
|
||
block.updateFlags = static_cast<uint16_t>(updateFlags);
|
||
|
||
LOG_DEBUG(" [Turtle] UpdateFlags: 0x", std::hex, (int)updateFlags, std::dec);
|
||
|
||
const uint8_t UPDATEFLAG_TRANSPORT = 0x02;
|
||
const uint8_t UPDATEFLAG_MELEE_ATTACKING = 0x04;
|
||
const uint8_t UPDATEFLAG_HIGHGUID = 0x08;
|
||
const uint8_t UPDATEFLAG_ALL = 0x10;
|
||
const uint8_t UPDATEFLAG_LIVING = 0x20;
|
||
const uint8_t UPDATEFLAG_HAS_POSITION = 0x40;
|
||
|
||
if (updateFlags & UPDATEFLAG_LIVING) {
|
||
// Minimum: moveFlags(4)+time(4)+position(16)+fallTime(4)+speeds(24) = 52 bytes
|
||
if (rem() < 52) return false;
|
||
size_t livingStart = packet.getReadPos();
|
||
|
||
uint32_t moveFlags = packet.readUInt32();
|
||
// Turtle: NO moveFlags2 (confirmed by hex dump — positions are only correct without it)
|
||
/*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(" [Turtle] LIVING: (", block.x, ", ", block.y, ", ", block.z,
|
||
"), o=", block.orientation, " moveFlags=0x", std::hex, moveFlags, std::dec);
|
||
|
||
// Transport — Classic flag position 0x02000000
|
||
if (moveFlags & TurtleMoveFlags::ONTRANSPORT) {
|
||
if (rem() < 1) return false; // PackedGuid mask byte
|
||
block.onTransport = true;
|
||
block.transportGuid = UpdateObjectParser::readPackedGuid(packet);
|
||
if (rem() < 20) return false; // 4 floats + u32 timestamp
|
||
block.transportX = packet.readFloat();
|
||
block.transportY = packet.readFloat();
|
||
block.transportZ = packet.readFloat();
|
||
block.transportO = packet.readFloat();
|
||
/*uint32_t transportTime =*/ packet.readUInt32(); // Turtle adds TBC-style timestamp
|
||
}
|
||
|
||
// Pitch (swimming only, Classic-style)
|
||
if (moveFlags & TurtleMoveFlags::SWIMMING) {
|
||
if (rem() < 4) return false;
|
||
/*float pitch =*/ packet.readFloat();
|
||
}
|
||
|
||
// Fall time (always present)
|
||
if (rem() < 4) return false;
|
||
/*uint32_t fallTime =*/ packet.readUInt32();
|
||
|
||
// Jump data
|
||
if (moveFlags & TurtleMoveFlags::JUMPING) {
|
||
if (rem() < 16) return false;
|
||
/*float jumpVelocity =*/ packet.readFloat();
|
||
/*float jumpSinAngle =*/ packet.readFloat();
|
||
/*float jumpCosAngle =*/ packet.readFloat();
|
||
/*float jumpXYSpeed =*/ packet.readFloat();
|
||
}
|
||
|
||
// Spline elevation
|
||
if (moveFlags & TurtleMoveFlags::SPLINE_ELEVATION) {
|
||
if (rem() < 4) return false;
|
||
/*float splineElevation =*/ packet.readFloat();
|
||
}
|
||
|
||
// Turtle: 6 speeds (same as Classic — no flight speeds)
|
||
if (rem() < 24) return false; // 6 × float
|
||
float walkSpeed = packet.readFloat();
|
||
float runSpeed = packet.readFloat();
|
||
float runBackSpeed = packet.readFloat();
|
||
float swimSpeed = packet.readFloat();
|
||
float swimBackSpeed = packet.readFloat();
|
||
float turnRate = packet.readFloat();
|
||
|
||
block.runSpeed = runSpeed;
|
||
|
||
LOG_DEBUG(" [Turtle] Speeds: walk=", walkSpeed, " run=", runSpeed,
|
||
" runBack=", runBackSpeed, " swim=", swimSpeed,
|
||
" swimBack=", swimBackSpeed, " turn=", turnRate);
|
||
|
||
// Spline data — check both Classic (0x00400000) and TBC (0x08000000) flag positions
|
||
bool hasSpline = (moveFlags & TurtleMoveFlags::SPLINE_CLASSIC) ||
|
||
(moveFlags & TurtleMoveFlags::SPLINE_TBC);
|
||
if (hasSpline) {
|
||
if (rem() < 4) return false;
|
||
uint32_t splineFlags = packet.readUInt32();
|
||
LOG_DEBUG(" [Turtle] Spline: flags=0x", std::hex, splineFlags, std::dec);
|
||
|
||
if (splineFlags & 0x00010000) {
|
||
if (rem() < 12) return false;
|
||
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||
} else if (splineFlags & 0x00020000) {
|
||
if (rem() < 8) return false;
|
||
packet.readUInt64();
|
||
} else if (splineFlags & 0x00040000) {
|
||
if (rem() < 4) return false;
|
||
packet.readFloat();
|
||
}
|
||
|
||
// timePassed + duration + splineId + pointCount = 16 bytes
|
||
if (rem() < 16) return false;
|
||
/*uint32_t timePassed =*/ packet.readUInt32();
|
||
/*uint32_t duration =*/ packet.readUInt32();
|
||
/*uint32_t splineId =*/ packet.readUInt32();
|
||
|
||
uint32_t pointCount = packet.readUInt32();
|
||
if (pointCount > 256) {
|
||
static uint32_t badTurtleSplineCount = 0;
|
||
++badTurtleSplineCount;
|
||
if (badTurtleSplineCount <= 5 || (badTurtleSplineCount % 100) == 0) {
|
||
LOG_WARNING(" [Turtle] Spline pointCount=", pointCount,
|
||
" exceeds max (occurrence=", badTurtleSplineCount, ")");
|
||
}
|
||
return false;
|
||
}
|
||
// points + endPoint
|
||
if (rem() < static_cast<size_t>(pointCount) * 12 + 12) return false;
|
||
for (uint32_t i = 0; i < pointCount; i++) {
|
||
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||
}
|
||
|
||
// End point
|
||
packet.readFloat(); packet.readFloat(); packet.readFloat();
|
||
}
|
||
|
||
LOG_DEBUG(" [Turtle] LIVING block consumed ", packet.getReadPos() - livingStart,
|
||
" bytes, readPos now=", packet.getReadPos());
|
||
}
|
||
else if (updateFlags & UPDATEFLAG_HAS_POSITION) {
|
||
if (rem() < 16) return false;
|
||
block.x = packet.readFloat();
|
||
block.y = packet.readFloat();
|
||
block.z = packet.readFloat();
|
||
block.orientation = packet.readFloat();
|
||
block.hasMovement = true;
|
||
|
||
LOG_DEBUG(" [Turtle] STATIONARY: (", block.x, ", ", block.y, ", ", block.z, ")");
|
||
}
|
||
|
||
// High GUID — 1×u32
|
||
if (updateFlags & UPDATEFLAG_HIGHGUID) {
|
||
if (rem() < 4) return false;
|
||
/*uint32_t highGuid =*/ packet.readUInt32();
|
||
}
|
||
|
||
if (updateFlags & UPDATEFLAG_ALL) {
|
||
if (rem() < 4) return false;
|
||
/*uint32_t unkAll =*/ packet.readUInt32();
|
||
}
|
||
|
||
if (updateFlags & UPDATEFLAG_MELEE_ATTACKING) {
|
||
if (rem() < 1) return false;
|
||
/*uint64_t meleeTargetGuid =*/ UpdateObjectParser::readPackedGuid(packet);
|
||
}
|
||
|
||
if (updateFlags & UPDATEFLAG_TRANSPORT) {
|
||
if (rem() < 4) return false;
|
||
/*uint32_t transportTime =*/ packet.readUInt32();
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
bool TurtlePacketParsers::parseUpdateObject(network::Packet& packet, UpdateObjectData& data) {
|
||
constexpr uint32_t kMaxReasonableUpdateBlocks = 4096;
|
||
|
||
auto parseWithLayout = [&](bool withHasTransportByte, UpdateObjectData& out) -> bool {
|
||
out = UpdateObjectData{};
|
||
const size_t start = packet.getReadPos();
|
||
if (packet.getSize() - start < 4) return false;
|
||
|
||
out.blockCount = packet.readUInt32();
|
||
if (out.blockCount > kMaxReasonableUpdateBlocks) {
|
||
packet.setReadPos(start);
|
||
return false;
|
||
}
|
||
|
||
if (withHasTransportByte) {
|
||
if (packet.getReadPos() >= packet.getSize()) {
|
||
packet.setReadPos(start);
|
||
return false;
|
||
}
|
||
/*uint8_t hasTransport =*/ packet.readUInt8();
|
||
}
|
||
|
||
uint32_t remainingBlockCount = out.blockCount;
|
||
|
||
if (packet.getReadPos() + 1 <= packet.getSize()) {
|
||
uint8_t firstByte = packet.readUInt8();
|
||
if (firstByte == static_cast<uint8_t>(UpdateType::OUT_OF_RANGE_OBJECTS)) {
|
||
if (remainingBlockCount == 0) {
|
||
packet.setReadPos(start);
|
||
return false;
|
||
}
|
||
--remainingBlockCount;
|
||
if (packet.getReadPos() + 4 > packet.getSize()) {
|
||
packet.setReadPos(start);
|
||
return false;
|
||
}
|
||
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;
|
||
}
|
||
out.outOfRangeGuids.push_back(UpdateObjectParser::readPackedGuid(packet));
|
||
}
|
||
} else {
|
||
packet.setReadPos(packet.getReadPos() - 1);
|
||
}
|
||
}
|
||
|
||
out.blockCount = remainingBlockCount;
|
||
out.blocks.reserve(out.blockCount);
|
||
for (uint32_t i = 0; i < out.blockCount; ++i) {
|
||
if (packet.getReadPos() >= packet.getSize()) {
|
||
packet.setReadPos(start);
|
||
return false;
|
||
}
|
||
|
||
const size_t blockStart = packet.getReadPos();
|
||
uint8_t updateTypeVal = packet.readUInt8();
|
||
if (updateTypeVal > static_cast<uint8_t>(UpdateType::NEAR_OBJECTS)) {
|
||
packet.setReadPos(start);
|
||
return false;
|
||
}
|
||
|
||
const UpdateType updateType = static_cast<UpdateType>(updateTypeVal);
|
||
UpdateBlock block;
|
||
block.updateType = updateType;
|
||
bool ok = false;
|
||
|
||
auto parseMovementVariant = [&](auto&& movementParser, const char* layoutName) -> bool {
|
||
packet.setReadPos(blockStart + 1);
|
||
block = UpdateBlock{};
|
||
block.updateType = updateType;
|
||
|
||
switch (updateType) {
|
||
case UpdateType::MOVEMENT:
|
||
block.guid = packet.readUInt64();
|
||
if (!movementParser(packet, block)) return false;
|
||
LOG_DEBUG("[Turtle] Parsed MOVEMENT block via ", layoutName, " layout");
|
||
return true;
|
||
case UpdateType::CREATE_OBJECT:
|
||
case UpdateType::CREATE_OBJECT2:
|
||
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||
if (packet.getReadPos() >= packet.getSize()) return false;
|
||
block.objectType = static_cast<ObjectType>(packet.readUInt8());
|
||
if (!movementParser(packet, block)) return false;
|
||
if (!UpdateObjectParser::parseUpdateFields(packet, block)) return false;
|
||
LOG_DEBUG("[Turtle] Parsed CREATE block via ", layoutName, " layout");
|
||
return true;
|
||
default:
|
||
return false;
|
||
}
|
||
};
|
||
|
||
switch (updateType) {
|
||
case UpdateType::VALUES:
|
||
block.guid = UpdateObjectParser::readPackedGuid(packet);
|
||
ok = UpdateObjectParser::parseUpdateFields(packet, block);
|
||
break;
|
||
case UpdateType::MOVEMENT:
|
||
case UpdateType::CREATE_OBJECT:
|
||
case UpdateType::CREATE_OBJECT2:
|
||
ok = parseMovementVariant(
|
||
[this](network::Packet& p, UpdateBlock& b) {
|
||
return this->TurtlePacketParsers::parseMovementBlock(p, b);
|
||
}, "turtle");
|
||
if (!ok) {
|
||
ok = parseMovementVariant(
|
||
[this](network::Packet& p, UpdateBlock& b) {
|
||
return this->ClassicPacketParsers::parseMovementBlock(p, b);
|
||
}, "classic");
|
||
}
|
||
if (!ok) {
|
||
ok = parseMovementVariant(
|
||
[this](network::Packet& p, UpdateBlock& b) {
|
||
return this->TbcPacketParsers::parseMovementBlock(p, b);
|
||
}, "tbc");
|
||
}
|
||
// NOTE: Do NOT fall back to WotLK parseMovementBlock here.
|
||
// WotLK uses uint16 updateFlags and 9 speeds vs Classic's uint8
|
||
// and 6 speeds. A false-positive WotLK parse consumes wrong bytes,
|
||
// corrupting subsequent update fields and losing NPC data.
|
||
break;
|
||
case UpdateType::OUT_OF_RANGE_OBJECTS:
|
||
case UpdateType::NEAR_OBJECTS:
|
||
ok = true;
|
||
break;
|
||
default:
|
||
ok = false;
|
||
break;
|
||
}
|
||
|
||
if (!ok) {
|
||
LOG_WARNING("[Turtle] SMSG_UPDATE_OBJECT block parse failed",
|
||
" blockIndex=", i,
|
||
" updateType=", updateTypeName(updateType),
|
||
" readPos=", packet.getReadPos(),
|
||
" blockStart=", blockStart,
|
||
" packetSize=", packet.getSize());
|
||
packet.setReadPos(start);
|
||
return false;
|
||
}
|
||
|
||
out.blocks.push_back(std::move(block));
|
||
}
|
||
|
||
return true;
|
||
};
|
||
|
||
const size_t startPos = packet.getReadPos();
|
||
UpdateObjectData parsed;
|
||
if (parseWithLayout(true, parsed)) {
|
||
data = std::move(parsed);
|
||
return true;
|
||
}
|
||
|
||
packet.setReadPos(startPos);
|
||
if (parseWithLayout(false, parsed)) {
|
||
LOG_DEBUG("[Turtle] SMSG_UPDATE_OBJECT parsed without has_transport byte fallback");
|
||
data = std::move(parsed);
|
||
return true;
|
||
}
|
||
|
||
packet.setReadPos(startPos);
|
||
if (ClassicPacketParsers::parseUpdateObject(packet, parsed)) {
|
||
LOG_DEBUG("[Turtle] SMSG_UPDATE_OBJECT parsed via full classic fallback");
|
||
data = std::move(parsed);
|
||
return true;
|
||
}
|
||
|
||
packet.setReadPos(startPos);
|
||
if (TbcPacketParsers::parseUpdateObject(packet, parsed)) {
|
||
LOG_DEBUG("[Turtle] SMSG_UPDATE_OBJECT parsed via full TBC fallback");
|
||
data = std::move(parsed);
|
||
return true;
|
||
}
|
||
|
||
packet.setReadPos(startPos);
|
||
return false;
|
||
}
|
||
|
||
bool TurtlePacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveData& data) {
|
||
// Turtle realms can emit vanilla-like, TBC-like, and WotLK-like monster move
|
||
// bodies. Try the lower-expansion layouts first before the WotLK parser that
|
||
// expects an extra unk byte after the packed GUID.
|
||
size_t start = packet.getReadPos();
|
||
if (MonsterMoveParser::parseVanilla(packet, data)) {
|
||
return true;
|
||
}
|
||
|
||
packet.setReadPos(start);
|
||
if (TbcPacketParsers::parseMonsterMove(packet, data)) {
|
||
LOG_DEBUG("[Turtle] SMSG_MONSTER_MOVE parsed via TBC fallback layout");
|
||
return true;
|
||
}
|
||
|
||
auto looksLikeWotlkMonsterMove = [&](network::Packet& probe) -> bool {
|
||
const size_t probeStart = probe.getReadPos();
|
||
uint64_t guid = UpdateObjectParser::readPackedGuid(probe);
|
||
if (guid == 0) {
|
||
probe.setReadPos(probeStart);
|
||
return false;
|
||
}
|
||
if (probe.getReadPos() >= probe.getSize()) {
|
||
probe.setReadPos(probeStart);
|
||
return false;
|
||
}
|
||
uint8_t unk = probe.readUInt8();
|
||
if (unk > 1) {
|
||
probe.setReadPos(probeStart);
|
||
return false;
|
||
}
|
||
if (probe.getReadPos() + 12 + 4 + 1 > probe.getSize()) {
|
||
probe.setReadPos(probeStart);
|
||
return false;
|
||
}
|
||
probe.readFloat(); probe.readFloat(); probe.readFloat(); // xyz
|
||
probe.readUInt32(); // splineId
|
||
uint8_t moveType = probe.readUInt8();
|
||
probe.setReadPos(probeStart);
|
||
return moveType >= 1 && moveType <= 4;
|
||
};
|
||
|
||
packet.setReadPos(start);
|
||
if (!looksLikeWotlkMonsterMove(packet)) {
|
||
packet.setReadPos(start);
|
||
return false;
|
||
}
|
||
|
||
packet.setReadPos(start);
|
||
if (MonsterMoveParser::parse(packet, data)) {
|
||
LOG_DEBUG("[Turtle] SMSG_MONSTER_MOVE parsed via WotLK fallback layout");
|
||
return true;
|
||
}
|
||
|
||
packet.setReadPos(start);
|
||
return false;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic/Vanilla quest giver status
|
||
//
|
||
// Vanilla sends status as uint32 with different enum values:
|
||
// 0=NONE, 1=UNAVAILABLE, 2=CHAT, 3=INCOMPLETE, 4=REWARD_REP, 5=AVAILABLE
|
||
// WotLK uses uint8 with:
|
||
// 0=NONE, 1=UNAVAILABLE, 5=INCOMPLETE, 6=REWARD_REP, 7=AVAILABLE_LOW, 8=AVAILABLE, 10=REWARD
|
||
//
|
||
// Read uint32, translate to WotLK enum values.
|
||
// ============================================================================
|
||
uint8_t ClassicPacketParsers::readQuestGiverStatus(network::Packet& packet) {
|
||
uint32_t vanillaStatus = packet.readUInt32();
|
||
switch (vanillaStatus) {
|
||
case 0: return 0; // NONE
|
||
case 1: return 1; // UNAVAILABLE
|
||
case 2: return 0; // CHAT → NONE (no marker)
|
||
case 3: return 5; // INCOMPLETE → WotLK INCOMPLETE
|
||
case 4: return 6; // REWARD_REP → WotLK REWARD_REP
|
||
case 5: return 8; // AVAILABLE → WotLK AVAILABLE
|
||
case 6: return 10; // REWARD → WotLK REWARD
|
||
default: return 0;
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// Classic CMSG_QUESTGIVER_QUERY_QUEST — Vanilla 1.12 format
|
||
// WotLK appends a trailing unk1 byte; Vanilla servers don't expect it and
|
||
// some reject or misparse the 13-byte packet, preventing quest details from
|
||
// being sent back. Classic format: guid(8) + questId(4) = 12 bytes.
|
||
// ============================================================================
|
||
network::Packet ClassicPacketParsers::buildQueryQuestPacket(uint64_t npcGuid, uint32_t questId) {
|
||
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_QUERY_QUEST));
|
||
packet.writeUInt64(npcGuid);
|
||
packet.writeUInt32(questId);
|
||
// No trailing unk byte (WotLK-only field)
|
||
return packet;
|
||
}
|
||
|
||
network::Packet ClassicPacketParsers::buildAcceptQuestPacket(uint64_t npcGuid, uint32_t questId) {
|
||
network::Packet packet(wireOpcode(Opcode::CMSG_QUESTGIVER_ACCEPT_QUEST));
|
||
packet.writeUInt64(npcGuid);
|
||
packet.writeUInt32(questId);
|
||
// Classic/Turtle: no trailing unk1 uint32
|
||
return packet;
|
||
}
|
||
|
||
// ============================================================================
|
||
// ClassicPacketParsers::parseCreatureQueryResponse
|
||
//
|
||
// Classic 1.12 SMSG_CREATURE_QUERY_RESPONSE lacks the iconName CString field
|
||
// that TBC 2.4.3 and WotLK 3.3.5a include between subName and typeFlags.
|
||
// Without this override, the TBC/WotLK parser reads typeFlags bytes as the
|
||
// iconName string, shifting typeFlags/creatureType/family/rank by 1-4 bytes.
|
||
// ============================================================================
|
||
bool ClassicPacketParsers::parseCreatureQueryResponse(network::Packet& packet,
|
||
CreatureQueryResponseData& data) {
|
||
// Validate minimum packet size: entry(4)
|
||
if (packet.getSize() < 4) {
|
||
LOG_ERROR("Classic SMSG_CREATURE_QUERY_RESPONSE: packet too small (", packet.getSize(), " bytes)");
|
||
return false;
|
||
}
|
||
|
||
data.entry = packet.readUInt32();
|
||
if (data.entry & 0x80000000) {
|
||
data.entry &= ~0x80000000;
|
||
data.name = "";
|
||
return true;
|
||
}
|
||
|
||
data.name = packet.readString();
|
||
packet.readString(); // name2
|
||
packet.readString(); // name3
|
||
packet.readString(); // name4
|
||
data.subName = packet.readString();
|
||
// NOTE: NO iconName field in Classic 1.12 — goes straight to typeFlags
|
||
if (packet.getReadPos() + 16 > packet.getSize()) {
|
||
LOG_WARNING("Classic SMSG_CREATURE_QUERY_RESPONSE: truncated at typeFlags (entry=", data.entry, ")");
|
||
data.typeFlags = 0;
|
||
data.creatureType = 0;
|
||
data.family = 0;
|
||
data.rank = 0;
|
||
return true; // Have name/sub fields; base fields are important but optional
|
||
}
|
||
data.typeFlags = packet.readUInt32();
|
||
data.creatureType = packet.readUInt32();
|
||
data.family = packet.readUInt32();
|
||
data.rank = packet.readUInt32();
|
||
|
||
LOG_DEBUG("Classic SMSG_CREATURE_QUERY_RESPONSE: ", data.name, " type=", data.creatureType,
|
||
" rank=", data.rank);
|
||
return true;
|
||
}
|
||
|
||
} // namespace game
|
||
} // namespace wowee
|