From c5e7dde93167924f98b22c55c908441ac0209023 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 20:52:34 -0700 Subject: [PATCH 01/36] fix(combatlog): parse proc log GUIDs in classic and tbc --- src/game/game_handler.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 535b2dea..186781b0 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6306,15 +6306,22 @@ void GameHandler::handlePacket(network::Packet& packet) { break; } case Opcode::SMSG_SPELL_CHANCE_PROC_LOG: { - // Format (all expansions): PackedGuid target + PackedGuid caster + uint32 spellId + ... - if (packet.getSize() - packet.getReadPos() < 3) { + // WotLK: packed_guid target + packed_guid caster + uint32 spellId + ... + // TBC/Classic: uint64 target + uint64 caster + uint32 spellId + ... + const bool procChanceTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); + auto readProcChanceGuid = [&]() -> uint64_t { + if (procChanceTbcLike) + return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0; + return UpdateObjectParser::readPackedGuid(packet); + }; + if (packet.getSize() - packet.getReadPos() < (procChanceTbcLike ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } - uint64_t procTargetGuid = UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 2) { + uint64_t procTargetGuid = readProcChanceGuid(); + if (packet.getSize() - packet.getReadPos() < (procChanceTbcLike ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } - uint64_t procCasterGuid = UpdateObjectParser::readPackedGuid(packet); + uint64_t procCasterGuid = readProcChanceGuid(); if (packet.getSize() - packet.getReadPos() < 4) { packet.setReadPos(packet.getSize()); break; } From d4d876a563526338ea4a8bb062825e795ebfd66a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 21:00:34 -0700 Subject: [PATCH 02/36] fix(combatlog): list all dispelled and stolen auras in system messages --- src/game/game_handler.cpp | 76 ++++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 24 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 186781b0..c639bd52 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -123,6 +123,40 @@ std::string formatCopperAmount(uint32_t amount) { return oss.str(); } +std::string displaySpellName(GameHandler& handler, uint32_t spellId) { + if (spellId == 0) return {}; + const std::string& name = handler.getSpellName(spellId); + if (!name.empty()) return name; + return "spell " + std::to_string(spellId); +} + +std::string formatSpellNameList(GameHandler& handler, + const std::vector& spellIds, + size_t maxShown = 3) { + if (spellIds.empty()) return {}; + + const size_t shownCount = std::min(spellIds.size(), maxShown); + std::ostringstream oss; + for (size_t i = 0; i < shownCount; ++i) { + if (i > 0) { + if (shownCount == 2) { + oss << " and "; + } else if (i == shownCount - 1) { + oss << ", and "; + } else { + oss << ", "; + } + } + oss << displaySpellName(handler, spellIds[i]); + } + + if (spellIds.size() > shownCount) { + oss << ", and " << (spellIds.size() - shownCount) << " more"; + } + + return oss.str(); +} + bool readCStringAt(const std::vector& data, size_t start, std::string& out, size_t& nextPos) { out.clear(); if (start >= data.size()) return false; @@ -6192,29 +6226,28 @@ void GameHandler::handlePacket(network::Packet& packet) { loggedIds = dispelledIds; } - const uint32_t displaySpellId = !loggedIds.empty() ? loggedIds.front() : 0; - const std::string displaySpellName = displaySpellId != 0 - ? [&]() { - const std::string& nm = getSpellName(displaySpellId); - return nm.empty() ? ("spell " + std::to_string(displaySpellId)) : nm; - }() - : std::string{}; - if (!displaySpellName.empty()) { + const std::string displaySpellNames = formatSpellNameList(*this, loggedIds); + if (!displaySpellNames.empty()) { char buf[256]; + const char* passiveVerb = loggedIds.size() == 1 ? "was" : "were"; if (isStolen) { if (victimGuid == playerGuid && casterGuid != playerGuid) - std::snprintf(buf, sizeof(buf), "%s was stolen.", displaySpellName.c_str()); + std::snprintf(buf, sizeof(buf), "%s %s stolen.", + displaySpellNames.c_str(), passiveVerb); else if (casterGuid == playerGuid) - std::snprintf(buf, sizeof(buf), "You steal %s.", displaySpellName.c_str()); + std::snprintf(buf, sizeof(buf), "You steal %s.", displaySpellNames.c_str()); else - std::snprintf(buf, sizeof(buf), "%s was stolen.", displaySpellName.c_str()); + std::snprintf(buf, sizeof(buf), "%s %s stolen.", + displaySpellNames.c_str(), passiveVerb); } else { if (victimGuid == playerGuid && casterGuid != playerGuid) - std::snprintf(buf, sizeof(buf), "%s was dispelled.", displaySpellName.c_str()); + std::snprintf(buf, sizeof(buf), "%s %s dispelled.", + displaySpellNames.c_str(), passiveVerb); else if (casterGuid == playerGuid) - std::snprintf(buf, sizeof(buf), "You dispel %s.", displaySpellName.c_str()); + std::snprintf(buf, sizeof(buf), "You dispel %s.", displaySpellNames.c_str()); else - std::snprintf(buf, sizeof(buf), "%s was dispelled.", displaySpellName.c_str()); + std::snprintf(buf, sizeof(buf), "%s %s dispelled.", + displaySpellNames.c_str(), passiveVerb); } addSystemChatMessage(buf); } @@ -6277,19 +6310,14 @@ void GameHandler::handlePacket(network::Packet& packet) { loggedIds.push_back(stolenId); } - const uint32_t displaySpellId = !loggedIds.empty() ? loggedIds.front() : 0; - const std::string displaySpellName = displaySpellId != 0 - ? [&]() { - const std::string& nm = getSpellName(displaySpellId); - return nm.empty() ? ("spell " + std::to_string(displaySpellId)) : nm; - }() - : std::string{}; - if (!displaySpellName.empty()) { + const std::string displaySpellNames = formatSpellNameList(*this, loggedIds); + if (!displaySpellNames.empty()) { char buf[256]; if (stealCaster == playerGuid) - std::snprintf(buf, sizeof(buf), "You stole %s.", displaySpellName.c_str()); + std::snprintf(buf, sizeof(buf), "You stole %s.", displaySpellNames.c_str()); else - std::snprintf(buf, sizeof(buf), "%s was stolen.", displaySpellName.c_str()); + std::snprintf(buf, sizeof(buf), "%s %s stolen.", displaySpellNames.c_str(), + loggedIds.size() == 1 ? "was" : "were"); addSystemChatMessage(buf); } // Some servers emit both SPELLDISPELLOG(isStolen=1) and SPELLSTEALLOG From 16c8a2fd33ee664b9c7fbbcdfc3e29d7daa01cd8 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 21:08:00 -0700 Subject: [PATCH 03/36] fix(combatlog): parse packed spell-go hit target GUIDs --- src/game/world_packets.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index dbcbf4c9..b135fd6b 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3660,12 +3660,13 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { data.hitTargets.reserve(data.hitCount); for (uint8_t i = 0; i < data.hitCount; ++i) { - if (packet.getSize() - packet.getReadPos() < 8) { + // WotLK hit targets are packed GUIDs, like the caster and miss targets. + if (packet.getSize() - packet.getReadPos() < 1) { LOG_WARNING("Spell go: truncated hit targets at index ", (int)i, "/", (int)data.hitCount); data.hitCount = i; break; } - data.hitTargets.push_back(packet.readUInt64()); + data.hitTargets.push_back(UpdateObjectParser::readPackedGuid(packet)); } // Validate missCount field exists From cf68c156f148702340566d4fe06fbf7d40948bb5 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 21:16:24 -0700 Subject: [PATCH 04/36] fix(combatlog): accept short packed spell-go packets --- src/game/world_packets.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index b135fd6b..64ae5e00 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3632,8 +3632,9 @@ bool SpellStartParser::parse(network::Packet& packet, SpellStartData& data) { } bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { - // Upfront validation: packed GUID(1-8) + packed GUID(1-8) + castCount(1) + spellId(4) + castFlags(4) + timestamp(4) + hitCount(1) + missCount(1) = 24 bytes minimum - if (packet.getSize() - packet.getReadPos() < 24) return false; + // Packed GUIDs are variable-length, so only require the smallest possible + // shape up front: 2 GUID masks + fixed fields through missCount. + if (packet.getSize() - packet.getReadPos() < 17) return false; size_t startPos = packet.getReadPos(); data.casterGuid = UpdateObjectParser::readPackedGuid(packet); From 5be55b1b14e0c2bbf2933da6401a2ff0bb6f13e2 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 22:14:04 -0700 Subject: [PATCH 05/36] fix(combatlog): validate full TBC spell-go header --- src/game/packet_parsers_tbc.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index 935b34ae..83e2511c 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -1261,7 +1261,9 @@ bool TbcPacketParsers::parseSpellStart(network::Packet& packet, SpellStartData& // WotLK uses packed GUIDs and adds a timestamp (u32) after castFlags. // ============================================================================ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) { - if (packet.getSize() - packet.getReadPos() < 19) return false; + // Fixed header before hit/miss lists: + // casterGuid(u64) + casterUnit(u64) + castCount(u8) + spellId(u32) + castFlags(u32) + if (packet.getSize() - packet.getReadPos() < 25) return false; data.casterGuid = packet.readUInt64(); // full GUID in TBC data.casterUnit = packet.readUInt64(); // full GUID in TBC From 3ef5b546fb14b0010cafeb72f45eda0378b72b1d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 22:22:00 -0700 Subject: [PATCH 06/36] fix(combatlog): render instakill events explicitly --- include/game/spell_defines.hpp | 2 +- src/game/game_handler.cpp | 6 ++---- src/ui/game_screen.cpp | 16 ++++++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/include/game/spell_defines.hpp b/include/game/spell_defines.hpp index 92deadd7..5ddfaa04 100644 --- a/include/game/spell_defines.hpp +++ b/include/game/spell_defines.hpp @@ -53,7 +53,7 @@ struct CombatTextEntry { MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK, CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL, ENERGIZE, XP_GAIN, IMMUNE, ABSORB, RESIST, PROC_TRIGGER, - DISPEL, STEAL, INTERRUPT + DISPEL, STEAL, INTERRUPT, INSTAKILL }; Type type; int32_t amount = 0; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index c639bd52..b266de02 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6376,11 +6376,9 @@ void GameHandler::handlePacket(network::Packet& packet) { uint32_t ikSpell = (ik_rem() >= 4) ? packet.readUInt32() : 0; // Show kill/death feedback for the local player if (ikCaster == playerGuid) { - // We killed a target instantly — show a KILL combat text hit - addCombatText(CombatTextEntry::MELEE_DAMAGE, 0, ikSpell, true, 0, ikCaster, ikVictim); + addCombatText(CombatTextEntry::INSTAKILL, 0, ikSpell, true, 0, ikCaster, ikVictim); } else if (ikVictim == playerGuid) { - // We were instantly killed — show a large incoming hit - addCombatText(CombatTextEntry::MELEE_DAMAGE, 0, ikSpell, false, 0, ikCaster, ikVictim); + addCombatText(CombatTextEntry::INSTAKILL, 0, ikSpell, false, 0, ikCaster, ikVictim); addSystemChatMessage("You were killed by an instant-kill effect."); } LOG_DEBUG("SMSG_SPELLINSTAKILLLOG: caster=0x", std::hex, ikCaster, diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 28aa882c..53e615bf 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -8414,6 +8414,11 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) { color = ImVec4(1.0f, 0.6f, 0.9f, alpha); break; } + case game::CombatTextEntry::INSTAKILL: + snprintf(text, sizeof(text), outgoing ? "Kill!" : "Killed!"); + color = outgoing ? ImVec4(1.0f, 0.25f, 0.25f, alpha) + : ImVec4(1.0f, 0.1f, 0.1f, alpha); + break; default: snprintf(text, sizeof(text), "%d", entry.amount); color = ImVec4(1.0f, 1.0f, 1.0f, alpha); @@ -20315,6 +20320,17 @@ void GameScreen::renderCombatLog(game::GameHandler& gameHandler) { snprintf(desc, sizeof(desc), "%s interrupted", tgt); color = ImVec4(1.0f, 0.6f, 0.9f, 1.0f); break; + case T::INSTAKILL: + if (spell && e.isPlayerSource) + snprintf(desc, sizeof(desc), "You instantly kill %s with %s", tgt, spell); + else if (spell) + snprintf(desc, sizeof(desc), "%s instantly kills %s with %s", src, tgt, spell); + else if (e.isPlayerSource) + snprintf(desc, sizeof(desc), "You instantly kill %s", tgt); + else + snprintf(desc, sizeof(desc), "%s instantly kills %s", src, tgt); + color = ImVec4(1.0f, 0.2f, 0.2f, 1.0f); + break; default: snprintf(desc, sizeof(desc), "Combat event (type %d, amount %d)", (int)e.type, e.amount); color = ImVec4(0.7f, 0.7f, 0.7f, 1.0f); From 21762485ea0691a0ccff1ac788c8a3fc6bfa3a45 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 22:30:25 -0700 Subject: [PATCH 07/36] fix(combatlog): guard truncated spell energize packets --- src/game/game_handler.cpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index b266de02..ca2b3262 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -4012,14 +4012,22 @@ void GameHandler::handlePacket(network::Packet& packet) { // TBC: full uint64 victim + uint64 caster + uint32 spellId + uint8 powerType + int32 amount // Classic/Vanilla: packed_guid (same as WotLK) const bool energizeTbc = isActiveExpansion("tbc"); - size_t rem = packet.getSize() - packet.getReadPos(); - if (rem < (energizeTbc ? 8u : 2u)) { packet.setReadPos(packet.getSize()); break; } - uint64_t victimGuid = energizeTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - uint64_t casterGuid = energizeTbc - ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - rem = packet.getSize() - packet.getReadPos(); - if (rem < 6) { packet.setReadPos(packet.getSize()); break; } + auto readEnergizeGuid = [&]() -> uint64_t { + if (energizeTbc) + return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0; + return UpdateObjectParser::readPackedGuid(packet); + }; + if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u)) { + packet.setReadPos(packet.getSize()); break; + } + uint64_t victimGuid = readEnergizeGuid(); + if (packet.getSize() - packet.getReadPos() < (energizeTbc ? 8u : 1u)) { + packet.setReadPos(packet.getSize()); break; + } + uint64_t casterGuid = readEnergizeGuid(); + if (packet.getSize() - packet.getReadPos() < 9) { + packet.setReadPos(packet.getSize()); break; + } uint32_t spellId = packet.readUInt32(); uint8_t energizePowerType = packet.readUInt8(); int32_t amount = static_cast(packet.readUInt32()); From 46b297aacc5eaebb42f08b5d6e3e32933c7b0b9b Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 22:38:35 -0700 Subject: [PATCH 08/36] fix(combatlog): consume reflect payload in spell miss logs --- src/game/game_handler.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index ca2b3262..e6c0b520 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2682,9 +2682,10 @@ void GameHandler::handlePacket(network::Packet& packet) { // All expansions: uint32 spellId first. // WotLK: spellId(4) + packed_guid caster + uint8 unk + uint32 count // + count × (packed_guid victim + uint8 missInfo) - // [missInfo==11(REFLECT): + uint32 reflectSpellId + uint8 reflectResult] // TBC/Classic: spellId(4) + uint64 caster + uint8 unk + uint32 count // + count × (uint64 victim + uint8 missInfo) + // All expansions append uint32 reflectSpellId + uint8 reflectResult when + // missInfo==11 (REFLECT). const bool spellMissTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); auto readSpellMissGuid = [&]() -> uint64_t { if (spellMissTbcLike) @@ -2706,7 +2707,7 @@ void GameHandler::handlePacket(network::Packet& packet) { if (packet.getSize() - packet.getReadPos() < 1) break; uint8_t missInfo = packet.readUInt8(); // REFLECT (11): extra uint32 reflectSpellId + uint8 reflectResult - if (missInfo == 11 && !spellMissTbcLike) { + if (missInfo == 11) { if (packet.getSize() - packet.getReadPos() >= 5) { /*uint32_t reflectSpellId =*/ packet.readUInt32(); /*uint8_t reflectResult =*/ packet.readUInt8(); From 3e4708fe156531b8117d9ea56371a9f32beb367a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 22:45:59 -0700 Subject: [PATCH 09/36] fix(combatlog): parse classic spell miss GUIDs as packed --- src/game/game_handler.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index e6c0b520..3ab47010 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2680,29 +2680,29 @@ void GameHandler::handlePacket(network::Packet& packet) { // ---- Spell log miss ---- case Opcode::SMSG_SPELLLOGMISS: { // All expansions: uint32 spellId first. - // WotLK: spellId(4) + packed_guid caster + uint8 unk + uint32 count - // + count × (packed_guid victim + uint8 missInfo) - // TBC/Classic: spellId(4) + uint64 caster + uint8 unk + uint32 count - // + count × (uint64 victim + uint8 missInfo) + // WotLK/Classic: spellId(4) + packed_guid caster + uint8 unk + uint32 count + // + count × (packed_guid victim + uint8 missInfo) + // TBC: spellId(4) + uint64 caster + uint8 unk + uint32 count + // + count × (uint64 victim + uint8 missInfo) // All expansions append uint32 reflectSpellId + uint8 reflectResult when // missInfo==11 (REFLECT). - const bool spellMissTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool spellMissUsesFullGuid = isActiveExpansion("tbc"); auto readSpellMissGuid = [&]() -> uint64_t { - if (spellMissTbcLike) + if (spellMissUsesFullGuid) return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0; return UpdateObjectParser::readPackedGuid(packet); }; // spellId prefix present in all expansions if (packet.getSize() - packet.getReadPos() < 4) break; uint32_t spellId = packet.readUInt32(); - if (packet.getSize() - packet.getReadPos() < (spellMissTbcLike ? 8 : 1)) break; + if (packet.getSize() - packet.getReadPos() < (spellMissUsesFullGuid ? 8u : 1u)) break; uint64_t casterGuid = readSpellMissGuid(); if (packet.getSize() - packet.getReadPos() < 5) break; /*uint8_t unk =*/ packet.readUInt8(); uint32_t count = packet.readUInt32(); count = std::min(count, 32u); for (uint32_t i = 0; i < count; ++i) { - if (packet.getSize() - packet.getReadPos() < (spellMissTbcLike ? 9u : 2u)) break; + if (packet.getSize() - packet.getReadPos() < (spellMissUsesFullGuid ? 9u : 2u)) break; uint64_t victimGuid = readSpellMissGuid(); if (packet.getSize() - packet.getReadPos() < 1) break; uint8_t missInfo = packet.readUInt8(); From 5392243575936b8e0c7f4b2f12dcf8a6d4f1df48 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 22:53:04 -0700 Subject: [PATCH 10/36] fix(combatlog): stop treating spell break logs as damage shield hits --- src/game/game_handler.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 3ab47010..475c149b 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6133,8 +6133,6 @@ void GameHandler::handlePacket(network::Packet& packet) { } // ---- Spell combat logs (consume) ---- - case Opcode::SMSG_AURACASTLOG: - case Opcode::SMSG_SPELLBREAKLOG: case Opcode::SMSG_SPELLDAMAGESHIELD: { // Classic/TBC: uint64 victim + uint64 caster + spellId(4) + damage(4) + schoolMask(4) // WotLK: packed_guid victim + packed_guid caster + spellId(4) + damage(4) + absorbed(4) + schoolMask(4) @@ -6165,6 +6163,12 @@ void GameHandler::handlePacket(network::Packet& packet) { } break; } + case Opcode::SMSG_AURACASTLOG: + case Opcode::SMSG_SPELLBREAKLOG: + // These packets are not damage-shield events. Consume them without + // synthesizing reflected damage entries or misattributing GUIDs. + packet.setReadPos(packet.getSize()); + break; case Opcode::SMSG_SPELLORDAMAGE_IMMUNE: { // WotLK: packed casterGuid + packed victimGuid + uint32 spellId + uint8 saveType // TBC/Classic: full uint64 casterGuid + full uint64 victimGuid + uint32 + uint8 From dceaf8f1ac40776fbb8de2c9d5ecc29a3d4b5b78 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 23:00:49 -0700 Subject: [PATCH 11/36] fix(combattext): show aura names for dispel and spellsteal --- src/ui/game_screen.cpp | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 53e615bf..9d0eafe8 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -8398,11 +8398,27 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) { break; } case game::CombatTextEntry::DISPEL: - snprintf(text, sizeof(text), "Dispel"); + if (entry.spellId != 0) { + const std::string& dispelledName = gameHandler.getSpellName(entry.spellId); + if (!dispelledName.empty()) + snprintf(text, sizeof(text), "Dispel %s", dispelledName.c_str()); + else + snprintf(text, sizeof(text), "Dispel"); + } else { + snprintf(text, sizeof(text), "Dispel"); + } color = ImVec4(0.6f, 0.9f, 1.0f, alpha); break; case game::CombatTextEntry::STEAL: - snprintf(text, sizeof(text), "Spellsteal"); + if (entry.spellId != 0) { + const std::string& stolenName = gameHandler.getSpellName(entry.spellId); + if (!stolenName.empty()) + snprintf(text, sizeof(text), "Spellsteal %s", stolenName.c_str()); + else + snprintf(text, sizeof(text), "Spellsteal"); + } else { + snprintf(text, sizeof(text), "Spellsteal"); + } color = ImVec4(0.8f, 0.7f, 1.0f, alpha); break; case game::CombatTextEntry::INTERRUPT: { From 77d53baa0973a7f389cbeb089093d2602e723456 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 23:08:49 -0700 Subject: [PATCH 12/36] fix(combattext): render deflect and reflect miss events --- include/game/spell_defines.hpp | 2 +- src/game/game_handler.cpp | 12 +++++++----- src/ui/game_screen.cpp | 24 ++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/include/game/spell_defines.hpp b/include/game/spell_defines.hpp index 5ddfaa04..cb90a3e2 100644 --- a/include/game/spell_defines.hpp +++ b/include/game/spell_defines.hpp @@ -52,7 +52,7 @@ struct CombatTextEntry { enum Type : uint8_t { MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK, CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL, - ENERGIZE, XP_GAIN, IMMUNE, ABSORB, RESIST, PROC_TRIGGER, + ENERGIZE, XP_GAIN, IMMUNE, ABSORB, RESIST, DEFLECT, REFLECT, PROC_TRIGGER, DISPEL, STEAL, INTERRUPT, INSTAKILL }; Type type; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 475c149b..9667c384 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2723,11 +2723,12 @@ void GameHandler::handlePacket(network::Packet& packet) { CombatTextEntry::BLOCK, // 3=BLOCK CombatTextEntry::MISS, // 4=EVADE CombatTextEntry::IMMUNE, // 5=IMMUNE - CombatTextEntry::MISS, // 6=DEFLECT + CombatTextEntry::DEFLECT, // 6=DEFLECT CombatTextEntry::ABSORB, // 7=ABSORB CombatTextEntry::RESIST, // 8=RESIST }; - CombatTextEntry::Type ct = (missInfo < 9) ? missTypes[missInfo] : CombatTextEntry::MISS; + CombatTextEntry::Type ct = (missInfo < 9) ? missTypes[missInfo] + : (missInfo == 11 ? CombatTextEntry::REFLECT : CombatTextEntry::MISS); if (casterGuid == playerGuid) { // We cast a spell and it missed the target addCombatText(ct, 0, spellId, true, 0, casterGuid, victimGuid); @@ -16386,7 +16387,7 @@ void GameHandler::handleAttackerStateUpdate(network::Packet& packet) { addCombatText(CombatTextEntry::IMMUNE, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); } else if (data.victimState == 7) { // VICTIMSTATE_DEFLECT: Attack was deflected (e.g. shield slam reflect). - addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); + addCombatText(CombatTextEntry::DEFLECT, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); } else { auto type = data.isCrit() ? CombatTextEntry::CRIT_DAMAGE : CombatTextEntry::MELEE_DAMAGE; addCombatText(type, data.totalDamage, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); @@ -16998,7 +16999,7 @@ void GameHandler::handleSpellGo(network::Packet& packet) { CombatTextEntry::BLOCK, // 3=BLOCK CombatTextEntry::MISS, // 4=EVADE CombatTextEntry::IMMUNE, // 5=IMMUNE - CombatTextEntry::MISS, // 6=DEFLECT + CombatTextEntry::DEFLECT, // 6=DEFLECT CombatTextEntry::ABSORB, // 7=ABSORB CombatTextEntry::RESIST, // 8=RESIST }; @@ -17009,7 +17010,8 @@ void GameHandler::handleSpellGo(network::Packet& packet) { if (!playerIsCaster && m.targetGuid != playerGuid) { continue; } - CombatTextEntry::Type ct = (m.missType < 9) ? missTypes[m.missType] : CombatTextEntry::MISS; + CombatTextEntry::Type ct = (m.missType < 9) ? missTypes[m.missType] + : (m.missType == 11 ? CombatTextEntry::REFLECT : CombatTextEntry::MISS); addCombatText(ct, 0, data.spellId, playerIsCaster, 0, spellCasterGuid, m.targetGuid); } } diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 9d0eafe8..f92be623 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -8388,6 +8388,16 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) { snprintf(text, sizeof(text), "Resisted"); color = ImVec4(0.7f, 0.7f, 0.7f, alpha); // Grey for resist break; + case game::CombatTextEntry::DEFLECT: + snprintf(text, sizeof(text), outgoing ? "Deflect" : "You Deflect"); + color = outgoing ? ImVec4(0.7f, 0.7f, 0.7f, alpha) + : ImVec4(0.5f, 0.9f, 1.0f, alpha); + break; + case game::CombatTextEntry::REFLECT: + snprintf(text, sizeof(text), outgoing ? "Reflect" : "Reflected"); + color = outgoing ? ImVec4(0.85f, 0.75f, 1.0f, alpha) + : ImVec4(0.75f, 0.85f, 1.0f, alpha); + break; case game::CombatTextEntry::PROC_TRIGGER: { const std::string& procName = entry.spellId ? gameHandler.getSpellName(entry.spellId) : ""; if (!procName.empty()) @@ -20281,6 +20291,20 @@ void GameScreen::renderCombatLog(game::GameHandler& gameHandler) { snprintf(desc, sizeof(desc), "Resisted"); color = ImVec4(0.6f, 0.6f, 0.9f, 1.0f); break; + case T::DEFLECT: + if (spell) + snprintf(desc, sizeof(desc), "%s deflects %s's %s", tgt, src, spell); + else + snprintf(desc, sizeof(desc), "%s deflects %s's attack", tgt, src); + color = ImVec4(0.65f, 0.8f, 0.95f, 1.0f); + break; + case T::REFLECT: + if (spell) + snprintf(desc, sizeof(desc), "%s reflects %s's %s", tgt, src, spell); + else + snprintf(desc, sizeof(desc), "%s reflects %s's attack", tgt, src); + color = ImVec4(0.8f, 0.7f, 1.0f, 1.0f); + break; case T::ENVIRONMENTAL: snprintf(desc, sizeof(desc), "Environmental damage: %d", e.amount); color = ImVec4(1.0f, 0.5f, 0.2f, 1.0f); From 3f1083e9b5b8c377be19560f6cab2f597ac18909 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 23:15:56 -0700 Subject: [PATCH 13/36] fix(combatlog): consume reflect payload in spell-go miss entries --- include/game/world_packets.hpp | 2 +- src/game/packet_parsers_classic.cpp | 5 +++++ src/game/packet_parsers_tbc.cpp | 7 +++++++ src/game/world_packets.cpp | 12 +++++++++++- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/include/game/world_packets.hpp b/include/game/world_packets.hpp index d864b57e..f29eecb7 100644 --- a/include/game/world_packets.hpp +++ b/include/game/world_packets.hpp @@ -1858,7 +1858,7 @@ public: /** SMSG_SPELL_GO data (simplified) */ struct SpellGoMissEntry { uint64_t targetGuid = 0; - uint8_t missType = 0; // 0=MISS 1=DODGE 2=PARRY 3=BLOCK 4=EVADE 5=IMMUNE 6=DEFLECT 7=ABSORB 8=RESIST + uint8_t missType = 0; // 0=MISS 1=DODGE 2=PARRY 3=BLOCK 4=EVADE 5=IMMUNE 6=DEFLECT 7=ABSORB 8=RESIST 11=REFLECT }; struct SpellGoData { diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 041af211..7077d0ab 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -421,6 +421,11 @@ bool ClassicPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& da m.targetGuid = UpdateObjectParser::readPackedGuid(packet); if (rem() < 1) break; m.missType = packet.readUInt8(); + if (m.missType == 11) { + if (rem() < 5) break; + (void)packet.readUInt32(); + (void)packet.readUInt8(); + } data.missTargets.push_back(m); } // Check if we read all expected misses diff --git a/src/game/packet_parsers_tbc.cpp b/src/game/packet_parsers_tbc.cpp index 83e2511c..d218926a 100644 --- a/src/game/packet_parsers_tbc.cpp +++ b/src/game/packet_parsers_tbc.cpp @@ -1306,6 +1306,13 @@ bool TbcPacketParsers::parseSpellGo(network::Packet& packet, SpellGoData& data) SpellGoMissEntry m; m.targetGuid = packet.readUInt64(); // full GUID in TBC m.missType = packet.readUInt8(); + if (m.missType == 11) { + if (packet.getReadPos() + 5 > packet.getSize()) { + break; + } + (void)packet.readUInt32(); + (void)packet.readUInt8(); + } data.missTargets.push_back(m); } // Check if we read all expected misses diff --git a/src/game/world_packets.cpp b/src/game/world_packets.cpp index 64ae5e00..4c0a0b1e 100644 --- a/src/game/world_packets.cpp +++ b/src/game/world_packets.cpp @@ -3684,7 +3684,8 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { data.missTargets.reserve(data.missCount); for (uint8_t i = 0; i < data.missCount; ++i) { - // Each miss entry: packed GUID(1-8 bytes) + missType(1 byte), validate before reading + // Each miss entry: packed GUID(1-8 bytes) + missType(1 byte). + // REFLECT additionally appends uint32 reflectSpellId + uint8 reflectResult. if (packet.getSize() - packet.getReadPos() < 2) { LOG_WARNING("Spell go: truncated miss targets at index ", (int)i, "/", (int)data.missCount); data.missCount = i; @@ -3693,6 +3694,15 @@ bool SpellGoParser::parse(network::Packet& packet, SpellGoData& data) { SpellGoMissEntry m; m.targetGuid = UpdateObjectParser::readPackedGuid(packet); // packed GUID in WotLK m.missType = (packet.getSize() - packet.getReadPos() >= 1) ? packet.readUInt8() : 0; + if (m.missType == 11) { + if (packet.getSize() - packet.getReadPos() < 5) { + LOG_WARNING("Spell go: truncated reflect payload at miss index ", (int)i, "/", (int)data.missCount); + data.missCount = i; + break; + } + (void)packet.readUInt32(); + (void)packet.readUInt8(); + } data.missTargets.push_back(m); } From 6095170167f05aab95535ad62f186d51492db184 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 23:24:09 -0700 Subject: [PATCH 14/36] fix(combattext): correct reflect miss floating text --- src/ui/game_screen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index f92be623..e51d0eb9 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -8394,7 +8394,7 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) { : ImVec4(0.5f, 0.9f, 1.0f, alpha); break; case game::CombatTextEntry::REFLECT: - snprintf(text, sizeof(text), outgoing ? "Reflect" : "Reflected"); + snprintf(text, sizeof(text), outgoing ? "Reflected" : "You Reflect"); color = outgoing ? ImVec4(0.85f, 0.75f, 1.0f, alpha) : ImVec4(0.75f, 0.85f, 1.0f, alpha); break; From 57265bfa4f2eef08b404cc8136c82edf90797d40 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 23:32:57 -0700 Subject: [PATCH 15/36] fix(combattext): render evade results explicitly --- include/game/spell_defines.hpp | 2 +- src/game/game_handler.cpp | 8 ++++---- src/ui/game_screen.cpp | 12 ++++++++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/include/game/spell_defines.hpp b/include/game/spell_defines.hpp index cb90a3e2..7b1ee689 100644 --- a/include/game/spell_defines.hpp +++ b/include/game/spell_defines.hpp @@ -51,7 +51,7 @@ struct ActionBarSlot { struct CombatTextEntry { enum Type : uint8_t { MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK, - CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL, + EVADE, CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL, ENERGIZE, XP_GAIN, IMMUNE, ABSORB, RESIST, DEFLECT, REFLECT, PROC_TRIGGER, DISPEL, STEAL, INTERRUPT, INSTAKILL }; diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 9667c384..0c229108 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2721,7 +2721,7 @@ void GameHandler::handlePacket(network::Packet& packet) { CombatTextEntry::DODGE, // 1=DODGE CombatTextEntry::PARRY, // 2=PARRY CombatTextEntry::BLOCK, // 3=BLOCK - CombatTextEntry::MISS, // 4=EVADE + CombatTextEntry::EVADE, // 4=EVADE CombatTextEntry::IMMUNE, // 5=IMMUNE CombatTextEntry::DEFLECT, // 6=DEFLECT CombatTextEntry::ABSORB, // 7=ABSORB @@ -16380,8 +16380,8 @@ void GameHandler::handleAttackerStateUpdate(network::Packet& packet) { addCombatText(CombatTextEntry::MELEE_DAMAGE, data.totalDamage, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); addCombatText(CombatTextEntry::BLOCK, static_cast(data.blocked), 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); } else if (data.victimState == 5) { - // VICTIMSTATE_EVADE: NPC evaded (out of combat zone). Show as miss. - addCombatText(CombatTextEntry::MISS, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); + // VICTIMSTATE_EVADE: NPC evaded (out of combat zone). + addCombatText(CombatTextEntry::EVADE, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); } else if (data.victimState == 6) { // VICTIMSTATE_IS_IMMUNE: Target is immune to this attack. addCombatText(CombatTextEntry::IMMUNE, 0, 0, isPlayerAttacker, 0, data.attackerGuid, data.targetGuid); @@ -16997,7 +16997,7 @@ void GameHandler::handleSpellGo(network::Packet& packet) { CombatTextEntry::DODGE, // 1=DODGE CombatTextEntry::PARRY, // 2=PARRY CombatTextEntry::BLOCK, // 3=BLOCK - CombatTextEntry::MISS, // 4=EVADE + CombatTextEntry::EVADE, // 4=EVADE CombatTextEntry::IMMUNE, // 5=IMMUNE CombatTextEntry::DEFLECT, // 6=DEFLECT CombatTextEntry::ABSORB, // 7=ABSORB diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index e51d0eb9..8807afe4 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -8342,6 +8342,11 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) { color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) : ImVec4(0.4f, 0.9f, 1.0f, alpha); break; + case game::CombatTextEntry::EVADE: + snprintf(text, sizeof(text), outgoing ? "Evade" : "You Evade"); + color = outgoing ? ImVec4(0.6f, 0.6f, 0.6f, alpha) + : ImVec4(0.4f, 0.9f, 1.0f, alpha); + break; case game::CombatTextEntry::PERIODIC_DAMAGE: snprintf(text, sizeof(text), "-%d", entry.amount); color = outgoing ? @@ -20262,6 +20267,13 @@ void GameScreen::renderCombatLog(game::GameHandler& gameHandler) { snprintf(desc, sizeof(desc), "%s blocks %s's attack (%d blocked)", tgt, src, e.amount); color = ImVec4(0.65f, 0.75f, 0.65f, 1.0f); break; + case T::EVADE: + if (spell) + snprintf(desc, sizeof(desc), "%s evades %s's %s", tgt, src, spell); + else + snprintf(desc, sizeof(desc), "%s evades %s's attack", tgt, src); + color = ImVec4(0.65f, 0.65f, 0.65f, 1.0f); + break; case T::IMMUNE: if (spell) snprintf(desc, sizeof(desc), "%s is immune to %s", tgt, spell); From 842771cb1014fb50e0250b4e934d46a4e44929ff Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 23:40:39 -0700 Subject: [PATCH 16/36] fix(combatlog): validate tbc spelllogexecute effect GUIDs --- src/game/game_handler.cpp | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 0c229108..5d3dbec4 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6432,9 +6432,12 @@ void GameHandler::handlePacket(network::Packet& packet) { if (effectType == 10) { // SPELL_EFFECT_POWER_DRAIN: packed_guid target + uint32 amount + uint32 powerType + float multiplier for (uint32_t li = 0; li < effectLogCount; ++li) { - if (packet.getSize() - packet.getReadPos() < 1) break; + const size_t guidBytes = exeTbcLike ? 8u : 1u; + if (packet.getSize() - packet.getReadPos() < guidBytes) { + packet.setReadPos(packet.getSize()); break; + } uint64_t drainTarget = exeTbcLike - ? (packet.getSize() - packet.getReadPos() >= 8 ? packet.readUInt64() : 0) + ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (packet.getSize() - packet.getReadPos() < 12) { packet.setReadPos(packet.getSize()); break; } uint32_t drainAmount = packet.readUInt32(); @@ -6454,9 +6457,12 @@ void GameHandler::handlePacket(network::Packet& packet) { } else if (effectType == 11) { // SPELL_EFFECT_HEALTH_LEECH: packed_guid target + uint32 amount + float multiplier for (uint32_t li = 0; li < effectLogCount; ++li) { - if (packet.getSize() - packet.getReadPos() < 1) break; + const size_t guidBytes = exeTbcLike ? 8u : 1u; + if (packet.getSize() - packet.getReadPos() < guidBytes) { + packet.setReadPos(packet.getSize()); break; + } uint64_t leechTarget = exeTbcLike - ? (packet.getSize() - packet.getReadPos() >= 8 ? packet.readUInt64() : 0) + ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (packet.getSize() - packet.getReadPos() < 8) { packet.setReadPos(packet.getSize()); break; } uint32_t leechAmount = packet.readUInt32(); @@ -6496,9 +6502,12 @@ void GameHandler::handlePacket(network::Packet& packet) { } else if (effectType == 26) { // SPELL_EFFECT_INTERRUPT_CAST: packed_guid target + uint32 interrupted_spell_id for (uint32_t li = 0; li < effectLogCount; ++li) { - if (packet.getSize() - packet.getReadPos() < 1) break; + const size_t guidBytes = exeTbcLike ? 8u : 1u; + if (packet.getSize() - packet.getReadPos() < guidBytes) { + packet.setReadPos(packet.getSize()); break; + } uint64_t icTarget = exeTbcLike - ? (packet.getSize() - packet.getReadPos() >= 8 ? packet.readUInt64() : 0) + ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (packet.getSize() - packet.getReadPos() < 4) { packet.setReadPos(packet.getSize()); break; } uint32_t icSpellId = packet.readUInt32(); From e9d2c431916a710b3cff406718823b0cbc0699c3 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 23:47:57 -0700 Subject: [PATCH 17/36] fix(combatlog): validate classic spelllogexecute packed GUIDs --- src/game/game_handler.cpp | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 5d3dbec4..77e76d58 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6411,9 +6411,26 @@ void GameHandler::handlePacket(network::Packet& packet) { // Effect 49 = FEED_PET: uint32 itemEntry // Effect 114= CREATE_ITEM2: uint32 itemEntry (same layout as CREATE_ITEM) const bool exeTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const auto hasFullPackedGuid = [&packet]() -> bool { + 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; + }; if (packet.getSize() - packet.getReadPos() < (exeTbcLike ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } + if (!exeTbcLike && !hasFullPackedGuid()) { + packet.setReadPos(packet.getSize()); break; + } uint64_t exeCaster = exeTbcLike ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (packet.getSize() - packet.getReadPos() < 8) { @@ -6432,8 +6449,8 @@ void GameHandler::handlePacket(network::Packet& packet) { if (effectType == 10) { // SPELL_EFFECT_POWER_DRAIN: packed_guid target + uint32 amount + uint32 powerType + float multiplier for (uint32_t li = 0; li < effectLogCount; ++li) { - const size_t guidBytes = exeTbcLike ? 8u : 1u; - if (packet.getSize() - packet.getReadPos() < guidBytes) { + if (packet.getSize() - packet.getReadPos() < (exeTbcLike ? 8u : 1u) + || (!exeTbcLike && !hasFullPackedGuid())) { packet.setReadPos(packet.getSize()); break; } uint64_t drainTarget = exeTbcLike @@ -6457,8 +6474,8 @@ void GameHandler::handlePacket(network::Packet& packet) { } else if (effectType == 11) { // SPELL_EFFECT_HEALTH_LEECH: packed_guid target + uint32 amount + float multiplier for (uint32_t li = 0; li < effectLogCount; ++li) { - const size_t guidBytes = exeTbcLike ? 8u : 1u; - if (packet.getSize() - packet.getReadPos() < guidBytes) { + if (packet.getSize() - packet.getReadPos() < (exeTbcLike ? 8u : 1u) + || (!exeTbcLike && !hasFullPackedGuid())) { packet.setReadPos(packet.getSize()); break; } uint64_t leechTarget = exeTbcLike @@ -6502,8 +6519,8 @@ void GameHandler::handlePacket(network::Packet& packet) { } else if (effectType == 26) { // SPELL_EFFECT_INTERRUPT_CAST: packed_guid target + uint32 interrupted_spell_id for (uint32_t li = 0; li < effectLogCount; ++li) { - const size_t guidBytes = exeTbcLike ? 8u : 1u; - if (packet.getSize() - packet.getReadPos() < guidBytes) { + if (packet.getSize() - packet.getReadPos() < (exeTbcLike ? 8u : 1u) + || (!exeTbcLike && !hasFullPackedGuid())) { packet.setReadPos(packet.getSize()); break; } uint64_t icTarget = exeTbcLike From 64483a31d5c191ba2ea7718389ddd321ce197a21 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Fri, 13 Mar 2026 23:56:44 -0700 Subject: [PATCH 18/36] fix(combattext): show power drain separately from damage --- include/game/spell_defines.hpp | 4 ++-- src/game/game_handler.cpp | 10 +++++----- src/ui/game_screen.cpp | 17 +++++++++++++++++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/include/game/spell_defines.hpp b/include/game/spell_defines.hpp index 7b1ee689..ffaf6bb2 100644 --- a/include/game/spell_defines.hpp +++ b/include/game/spell_defines.hpp @@ -52,7 +52,7 @@ struct CombatTextEntry { enum Type : uint8_t { MELEE_DAMAGE, SPELL_DAMAGE, HEAL, MISS, DODGE, PARRY, BLOCK, EVADE, CRIT_DAMAGE, CRIT_HEAL, PERIODIC_DAMAGE, PERIODIC_HEAL, ENVIRONMENTAL, - ENERGIZE, XP_GAIN, IMMUNE, ABSORB, RESIST, DEFLECT, REFLECT, PROC_TRIGGER, + ENERGIZE, POWER_DRAIN, XP_GAIN, IMMUNE, ABSORB, RESIST, DEFLECT, REFLECT, PROC_TRIGGER, DISPEL, STEAL, INTERRUPT, INSTAKILL }; Type type; @@ -60,7 +60,7 @@ struct CombatTextEntry { uint32_t spellId = 0; float age = 0.0f; // Seconds since creation (for fadeout) bool isPlayerSource = false; // True if player dealt this - uint8_t powerType = 0; // For ENERGIZE: 0=mana,1=rage,2=focus,3=energy,6=runicpower + uint8_t powerType = 0; // For ENERGIZE/POWER_DRAIN: 0=mana,1=rage,2=focus,3=energy,6=runicpower static constexpr float LIFETIME = 2.5f; bool isExpired() const { return age >= LIFETIME; } diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 77e76d58..a79cdf2f 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3993,13 +3993,12 @@ void GameHandler::handlePacket(network::Packet& packet) { } else if (auraType == 98) { // PERIODIC_MANA_LEECH: miscValue(powerType) + amount + float multiplier if (packet.getSize() - packet.getReadPos() < 12) break; - /*uint32_t powerType =*/ packet.readUInt32(); + uint8_t powerType = static_cast(packet.readUInt32()); uint32_t amount = packet.readUInt32(); /*float multiplier =*/ packet.readUInt32(); // read as raw uint32 (float bits) - // Show as periodic damage from victim's perspective (mana drained) if (isPlayerVictim && amount > 0) - addCombatText(CombatTextEntry::PERIODIC_DAMAGE, static_cast(amount), - spellId, false, 0, casterGuid, victimGuid); + addCombatText(CombatTextEntry::POWER_DRAIN, static_cast(amount), + spellId, false, powerType, casterGuid, victimGuid); } else { // Unknown/untracked aura type — stop parsing this event safely packet.setReadPos(packet.getSize()); @@ -6462,7 +6461,8 @@ void GameHandler::handlePacket(network::Packet& packet) { /*float drainMult =*/ packet.readFloat(); if (drainAmount > 0) { if (drainTarget == playerGuid) - addCombatText(CombatTextEntry::PERIODIC_DAMAGE, static_cast(drainAmount), exeSpellId, false, 0, + addCombatText(CombatTextEntry::POWER_DRAIN, static_cast(drainAmount), exeSpellId, false, + static_cast(drainPower), exeCaster, drainTarget); else if (isPlayerCaster) addCombatText(CombatTextEntry::ENERGIZE, static_cast(drainAmount), exeSpellId, true, diff --git a/src/ui/game_screen.cpp b/src/ui/game_screen.cpp index 8807afe4..788150b6 100644 --- a/src/ui/game_screen.cpp +++ b/src/ui/game_screen.cpp @@ -8371,6 +8371,16 @@ void GameScreen::renderCombatText(game::GameHandler& gameHandler) { default: color = ImVec4(0.3f, 0.6f, 1.0f, alpha); break; // Mana (0): blue } break; + case game::CombatTextEntry::POWER_DRAIN: + snprintf(text, sizeof(text), "-%d", entry.amount); + switch (entry.powerType) { + case 1: color = ImVec4(1.0f, 0.35f, 0.35f, alpha); break; + case 2: color = ImVec4(1.0f, 0.7f, 0.2f, alpha); break; + case 3: color = ImVec4(1.0f, 0.95f, 0.35f, alpha); break; + case 6: color = ImVec4(0.45f, 0.95f, 0.85f, alpha); break; + default: color = ImVec4(0.45f, 0.75f, 1.0f, alpha); break; + } + break; case game::CombatTextEntry::XP_GAIN: snprintf(text, sizeof(text), "+%d XP", entry.amount); color = ImVec4(0.7f, 0.3f, 1.0f, alpha); // Purple for XP @@ -20328,6 +20338,13 @@ void GameScreen::renderCombatLog(game::GameHandler& gameHandler) { snprintf(desc, sizeof(desc), "%s gains %d power", tgt, e.amount); color = ImVec4(0.4f, 0.6f, 1.0f, 1.0f); break; + case T::POWER_DRAIN: + if (spell) + snprintf(desc, sizeof(desc), "%s loses %d power to %s's %s", tgt, e.amount, src, spell); + else + snprintf(desc, sizeof(desc), "%s loses %d power", tgt, e.amount); + color = ImVec4(0.45f, 0.75f, 1.0f, 1.0f); + break; case T::XP_GAIN: snprintf(desc, sizeof(desc), "You gain %d experience", e.amount); color = ImVec4(0.8f, 0.6f, 1.0f, 1.0f); From 209f8db382cb4dcfc0daa13ae0e47e5bde3fe48a Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 00:06:05 -0700 Subject: [PATCH 19/36] fix(combattext): honor power drain multipliers --- src/game/game_handler.cpp | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index a79cdf2f..74e1782c 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3995,10 +3995,18 @@ void GameHandler::handlePacket(network::Packet& packet) { if (packet.getSize() - packet.getReadPos() < 12) break; uint8_t powerType = static_cast(packet.readUInt32()); uint32_t amount = packet.readUInt32(); - /*float multiplier =*/ packet.readUInt32(); // read as raw uint32 (float bits) + float multiplier = packet.readFloat(); if (isPlayerVictim && amount > 0) addCombatText(CombatTextEntry::POWER_DRAIN, static_cast(amount), spellId, false, powerType, casterGuid, victimGuid); + if (isPlayerCaster && amount > 0 && multiplier > 0.0f && std::isfinite(multiplier)) { + const uint32_t gainedAmount = static_cast( + std::lround(static_cast(amount) * static_cast(multiplier))); + if (gainedAmount > 0) { + addCombatText(CombatTextEntry::ENERGIZE, static_cast(gainedAmount), + spellId, true, powerType, casterGuid, casterGuid); + } + } } else { // Unknown/untracked aura type — stop parsing this event safely packet.setReadPos(packet.getSize()); @@ -6458,18 +6466,30 @@ void GameHandler::handlePacket(network::Packet& packet) { if (packet.getSize() - packet.getReadPos() < 12) { packet.setReadPos(packet.getSize()); break; } uint32_t drainAmount = packet.readUInt32(); uint32_t drainPower = packet.readUInt32(); // 0=mana,1=rage,3=energy,6=runic - /*float drainMult =*/ packet.readFloat(); + float drainMult = packet.readFloat(); if (drainAmount > 0) { if (drainTarget == playerGuid) addCombatText(CombatTextEntry::POWER_DRAIN, static_cast(drainAmount), exeSpellId, false, static_cast(drainPower), exeCaster, drainTarget); - else if (isPlayerCaster) - addCombatText(CombatTextEntry::ENERGIZE, static_cast(drainAmount), exeSpellId, true, - static_cast(drainPower), exeCaster, exeCaster); + if (isPlayerCaster) { + if (drainTarget != playerGuid) { + addCombatText(CombatTextEntry::POWER_DRAIN, static_cast(drainAmount), exeSpellId, true, + static_cast(drainPower), exeCaster, drainTarget); + } + if (drainMult > 0.0f && std::isfinite(drainMult)) { + const uint32_t gainedAmount = static_cast( + std::lround(static_cast(drainAmount) * static_cast(drainMult))); + if (gainedAmount > 0) { + addCombatText(CombatTextEntry::ENERGIZE, static_cast(gainedAmount), exeSpellId, true, + static_cast(drainPower), exeCaster, exeCaster); + } + } + } } LOG_DEBUG("SMSG_SPELLLOGEXECUTE POWER_DRAIN: spell=", exeSpellId, - " power=", drainPower, " amount=", drainAmount); + " power=", drainPower, " amount=", drainAmount, + " multiplier=", drainMult); } } else if (effectType == 11) { // SPELL_EFFECT_HEALTH_LEECH: packed_guid target + uint32 amount + float multiplier From a147347393a3fad09f76f7592ed0c9499b25b3af Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 00:16:28 -0700 Subject: [PATCH 20/36] fix(combattext): honor health leech multipliers --- src/game/game_handler.cpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 74e1782c..fa82fd09 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6503,16 +6503,26 @@ void GameHandler::handlePacket(network::Packet& packet) { : UpdateObjectParser::readPackedGuid(packet); if (packet.getSize() - packet.getReadPos() < 8) { packet.setReadPos(packet.getSize()); break; } uint32_t leechAmount = packet.readUInt32(); - /*float leechMult =*/ packet.readFloat(); + float leechMult = packet.readFloat(); if (leechAmount > 0) { - if (leechTarget == playerGuid) + if (leechTarget == playerGuid) { addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast(leechAmount), exeSpellId, false, 0, exeCaster, leechTarget); - else if (isPlayerCaster) - addCombatText(CombatTextEntry::HEAL, static_cast(leechAmount), exeSpellId, true, 0, - exeCaster, exeCaster); + } else if (isPlayerCaster) { + addCombatText(CombatTextEntry::SPELL_DAMAGE, static_cast(leechAmount), exeSpellId, true, 0, + exeCaster, leechTarget); + } + if (isPlayerCaster && leechMult > 0.0f && std::isfinite(leechMult)) { + const uint32_t gainedAmount = static_cast( + std::lround(static_cast(leechAmount) * static_cast(leechMult))); + if (gainedAmount > 0) { + addCombatText(CombatTextEntry::HEAL, static_cast(gainedAmount), exeSpellId, true, 0, + exeCaster, exeCaster); + } + } } - LOG_DEBUG("SMSG_SPELLLOGEXECUTE HEALTH_LEECH: spell=", exeSpellId, " amount=", leechAmount); + LOG_DEBUG("SMSG_SPELLLOGEXECUTE HEALTH_LEECH: spell=", exeSpellId, + " amount=", leechAmount, " multiplier=", leechMult); } } else if (effectType == 24 || effectType == 114) { // SPELL_EFFECT_CREATE_ITEM / CREATE_ITEM2: uint32 itemEntry per log entry From ed5134d60115ceab56b5ffbf8682e10c192a8e94 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 00:24:21 -0700 Subject: [PATCH 21/36] fix(combatlog): parse classic spelllogexecute GUIDs as packed --- src/game/game_handler.cpp | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index fa82fd09..ca3b453c 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6408,8 +6408,8 @@ void GameHandler::handlePacket(network::Packet& packet) { break; } case Opcode::SMSG_SPELLLOGEXECUTE: { - // WotLK: packed_guid caster + uint32 spellId + uint32 effectCount - // TBC/Classic: uint64 caster + uint32 spellId + uint32 effectCount + // WotLK/Classic/Turtle: packed_guid caster + uint32 spellId + uint32 effectCount + // TBC: uint64 caster + uint32 spellId + uint32 effectCount // Per-effect: uint8 effectType + uint32 effectLogCount + effect-specific data // Effect 10 = POWER_DRAIN: packed_guid target + uint32 amount + uint32 powerType + float multiplier // Effect 11 = HEALTH_LEECH: packed_guid target + uint32 amount + float multiplier @@ -6417,7 +6417,7 @@ void GameHandler::handlePacket(network::Packet& packet) { // Effect 26 = INTERRUPT_CAST: packed_guid target + uint32 interrupted_spell_id // Effect 49 = FEED_PET: uint32 itemEntry // Effect 114= CREATE_ITEM2: uint32 itemEntry (same layout as CREATE_ITEM) - const bool exeTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool exeUsesFullGuid = isActiveExpansion("tbc"); const auto hasFullPackedGuid = [&packet]() -> bool { if (packet.getReadPos() >= packet.getSize()) { return false; @@ -6432,13 +6432,13 @@ void GameHandler::handlePacket(network::Packet& packet) { } return packet.getSize() - packet.getReadPos() >= guidBytes; }; - if (packet.getSize() - packet.getReadPos() < (exeTbcLike ? 8u : 1u)) { + if (packet.getSize() - packet.getReadPos() < (exeUsesFullGuid ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } - if (!exeTbcLike && !hasFullPackedGuid()) { + if (!exeUsesFullGuid && !hasFullPackedGuid()) { packet.setReadPos(packet.getSize()); break; } - uint64_t exeCaster = exeTbcLike + uint64_t exeCaster = exeUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (packet.getSize() - packet.getReadPos() < 8) { packet.setReadPos(packet.getSize()); break; @@ -6456,11 +6456,11 @@ void GameHandler::handlePacket(network::Packet& packet) { if (effectType == 10) { // SPELL_EFFECT_POWER_DRAIN: packed_guid target + uint32 amount + uint32 powerType + float multiplier for (uint32_t li = 0; li < effectLogCount; ++li) { - if (packet.getSize() - packet.getReadPos() < (exeTbcLike ? 8u : 1u) - || (!exeTbcLike && !hasFullPackedGuid())) { + if (packet.getSize() - packet.getReadPos() < (exeUsesFullGuid ? 8u : 1u) + || (!exeUsesFullGuid && !hasFullPackedGuid())) { packet.setReadPos(packet.getSize()); break; } - uint64_t drainTarget = exeTbcLike + uint64_t drainTarget = exeUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (packet.getSize() - packet.getReadPos() < 12) { packet.setReadPos(packet.getSize()); break; } @@ -6494,11 +6494,11 @@ void GameHandler::handlePacket(network::Packet& packet) { } else if (effectType == 11) { // SPELL_EFFECT_HEALTH_LEECH: packed_guid target + uint32 amount + float multiplier for (uint32_t li = 0; li < effectLogCount; ++li) { - if (packet.getSize() - packet.getReadPos() < (exeTbcLike ? 8u : 1u) - || (!exeTbcLike && !hasFullPackedGuid())) { + if (packet.getSize() - packet.getReadPos() < (exeUsesFullGuid ? 8u : 1u) + || (!exeUsesFullGuid && !hasFullPackedGuid())) { packet.setReadPos(packet.getSize()); break; } - uint64_t leechTarget = exeTbcLike + uint64_t leechTarget = exeUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (packet.getSize() - packet.getReadPos() < 8) { packet.setReadPos(packet.getSize()); break; } @@ -6549,11 +6549,11 @@ void GameHandler::handlePacket(network::Packet& packet) { } else if (effectType == 26) { // SPELL_EFFECT_INTERRUPT_CAST: packed_guid target + uint32 interrupted_spell_id for (uint32_t li = 0; li < effectLogCount; ++li) { - if (packet.getSize() - packet.getReadPos() < (exeTbcLike ? 8u : 1u) - || (!exeTbcLike && !hasFullPackedGuid())) { + if (packet.getSize() - packet.getReadPos() < (exeUsesFullGuid ? 8u : 1u) + || (!exeUsesFullGuid && !hasFullPackedGuid())) { packet.setReadPos(packet.getSize()); break; } - uint64_t icTarget = exeTbcLike + uint64_t icTarget = exeUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (packet.getSize() - packet.getReadPos() < 4) { packet.setReadPos(packet.getSize()); break; } From 9c3b5d17cf97aa7eeb540dd939b7b75874aa02f4 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 00:31:35 -0700 Subject: [PATCH 22/36] fix(combatlog): parse classic resist log GUIDs as packed --- src/game/game_handler.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index ca3b453c..ff594ad6 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -7002,19 +7002,19 @@ void GameHandler::handlePacket(network::Packet& packet) { // ---- Resistance/combat log ---- case Opcode::SMSG_RESISTLOG: { - // WotLK: uint32 hitInfo + packed_guid attacker + packed_guid victim + uint32 spellId - // + float resistFactor + uint32 targetRes + uint32 resistedValue + ... - // TBC/Classic: same but full uint64 GUIDs + // WotLK/Classic/Turtle: uint32 hitInfo + packed_guid attacker + packed_guid victim + uint32 spellId + // + float resistFactor + uint32 targetRes + uint32 resistedValue + ... + // TBC: same layout but full uint64 GUIDs // Show RESIST combat text when player resists an incoming spell. - const bool rlTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); + const bool rlUsesFullGuid = isActiveExpansion("tbc"); auto rl_rem = [&]() { return packet.getSize() - packet.getReadPos(); }; if (rl_rem() < 4) { packet.setReadPos(packet.getSize()); break; } /*uint32_t hitInfo =*/ packet.readUInt32(); - if (rl_rem() < (rlTbcLike ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } - uint64_t attackerGuid = rlTbcLike + if (rl_rem() < (rlUsesFullGuid ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } + uint64_t attackerGuid = rlUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (rl_rem() < (rlTbcLike ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } - uint64_t victimGuid = rlTbcLike + if (rl_rem() < (rlUsesFullGuid ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } + uint64_t victimGuid = rlUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (rl_rem() < 4) { packet.setReadPos(packet.getSize()); break; } uint32_t spellId = packet.readUInt32(); From 8ba5ca5337b5570ee75b9551277d7070cf3ca57c Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 00:38:22 -0700 Subject: [PATCH 23/36] fix(combatlog): parse classic instakill log GUIDs as packed --- src/game/game_handler.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index ff594ad6..f7937079 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6384,15 +6384,15 @@ void GameHandler::handlePacket(network::Packet& packet) { } case Opcode::SMSG_SPELLINSTAKILLLOG: { // Sent when a unit is killed by a spell with SPELL_ATTR_EX2_INSTAKILL (e.g. Execute, Obliterate, etc.) - // WotLK: packed_guid caster + packed_guid victim + uint32 spellId - // TBC/Classic: full uint64 caster + full uint64 victim + uint32 spellId - const bool ikTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); + // WotLK/Classic/Turtle: packed_guid caster + packed_guid victim + uint32 spellId + // TBC: full uint64 caster + full uint64 victim + uint32 spellId + const bool ikUsesFullGuid = isActiveExpansion("tbc"); auto ik_rem = [&]() { return packet.getSize() - packet.getReadPos(); }; - if (ik_rem() < (ikTbcLike ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } - uint64_t ikCaster = ikTbcLike + if (ik_rem() < (ikUsesFullGuid ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } + uint64_t ikCaster = ikUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (ik_rem() < (ikTbcLike ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } - uint64_t ikVictim = ikTbcLike + if (ik_rem() < (ikUsesFullGuid ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } + uint64_t ikVictim = ikUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); uint32_t ikSpell = (ik_rem() >= 4) ? packet.readUInt32() : 0; // Show kill/death feedback for the local player From fd8ea4e69e026b52b2ae00c19a41d17ff98988cd Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 00:45:50 -0700 Subject: [PATCH 24/36] fix(combatlog): parse classic proc log GUIDs as packed --- src/game/game_handler.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index f7937079..beba372d 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2086,17 +2086,17 @@ void GameHandler::handlePacket(network::Packet& packet) { // ---- Spell proc resist log ---- case Opcode::SMSG_PROCRESIST: { - // WotLK: packed_guid caster + packed_guid victim + uint32 spellId + ... - // TBC/Classic: uint64 caster + uint64 victim + uint32 spellId + ... - const bool prTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); + // WotLK/Classic/Turtle: packed_guid caster + packed_guid victim + uint32 spellId + ... + // TBC: uint64 caster + uint64 victim + uint32 spellId + ... + const bool prUsesFullGuid = isActiveExpansion("tbc"); auto readPrGuid = [&]() -> uint64_t { - if (prTbcLike) + if (prUsesFullGuid) return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0; return UpdateObjectParser::readPackedGuid(packet); }; - if (packet.getSize() - packet.getReadPos() < (prTbcLike ? 8u : 1u)) break; + if (packet.getSize() - packet.getReadPos() < (prUsesFullGuid ? 8u : 1u)) break; uint64_t caster = readPrGuid(); - if (packet.getSize() - packet.getReadPos() < (prTbcLike ? 8u : 1u)) break; + if (packet.getSize() - packet.getReadPos() < (prUsesFullGuid ? 8u : 1u)) break; uint64_t victim = readPrGuid(); if (packet.getSize() - packet.getReadPos() < 4) break; uint32_t spellId = packet.readUInt32(); @@ -6355,19 +6355,19 @@ void GameHandler::handlePacket(network::Packet& packet) { break; } case Opcode::SMSG_SPELL_CHANCE_PROC_LOG: { - // WotLK: packed_guid target + packed_guid caster + uint32 spellId + ... - // TBC/Classic: uint64 target + uint64 caster + uint32 spellId + ... - const bool procChanceTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); + // WotLK/Classic/Turtle: packed_guid target + packed_guid caster + uint32 spellId + ... + // TBC: uint64 target + uint64 caster + uint32 spellId + ... + const bool procChanceUsesFullGuid = isActiveExpansion("tbc"); auto readProcChanceGuid = [&]() -> uint64_t { - if (procChanceTbcLike) + if (procChanceUsesFullGuid) return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0; return UpdateObjectParser::readPackedGuid(packet); }; - if (packet.getSize() - packet.getReadPos() < (procChanceTbcLike ? 8u : 1u)) { + if (packet.getSize() - packet.getReadPos() < (procChanceUsesFullGuid ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } uint64_t procTargetGuid = readProcChanceGuid(); - if (packet.getSize() - packet.getReadPos() < (procChanceTbcLike ? 8u : 1u)) { + if (packet.getSize() - packet.getReadPos() < (procChanceUsesFullGuid ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } uint64_t procCasterGuid = readProcChanceGuid(); From 1fa2cbc64e1713d73387c9b0dd917f72d7cfe16d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 00:53:42 -0700 Subject: [PATCH 25/36] fix(combatlog): parse classic dispel and spellsteal GUIDs as packed --- src/game/game_handler.cpp | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index beba372d..a43e4c94 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6202,17 +6202,17 @@ void GameHandler::handlePacket(network::Packet& packet) { break; } case Opcode::SMSG_SPELLDISPELLOG: { - // WotLK: packed casterGuid + packed victimGuid + uint32 dispelSpell + uint8 isStolen - // TBC/Classic: full uint64 casterGuid + full uint64 victimGuid + ... + // WotLK/Classic/Turtle: packed casterGuid + packed victimGuid + uint32 dispelSpell + uint8 isStolen + // TBC: full uint64 casterGuid + full uint64 victimGuid + ... // + uint32 count + count × (uint32 dispelled_spellId + uint32 unk) - const bool dispelTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (dispelTbcLike ? 8u : 2u)) { + const bool dispelUsesFullGuid = isActiveExpansion("tbc"); + if (packet.getSize() - packet.getReadPos() < (dispelUsesFullGuid ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } - uint64_t casterGuid = dispelTbcLike + uint64_t casterGuid = dispelUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < (dispelTbcLike ? 8u : 2u)) break; - uint64_t victimGuid = dispelTbcLike + if (packet.getSize() - packet.getReadPos() < (dispelUsesFullGuid ? 8u : 1u)) break; + uint64_t victimGuid = dispelUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (packet.getSize() - packet.getReadPos() < 9) break; /*uint32_t dispelSpell =*/ packet.readUInt32(); @@ -6220,12 +6220,12 @@ void GameHandler::handlePacket(network::Packet& packet) { uint32_t count = packet.readUInt32(); // Preserve every dispelled aura in the combat log instead of collapsing // multi-aura packets down to the first entry only. - const size_t dispelEntrySize = dispelTbcLike ? 8u : 5u; + const size_t dispelEntrySize = dispelUsesFullGuid ? 8u : 5u; std::vector dispelledIds; dispelledIds.reserve(count); for (uint32_t i = 0; i < count && packet.getSize() - packet.getReadPos() >= dispelEntrySize; ++i) { uint32_t dispelledId = packet.readUInt32(); - if (dispelTbcLike) { + if (dispelUsesFullGuid) { /*uint32_t unk =*/ packet.readUInt32(); } else { /*uint8_t isPositive =*/ packet.readUInt8(); @@ -6288,19 +6288,19 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_SPELLSTEALLOG: { // Sent to the CASTER (Mage) when Spellsteal succeeds. // Wire format mirrors SPELLDISPELLOG: - // WotLK: packed victim + packed caster + uint32 spellId + uint8 isStolen + uint32 count - // + count × (uint32 stolenSpellId + uint8 isPositive) - // TBC/Classic: full uint64 victim + full uint64 caster + same tail - const bool stealTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (stealTbcLike ? 8u : 2u)) { + // WotLK/Classic/Turtle: packed victim + packed caster + uint32 spellId + uint8 isStolen + uint32 count + // + count × (uint32 stolenSpellId + uint8 isPositive) + // TBC: full uint64 victim + full uint64 caster + same tail + const bool stealUsesFullGuid = isActiveExpansion("tbc"); + if (packet.getSize() - packet.getReadPos() < (stealUsesFullGuid ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } - uint64_t stealVictim = stealTbcLike + uint64_t stealVictim = stealUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < (stealTbcLike ? 8u : 2u)) { + if (packet.getSize() - packet.getReadPos() < (stealUsesFullGuid ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } - uint64_t stealCaster = stealTbcLike + uint64_t stealCaster = stealUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (packet.getSize() - packet.getReadPos() < 9) { packet.setReadPos(packet.getSize()); break; @@ -6309,12 +6309,12 @@ void GameHandler::handlePacket(network::Packet& packet) { /*uint8_t isStolen =*/ packet.readUInt8(); uint32_t stealCount = packet.readUInt32(); // Preserve every stolen aura in the combat log instead of only the first. - const size_t stealEntrySize = stealTbcLike ? 8u : 5u; + const size_t stealEntrySize = stealUsesFullGuid ? 8u : 5u; std::vector stolenIds; stolenIds.reserve(stealCount); for (uint32_t i = 0; i < stealCount && packet.getSize() - packet.getReadPos() >= stealEntrySize; ++i) { uint32_t stolenId = packet.readUInt32(); - if (stealTbcLike) { + if (stealUsesFullGuid) { /*uint32_t unk =*/ packet.readUInt32(); } else { /*uint8_t isPos =*/ packet.readUInt8(); From bd8c46fa49c702b4f529fd5e939f81eb76285695 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 01:00:56 -0700 Subject: [PATCH 26/36] fix(combatlog): parse classic dispel failed GUIDs as packed --- src/game/game_handler.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index a43e4c94..d528eca5 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -3239,12 +3239,14 @@ void GameHandler::handlePacket(network::Packet& packet) { case Opcode::SMSG_DISPEL_FAILED: { // WotLK: uint32 dispelSpellId + packed_guid caster + packed_guid victim // [+ count × uint32 failedSpellId] - // TBC/Classic: uint64 caster + uint64 victim + uint32 spellId + // Classic: uint32 dispelSpellId + packed_guid caster + packed_guid victim // [+ count × uint32 failedSpellId] - const bool dispelTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); + // TBC: uint64 caster + uint64 victim + uint32 spellId + // [+ count × uint32 failedSpellId] + const bool dispelUsesFullGuid = isActiveExpansion("tbc"); uint32_t dispelSpellId = 0; uint64_t dispelCasterGuid = 0; - if (dispelTbcLike) { + if (dispelUsesFullGuid) { if (packet.getSize() - packet.getReadPos() < 20) break; dispelCasterGuid = packet.readUInt64(); /*uint64_t victim =*/ packet.readUInt64(); From dbdc45a8a9a9034a84920b9080c6924998db191b Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 01:10:43 -0700 Subject: [PATCH 27/36] fix(combatlog): validate packed dispel-family GUIDs --- src/game/game_handler.cpp | 60 +++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index d528eca5..a3275997 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -100,6 +100,22 @@ bool envFlagEnabled(const char* key, bool defaultValue = false) { raw[0] == 'n' || raw[0] == 'N'); } +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 formatCopperAmount(uint32_t amount) { uint32_t gold = amount / 10000; uint32_t silver = (amount / 100) % 100; @@ -3254,9 +3270,13 @@ void GameHandler::handlePacket(network::Packet& packet) { } else { if (packet.getSize() - packet.getReadPos() < 4) break; dispelSpellId = packet.readUInt32(); - if (packet.getSize() - packet.getReadPos() < 1) break; + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(packet.getSize()); break; + } dispelCasterGuid = UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 1) break; + if (!hasFullPackedGuid(packet)) { + packet.setReadPos(packet.getSize()); break; + } /*uint64_t victim =*/ UpdateObjectParser::readPackedGuid(packet); } // Only show failure to the player who attempted the dispel @@ -6208,12 +6228,16 @@ void GameHandler::handlePacket(network::Packet& packet) { // TBC: full uint64 casterGuid + full uint64 victimGuid + ... // + uint32 count + count × (uint32 dispelled_spellId + uint32 unk) const bool dispelUsesFullGuid = isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (dispelUsesFullGuid ? 8u : 1u)) { + if (packet.getSize() - packet.getReadPos() < (dispelUsesFullGuid ? 8u : 1u) + || (!dispelUsesFullGuid && !hasFullPackedGuid(packet))) { packet.setReadPos(packet.getSize()); break; } uint64_t casterGuid = dispelUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < (dispelUsesFullGuid ? 8u : 1u)) break; + if (packet.getSize() - packet.getReadPos() < (dispelUsesFullGuid ? 8u : 1u) + || (!dispelUsesFullGuid && !hasFullPackedGuid(packet))) { + packet.setReadPos(packet.getSize()); break; + } uint64_t victimGuid = dispelUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (packet.getSize() - packet.getReadPos() < 9) break; @@ -6294,12 +6318,14 @@ void GameHandler::handlePacket(network::Packet& packet) { // + count × (uint32 stolenSpellId + uint8 isPositive) // TBC: full uint64 victim + full uint64 caster + same tail const bool stealUsesFullGuid = isActiveExpansion("tbc"); - if (packet.getSize() - packet.getReadPos() < (stealUsesFullGuid ? 8u : 1u)) { + if (packet.getSize() - packet.getReadPos() < (stealUsesFullGuid ? 8u : 1u) + || (!stealUsesFullGuid && !hasFullPackedGuid(packet))) { packet.setReadPos(packet.getSize()); break; } uint64_t stealVictim = stealUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < (stealUsesFullGuid ? 8u : 1u)) { + if (packet.getSize() - packet.getReadPos() < (stealUsesFullGuid ? 8u : 1u) + || (!stealUsesFullGuid && !hasFullPackedGuid(packet))) { packet.setReadPos(packet.getSize()); break; } uint64_t stealCaster = stealUsesFullGuid @@ -6420,24 +6446,10 @@ void GameHandler::handlePacket(network::Packet& packet) { // Effect 49 = FEED_PET: uint32 itemEntry // Effect 114= CREATE_ITEM2: uint32 itemEntry (same layout as CREATE_ITEM) const bool exeUsesFullGuid = isActiveExpansion("tbc"); - const auto hasFullPackedGuid = [&packet]() -> bool { - 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; - }; if (packet.getSize() - packet.getReadPos() < (exeUsesFullGuid ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } - if (!exeUsesFullGuid && !hasFullPackedGuid()) { + if (!exeUsesFullGuid && !hasFullPackedGuid(packet)) { packet.setReadPos(packet.getSize()); break; } uint64_t exeCaster = exeUsesFullGuid @@ -6459,7 +6471,7 @@ void GameHandler::handlePacket(network::Packet& packet) { // SPELL_EFFECT_POWER_DRAIN: packed_guid target + uint32 amount + uint32 powerType + float multiplier for (uint32_t li = 0; li < effectLogCount; ++li) { if (packet.getSize() - packet.getReadPos() < (exeUsesFullGuid ? 8u : 1u) - || (!exeUsesFullGuid && !hasFullPackedGuid())) { + || (!exeUsesFullGuid && !hasFullPackedGuid(packet))) { packet.setReadPos(packet.getSize()); break; } uint64_t drainTarget = exeUsesFullGuid @@ -6497,7 +6509,7 @@ void GameHandler::handlePacket(network::Packet& packet) { // SPELL_EFFECT_HEALTH_LEECH: packed_guid target + uint32 amount + float multiplier for (uint32_t li = 0; li < effectLogCount; ++li) { if (packet.getSize() - packet.getReadPos() < (exeUsesFullGuid ? 8u : 1u) - || (!exeUsesFullGuid && !hasFullPackedGuid())) { + || (!exeUsesFullGuid && !hasFullPackedGuid(packet))) { packet.setReadPos(packet.getSize()); break; } uint64_t leechTarget = exeUsesFullGuid @@ -6552,7 +6564,7 @@ void GameHandler::handlePacket(network::Packet& packet) { // SPELL_EFFECT_INTERRUPT_CAST: packed_guid target + uint32 interrupted_spell_id for (uint32_t li = 0; li < effectLogCount; ++li) { if (packet.getSize() - packet.getReadPos() < (exeUsesFullGuid ? 8u : 1u) - || (!exeUsesFullGuid && !hasFullPackedGuid())) { + || (!exeUsesFullGuid && !hasFullPackedGuid(packet))) { packet.setReadPos(packet.getSize()); break; } uint64_t icTarget = exeUsesFullGuid From 0fc887a3d249b98c1a6cc37c91e6b95ce29b04be Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 01:18:28 -0700 Subject: [PATCH 28/36] fix(combatlog): validate packed proc log GUIDs --- src/game/game_handler.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index a3275997..b8db9c3a 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2110,9 +2110,15 @@ void GameHandler::handlePacket(network::Packet& packet) { return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0; return UpdateObjectParser::readPackedGuid(packet); }; - if (packet.getSize() - packet.getReadPos() < (prUsesFullGuid ? 8u : 1u)) break; + if (packet.getSize() - packet.getReadPos() < (prUsesFullGuid ? 8u : 1u) + || (!prUsesFullGuid && !hasFullPackedGuid(packet))) { + packet.setReadPos(packet.getSize()); break; + } uint64_t caster = readPrGuid(); - if (packet.getSize() - packet.getReadPos() < (prUsesFullGuid ? 8u : 1u)) break; + if (packet.getSize() - packet.getReadPos() < (prUsesFullGuid ? 8u : 1u) + || (!prUsesFullGuid && !hasFullPackedGuid(packet))) { + packet.setReadPos(packet.getSize()); break; + } uint64_t victim = readPrGuid(); if (packet.getSize() - packet.getReadPos() < 4) break; uint32_t spellId = packet.readUInt32(); @@ -6391,11 +6397,13 @@ void GameHandler::handlePacket(network::Packet& packet) { return (packet.getSize() - packet.getReadPos() >= 8) ? packet.readUInt64() : 0; return UpdateObjectParser::readPackedGuid(packet); }; - if (packet.getSize() - packet.getReadPos() < (procChanceUsesFullGuid ? 8u : 1u)) { + if (packet.getSize() - packet.getReadPos() < (procChanceUsesFullGuid ? 8u : 1u) + || (!procChanceUsesFullGuid && !hasFullPackedGuid(packet))) { packet.setReadPos(packet.getSize()); break; } uint64_t procTargetGuid = readProcChanceGuid(); - if (packet.getSize() - packet.getReadPos() < (procChanceUsesFullGuid ? 8u : 1u)) { + if (packet.getSize() - packet.getReadPos() < (procChanceUsesFullGuid ? 8u : 1u) + || (!procChanceUsesFullGuid && !hasFullPackedGuid(packet))) { packet.setReadPos(packet.getSize()); break; } uint64_t procCasterGuid = readProcChanceGuid(); From a48f6d1044a3bdb8d720e8cf04aa21526bb03175 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 01:25:47 -0700 Subject: [PATCH 29/36] fix(combatlog): parse classic immune log GUIDs as packed --- src/game/game_handler.cpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index b8db9c3a..fee5dd0c 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6206,17 +6206,23 @@ void GameHandler::handlePacket(network::Packet& packet) { packet.setReadPos(packet.getSize()); break; case Opcode::SMSG_SPELLORDAMAGE_IMMUNE: { - // WotLK: packed casterGuid + packed victimGuid + uint32 spellId + uint8 saveType - // TBC/Classic: full uint64 casterGuid + full uint64 victimGuid + uint32 + uint8 - const bool immuneTbcLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - const size_t minSz = immuneTbcLike ? 21u : 2u; + // WotLK/Classic/Turtle: packed casterGuid + packed victimGuid + uint32 spellId + uint8 saveType + // TBC: full uint64 casterGuid + full uint64 victimGuid + uint32 + uint8 + const bool immuneUsesFullGuid = isActiveExpansion("tbc"); + const size_t minSz = immuneUsesFullGuid ? 21u : 2u; if (packet.getSize() - packet.getReadPos() < minSz) { packet.setReadPos(packet.getSize()); break; } - uint64_t casterGuid = immuneTbcLike + if (!immuneUsesFullGuid && !hasFullPackedGuid(packet)) { + packet.setReadPos(packet.getSize()); break; + } + uint64_t casterGuid = immuneUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < (immuneTbcLike ? 8u : 2u)) break; - uint64_t victimGuid = immuneTbcLike + if (packet.getSize() - packet.getReadPos() < (immuneUsesFullGuid ? 8u : 2u) + || (!immuneUsesFullGuid && !hasFullPackedGuid(packet))) { + packet.setReadPos(packet.getSize()); break; + } + uint64_t victimGuid = immuneUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (packet.getSize() - packet.getReadPos() < 5) break; uint32_t immuneSpellId = packet.readUInt32(); From 0968a11234fa837e1e0403131638162b845662df Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 01:32:45 -0700 Subject: [PATCH 30/36] fix(combatlog): validate packed instakill GUIDs --- src/game/game_handler.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index fee5dd0c..0b849230 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6430,10 +6430,16 @@ void GameHandler::handlePacket(network::Packet& packet) { // TBC: full uint64 caster + full uint64 victim + uint32 spellId const bool ikUsesFullGuid = isActiveExpansion("tbc"); auto ik_rem = [&]() { return packet.getSize() - packet.getReadPos(); }; - if (ik_rem() < (ikUsesFullGuid ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } + if (ik_rem() < (ikUsesFullGuid ? 8u : 1u) + || (!ikUsesFullGuid && !hasFullPackedGuid(packet))) { + packet.setReadPos(packet.getSize()); break; + } uint64_t ikCaster = ikUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (ik_rem() < (ikUsesFullGuid ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } + if (ik_rem() < (ikUsesFullGuid ? 8u : 1u) + || (!ikUsesFullGuid && !hasFullPackedGuid(packet))) { + packet.setReadPos(packet.getSize()); break; + } uint64_t ikVictim = ikUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); uint32_t ikSpell = (ik_rem() >= 4) ? packet.readUInt32() : 0; From 468880e2c89a170d839b44aeba68b5b73473dc3d Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 01:39:53 -0700 Subject: [PATCH 31/36] fix(combatlog): validate packed resist log GUIDs --- src/game/game_handler.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 0b849230..54ca6119 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -7044,10 +7044,16 @@ void GameHandler::handlePacket(network::Packet& packet) { auto rl_rem = [&]() { return packet.getSize() - packet.getReadPos(); }; if (rl_rem() < 4) { packet.setReadPos(packet.getSize()); break; } /*uint32_t hitInfo =*/ packet.readUInt32(); - if (rl_rem() < (rlUsesFullGuid ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } + if (rl_rem() < (rlUsesFullGuid ? 8u : 1u) + || (!rlUsesFullGuid && !hasFullPackedGuid(packet))) { + packet.setReadPos(packet.getSize()); break; + } uint64_t attackerGuid = rlUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (rl_rem() < (rlUsesFullGuid ? 8u : 1u)) { packet.setReadPos(packet.getSize()); break; } + if (rl_rem() < (rlUsesFullGuid ? 8u : 1u) + || (!rlUsesFullGuid && !hasFullPackedGuid(packet))) { + packet.setReadPos(packet.getSize()); break; + } uint64_t victimGuid = rlUsesFullGuid ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (rl_rem() < 4) { packet.setReadPos(packet.getSize()); break; } From b059bbcf891b17fdf1d20209465263681bdb2ed7 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 01:47:06 -0700 Subject: [PATCH 32/36] fix(combatlog): parse classic spell damage shield GUIDs as packed --- src/game/game_handler.cpp | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 54ca6119..844bafd6 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6170,23 +6170,32 @@ void GameHandler::handlePacket(network::Packet& packet) { // ---- Spell combat logs (consume) ---- case Opcode::SMSG_SPELLDAMAGESHIELD: { - // Classic/TBC: uint64 victim + uint64 caster + spellId(4) + damage(4) + schoolMask(4) - // WotLK: packed_guid victim + packed_guid caster + spellId(4) + damage(4) + absorbed(4) + schoolMask(4) - const bool shieldClassicLike = isClassicLikeExpansion() || isActiveExpansion("tbc"); - const size_t shieldMinSz = shieldClassicLike ? 24u : 2u; + // Classic: packed_guid victim + packed_guid caster + spellId(4) + damage(4) + schoolMask(4) + // TBC: uint64 victim + uint64 caster + spellId(4) + damage(4) + schoolMask(4) + // WotLK: packed_guid victim + packed_guid caster + spellId(4) + damage(4) + absorbed(4) + schoolMask(4) + const bool shieldTbc = isActiveExpansion("tbc"); + const bool shieldWotlkLike = !isClassicLikeExpansion() && !shieldTbc; + const size_t shieldMinSz = shieldTbc ? 24u : 2u; if (packet.getSize() - packet.getReadPos() < shieldMinSz) { packet.setReadPos(packet.getSize()); break; } - uint64_t victimGuid = shieldClassicLike + if (!shieldTbc && (!hasFullPackedGuid(packet))) { + packet.setReadPos(packet.getSize()); break; + } + uint64_t victimGuid = shieldTbc ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - uint64_t casterGuid = shieldClassicLike + if (packet.getSize() - packet.getReadPos() < (shieldTbc ? 8u : 1u) + || (!shieldTbc && !hasFullPackedGuid(packet))) { + packet.setReadPos(packet.getSize()); break; + } + uint64_t casterGuid = shieldTbc ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); if (packet.getSize() - packet.getReadPos() < 12) { packet.setReadPos(packet.getSize()); break; } uint32_t shieldSpellId = packet.readUInt32(); uint32_t damage = packet.readUInt32(); - if (!shieldClassicLike && packet.getSize() - packet.getReadPos() >= 4) + if (shieldWotlkLike && packet.getSize() - packet.getReadPos() >= 4) /*uint32_t absorbed =*/ packet.readUInt32(); /*uint32_t school =*/ packet.readUInt32(); // Show combat text: damage shield reflect From f6d8c01779d18a2ec7e78fa1068ef15f5cad2aa9 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 01:54:01 -0700 Subject: [PATCH 33/36] fix(combatlog): validate packed spell miss GUIDs --- src/game/game_handler.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 844bafd6..ca2edcd8 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -2717,14 +2717,20 @@ void GameHandler::handlePacket(network::Packet& packet) { // spellId prefix present in all expansions if (packet.getSize() - packet.getReadPos() < 4) break; uint32_t spellId = packet.readUInt32(); - if (packet.getSize() - packet.getReadPos() < (spellMissUsesFullGuid ? 8u : 1u)) break; + if (packet.getSize() - packet.getReadPos() < (spellMissUsesFullGuid ? 8u : 1u) + || (!spellMissUsesFullGuid && !hasFullPackedGuid(packet))) { + packet.setReadPos(packet.getSize()); break; + } uint64_t casterGuid = readSpellMissGuid(); if (packet.getSize() - packet.getReadPos() < 5) break; /*uint8_t unk =*/ packet.readUInt8(); uint32_t count = packet.readUInt32(); count = std::min(count, 32u); for (uint32_t i = 0; i < count; ++i) { - if (packet.getSize() - packet.getReadPos() < (spellMissUsesFullGuid ? 9u : 2u)) break; + if (packet.getSize() - packet.getReadPos() < (spellMissUsesFullGuid ? 9u : 2u) + || (!spellMissUsesFullGuid && !hasFullPackedGuid(packet))) { + packet.setReadPos(packet.getSize()); break; + } uint64_t victimGuid = readSpellMissGuid(); if (packet.getSize() - packet.getReadPos() < 1) break; uint8_t missInfo = packet.readUInt8(); From 011a148105123da253e45a1133596d6ae00c68da Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 02:01:07 -0700 Subject: [PATCH 34/36] fix(combatlog): validate packed damage shield GUIDs --- src/game/game_handler.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index ca2edcd8..30e91ccb 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -6181,6 +6181,7 @@ void GameHandler::handlePacket(network::Packet& packet) { // WotLK: packed_guid victim + packed_guid caster + spellId(4) + damage(4) + absorbed(4) + schoolMask(4) const bool shieldTbc = isActiveExpansion("tbc"); const bool shieldWotlkLike = !isClassicLikeExpansion() && !shieldTbc; + const auto shieldRem = [&]() { return packet.getSize() - packet.getReadPos(); }; const size_t shieldMinSz = shieldTbc ? 24u : 2u; if (packet.getSize() - packet.getReadPos() < shieldMinSz) { packet.setReadPos(packet.getSize()); break; @@ -6196,12 +6197,13 @@ void GameHandler::handlePacket(network::Packet& packet) { } uint64_t casterGuid = shieldTbc ? packet.readUInt64() : UpdateObjectParser::readPackedGuid(packet); - if (packet.getSize() - packet.getReadPos() < 12) { + const size_t shieldTailSize = shieldWotlkLike ? 16u : 12u; + if (shieldRem() < shieldTailSize) { packet.setReadPos(packet.getSize()); break; } uint32_t shieldSpellId = packet.readUInt32(); uint32_t damage = packet.readUInt32(); - if (shieldWotlkLike && packet.getSize() - packet.getReadPos() >= 4) + if (shieldWotlkLike) /*uint32_t absorbed =*/ packet.readUInt32(); /*uint32_t school =*/ packet.readUInt32(); // Show combat text: damage shield reflect From 6a7071fd64963e25dd5032236dfbdd75a42ca7aa Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 02:10:14 -0700 Subject: [PATCH 35/36] fix(combatlog): validate classic spell damage and heal GUIDs --- src/game/packet_parsers_classic.cpp | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 7077d0ab..046839d0 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -4,6 +4,26 @@ 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; +} + +} // namespace + // ============================================================================ // Classic 1.12.1 movement flag constants // Key differences from TBC: @@ -497,10 +517,10 @@ bool ClassicPacketParsers::parseAttackerStateUpdate(network::Packet& packet, Att // ============================================================================ bool ClassicPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDamageLogData& data) { auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; - if (rem() < 2) return false; + if (rem() < 2 || !hasFullPackedGuid(packet)) return false; data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla - if (rem() < 1) return false; + if (rem() < 1 || !hasFullPackedGuid(packet)) return false; data.attackerGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla // uint32(spellId) + uint32(damage) + uint8(schoolMask) + uint32(absorbed) @@ -532,10 +552,10 @@ bool ClassicPacketParsers::parseSpellDamageLog(network::Packet& packet, SpellDam // ============================================================================ bool ClassicPacketParsers::parseSpellHealLog(network::Packet& packet, SpellHealLogData& data) { auto rem = [&]() { return packet.getSize() - packet.getReadPos(); }; - if (rem() < 2) return false; + if (rem() < 2 || !hasFullPackedGuid(packet)) return false; data.targetGuid = UpdateObjectParser::readPackedGuid(packet); // PackedGuid in Vanilla - if (rem() < 1) return false; + 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 From c1b66f73c5a1b47392c82c86fb3c9c57da820599 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 03:25:52 -0700 Subject: [PATCH 36/36] fix(vulkan): defer resource frees until frame fence --- include/rendering/vk_context.hpp | 12 +++++ src/rendering/terrain_renderer.cpp | 76 +++++++++++++++++++----------- src/rendering/vk_context.cpp | 21 +++++++++ src/rendering/water_renderer.cpp | 58 ++++++++++++++--------- 4 files changed, 118 insertions(+), 49 deletions(-) diff --git a/include/rendering/vk_context.hpp b/include/rendering/vk_context.hpp index 154a4f98..654729b3 100644 --- a/include/rendering/vk_context.hpp +++ b/include/rendering/vk_context.hpp @@ -57,6 +57,15 @@ public: void pollUploadBatches(); // Check completed async uploads, free staging buffers void waitAllUploads(); // Block until all in-flight uploads complete + // Defer resource destruction until it is safe with multiple frames in flight. + // + // This queues work to run after the fence for the *current frame slot* has + // signaled the next time we enter beginFrame() for that slot (i.e. after + // MAX_FRAMES_IN_FLIGHT submissions). Use this for resources that may still + // be referenced by command buffers submitted in the previous frame(s), + // such as descriptor sets and buffers freed during streaming/unload. + void deferAfterFrameFence(std::function&& fn); + // Accessors VkInstance getInstance() const { return instance; } VkPhysicalDevice getPhysicalDevice() const { return physicalDevice; } @@ -173,6 +182,9 @@ private: }; std::vector inFlightBatches_; + void runDeferredCleanup(uint32_t frameIndex); + std::vector> deferredCleanup_[MAX_FRAMES_IN_FLIGHT]; + // Depth buffer (shared across all framebuffers) VkImage depthImage = VK_NULL_HANDLE; VkImageView depthImageView = VK_NULL_HANDLE; diff --git a/src/rendering/terrain_renderer.cpp b/src/rendering/terrain_renderer.cpp index 75ca41c9..a8b518a2 100644 --- a/src/rendering/terrain_renderer.cpp +++ b/src/rendering/terrain_renderer.cpp @@ -990,42 +990,64 @@ void TerrainRenderer::clear() { } chunks.clear(); renderedChunks = 0; - - if (materialDescPool) { - vkResetDescriptorPool(vkCtx->getDevice(), materialDescPool, 0); - } } void TerrainRenderer::destroyChunkGPU(TerrainChunkGPU& chunk) { + if (!vkCtx) return; + + VkDevice device = vkCtx->getDevice(); VmaAllocator allocator = vkCtx->getAllocator(); - if (chunk.vertexBuffer) { - AllocatedBuffer ab{}; ab.buffer = chunk.vertexBuffer; ab.allocation = chunk.vertexAlloc; - destroyBuffer(allocator, ab); - chunk.vertexBuffer = VK_NULL_HANDLE; - } - if (chunk.indexBuffer) { - AllocatedBuffer ab{}; ab.buffer = chunk.indexBuffer; ab.allocation = chunk.indexAlloc; - destroyBuffer(allocator, ab); - chunk.indexBuffer = VK_NULL_HANDLE; - } - if (chunk.paramsUBO) { - AllocatedBuffer ab{}; ab.buffer = chunk.paramsUBO; ab.allocation = chunk.paramsAlloc; - destroyBuffer(allocator, ab); - chunk.paramsUBO = VK_NULL_HANDLE; - } - // Return material descriptor set to the pool so it can be reused by new chunks - if (chunk.materialSet && materialDescPool) { - vkFreeDescriptorSets(vkCtx->getDevice(), materialDescPool, 1, &chunk.materialSet); - } - chunk.materialSet = VK_NULL_HANDLE; + // These resources may still be referenced by in-flight command buffers from + // previous frames. Defer actual destruction until this frame slot is safe. + ::VkBuffer vertexBuffer = chunk.vertexBuffer; + VmaAllocation vertexAlloc = chunk.vertexAlloc; + ::VkBuffer indexBuffer = chunk.indexBuffer; + VmaAllocation indexAlloc = chunk.indexAlloc; + ::VkBuffer paramsUBO = chunk.paramsUBO; + VmaAllocation paramsAlloc = chunk.paramsAlloc; + VkDescriptorPool pool = materialDescPool; + VkDescriptorSet materialSet = chunk.materialSet; - // Destroy owned alpha textures (VkTexture::~VkTexture is a no-op, must call destroy() explicitly) - VkDevice device = vkCtx->getDevice(); + std::vector alphaTextures; + alphaTextures.reserve(chunk.ownedAlphaTextures.size()); for (auto& tex : chunk.ownedAlphaTextures) { - if (tex) tex->destroy(device, allocator); + alphaTextures.push_back(tex.release()); } + + chunk.vertexBuffer = VK_NULL_HANDLE; + chunk.vertexAlloc = VK_NULL_HANDLE; + chunk.indexBuffer = VK_NULL_HANDLE; + chunk.indexAlloc = VK_NULL_HANDLE; + chunk.paramsUBO = VK_NULL_HANDLE; + chunk.paramsAlloc = VK_NULL_HANDLE; + chunk.materialSet = VK_NULL_HANDLE; chunk.ownedAlphaTextures.clear(); + + vkCtx->deferAfterFrameFence([device, allocator, vertexBuffer, vertexAlloc, indexBuffer, indexAlloc, + paramsUBO, paramsAlloc, pool, materialSet, alphaTextures]() { + if (vertexBuffer) { + AllocatedBuffer ab{}; ab.buffer = vertexBuffer; ab.allocation = vertexAlloc; + destroyBuffer(allocator, ab); + } + if (indexBuffer) { + AllocatedBuffer ab{}; ab.buffer = indexBuffer; ab.allocation = indexAlloc; + destroyBuffer(allocator, ab); + } + if (paramsUBO) { + AllocatedBuffer ab{}; ab.buffer = paramsUBO; ab.allocation = paramsAlloc; + destroyBuffer(allocator, ab); + } + if (materialSet && pool) { + VkDescriptorSet set = materialSet; + vkFreeDescriptorSets(device, pool, 1, &set); + } + for (VkTexture* tex : alphaTextures) { + if (!tex) continue; + tex->destroy(device, allocator); + delete tex; + } + }); } int TerrainRenderer::getTriangleCount() const { diff --git a/src/rendering/vk_context.cpp b/src/rendering/vk_context.cpp index fdd07d8e..51781a3c 100644 --- a/src/rendering/vk_context.cpp +++ b/src/rendering/vk_context.cpp @@ -55,6 +55,11 @@ void VkContext::shutdown() { vkDeviceWaitIdle(device); } + // With the device idle, it is safe to run any deferred per-frame cleanup. + for (uint32_t fi = 0; fi < MAX_FRAMES_IN_FLIGHT; fi++) { + runDeferredCleanup(fi); + } + LOG_WARNING("VkContext::shutdown - destroyImGuiResources..."); destroyImGuiResources(); @@ -103,6 +108,19 @@ void VkContext::shutdown() { LOG_WARNING("Vulkan context shutdown complete"); } +void VkContext::deferAfterFrameFence(std::function&& fn) { + deferredCleanup_[currentFrame].push_back(std::move(fn)); +} + +void VkContext::runDeferredCleanup(uint32_t frameIndex) { + auto& q = deferredCleanup_[frameIndex]; + if (q.empty()) return; + for (auto& fn : q) { + if (fn) fn(); + } + q.clear(); +} + bool VkContext::createInstance(SDL_Window* window) { // Get required SDL extensions unsigned int sdlExtCount = 0; @@ -1349,6 +1367,9 @@ VkCommandBuffer VkContext::beginFrame(uint32_t& imageIndex) { return VK_NULL_HANDLE; } + // Any work queued for this frame slot is now guaranteed to be unused by the GPU. + runDeferredCleanup(currentFrame); + // Acquire next swapchain image VkResult result = vkAcquireNextImageKHR(device, swapchain, UINT64_MAX, frame.imageAvailableSemaphore, VK_NULL_HANDLE, &imageIndex); diff --git a/src/rendering/water_renderer.cpp b/src/rendering/water_renderer.cpp index d79e53f7..2bbff1a3 100644 --- a/src/rendering/water_renderer.cpp +++ b/src/rendering/water_renderer.cpp @@ -1029,10 +1029,6 @@ void WaterRenderer::clear() { destroyWaterMesh(surface); } surfaces.clear(); - - if (vkCtx && materialDescPool) { - vkResetDescriptorPool(vkCtx->getDevice(), materialDescPool, 0); - } } // ============================================================== @@ -1358,27 +1354,45 @@ void WaterRenderer::createWaterMesh(WaterSurface& surface) { void WaterRenderer::destroyWaterMesh(WaterSurface& surface) { if (!vkCtx) return; + VkDevice device = vkCtx->getDevice(); VmaAllocator allocator = vkCtx->getAllocator(); - if (surface.vertexBuffer) { - AllocatedBuffer ab{}; ab.buffer = surface.vertexBuffer; ab.allocation = surface.vertexAlloc; - destroyBuffer(allocator, ab); - surface.vertexBuffer = VK_NULL_HANDLE; - } - if (surface.indexBuffer) { - AllocatedBuffer ab{}; ab.buffer = surface.indexBuffer; ab.allocation = surface.indexAlloc; - destroyBuffer(allocator, ab); - surface.indexBuffer = VK_NULL_HANDLE; - } - if (surface.materialUBO) { - AllocatedBuffer ab{}; ab.buffer = surface.materialUBO; ab.allocation = surface.materialAlloc; - destroyBuffer(allocator, ab); - surface.materialUBO = VK_NULL_HANDLE; - } - if (surface.materialSet && materialDescPool) { - vkFreeDescriptorSets(vkCtx->getDevice(), materialDescPool, 1, &surface.materialSet); - } + ::VkBuffer vertexBuffer = surface.vertexBuffer; + VmaAllocation vertexAlloc = surface.vertexAlloc; + ::VkBuffer indexBuffer = surface.indexBuffer; + VmaAllocation indexAlloc = surface.indexAlloc; + ::VkBuffer materialUBO = surface.materialUBO; + VmaAllocation materialAlloc = surface.materialAlloc; + VkDescriptorPool pool = materialDescPool; + VkDescriptorSet materialSet = surface.materialSet; + + surface.vertexBuffer = VK_NULL_HANDLE; + surface.vertexAlloc = VK_NULL_HANDLE; + surface.indexBuffer = VK_NULL_HANDLE; + surface.indexAlloc = VK_NULL_HANDLE; + surface.materialUBO = VK_NULL_HANDLE; + surface.materialAlloc = VK_NULL_HANDLE; surface.materialSet = VK_NULL_HANDLE; + + vkCtx->deferAfterFrameFence([device, allocator, vertexBuffer, vertexAlloc, indexBuffer, indexAlloc, + materialUBO, materialAlloc, pool, materialSet]() { + if (vertexBuffer) { + AllocatedBuffer ab{}; ab.buffer = vertexBuffer; ab.allocation = vertexAlloc; + destroyBuffer(allocator, ab); + } + if (indexBuffer) { + AllocatedBuffer ab{}; ab.buffer = indexBuffer; ab.allocation = indexAlloc; + destroyBuffer(allocator, ab); + } + if (materialUBO) { + AllocatedBuffer ab{}; ab.buffer = materialUBO; ab.allocation = materialAlloc; + destroyBuffer(allocator, ab); + } + if (materialSet && pool) { + VkDescriptorSet set = materialSet; + vkFreeDescriptorSets(device, pool, 1, &set); + } + }); } // ==============================================================