From b15a21a957a34a32535b564fbfac15d573ae4f8e Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 10 Mar 2026 00:27:08 -0700 Subject: [PATCH] game: add Classic 1.12 overrides for melee/spell damage log packets Vanilla 1.12 SMSG_ATTACKERSTATEUPDATE, SMSG_SPELLNONMELEEDAMAGELOG, and SMSG_SPELLHEALLOG use PackedGuid for all entity GUIDs, not full uint64 as TBC and WotLK do. Without these overrides Classic inherited TBC's implementations, which over-read PackedGuid fields as fixed 8-byte GUIDs, misaligning all subsequent damage/heal fields and making combat parsing unusable on Classic servers. The Classic override logic is identical to TBC except for the GUID reads, so combat text, damage numbers, and kill tracking now work correctly on Vanilla 1.12 connections. --- include/game/packet_parsers.hpp | 4 + src/game/packet_parsers_classic.cpp | 109 ++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/include/game/packet_parsers.hpp b/include/game/packet_parsers.hpp index 2d7cd1e7..d34df5d5 100644 --- a/include/game/packet_parsers.hpp +++ b/include/game/packet_parsers.hpp @@ -408,6 +408,10 @@ public: // Classic 1.12 uses PackedGuid (not full uint64) and uint16 castFlags (not uint32) bool parseSpellStart(network::Packet& packet, SpellStartData& data) override; bool parseSpellGo(network::Packet& packet, SpellGoData& data) override; + // Classic 1.12 melee/spell log packets use PackedGuid (not full uint64) + bool parseAttackerStateUpdate(network::Packet& packet, AttackerStateUpdateData& data) override; + bool parseSpellDamageLog(network::Packet& packet, SpellDamageLogData& data) override; + bool parseSpellHealLog(network::Packet& packet, SpellHealLogData& data) override; }; /** diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index bfc6cdc4..bd65e03d 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -406,6 +406,115 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da 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) { + auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; + if (rem() < 5) return false; // hitInfo(4) + at least GUID mask byte(1) + + data.hitInfo = packet.readUInt32(); + data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla + if (rem() < 1) return false; + data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla + + if (rem() < 5) return false; // int32 totalDamage + uint8 subDamageCount + data.totalDamage = static_cast(packet.readUInt32()); + data.subDamageCount = packet.readUInt8(); + + for (uint8_t i = 0; i < data.subDamageCount && rem() >= 20; ++i) { + 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); + } + + if (rem() < 8) return true; + data.victimState = packet.readUInt32(); + data.overkill = static_cast(packet.readUInt32()); + + if (rem() >= 4) { + data.blocked = packet.readUInt32(); + } + + LOG_INFO("[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) return false; + + data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla + if (rem() < 1) 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_INFO("[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) return false; + + data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla + if (rem() < 1) 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_INFO("[Classic] Spell heal: spellId=", data.spellId, " heal=", data.heal, + data.isCrit ? " CRIT" : ""); + return true; +} + // ============================================================================ // Classic SMSG_CAST_FAILED: no castCount byte (added in TBC/WotLK) // Format: spellId(u32) + result(u8)