# DPS Log Module Research ## Goal Provide structured Lua objects for combat log events so addons can read parsed fields directly instead of re-parsing localized combat log strings. Modeled after TBC's `COMBAT_LOG_EVENT_UNFILTERED`. ## The Problem Vanilla 1.12.1 fires `CHAT_MSG_COMBAT_*` events with localized text in `arg1`. Addons like DPSMate parse these with 5-15 `strfind` calls per event across 2177 lines of locale-specific pattern matching. In 40-man raids this causes measurable frame rate impact. --- ## Two Pipelines ### Pipeline 1: Chat-type combat messages (the Lua events addons see) ``` Server (SMSG_MESSAGECHAT) -> ProcessIncomingChatPacket (0x0049d560) -> AddChatMessageToQueue (0x0049cae0) [enqueue with type + text + GUIDs] -> queue at PTR_00b4fdc0 -> ProcessChatText (0x0049b560) [dequeue, fire Lua events] -> CHAT_MSG_COMBAT_SELF_HITS, CHAT_MSG_SPELL_SELF_DAMAGE, etc. ``` - `AddChatMessageToQueue`: `__fastcall(ECX=msgType, EDX=textPtr, stack: guid_lo, guid_hi, ...)` ~13 params, allocates 0xC8-byte queue entry, stores type + text + GUIDs - `ProcessChatText` (0x0049b560): dequeues messages, fires Lua events - These are the **formatted localized strings** DPSMate has to parse ### Pipeline 2: Spell/combat log display (structured data) ``` Server (SMSG_SPELLLOGEXECUTE / SMSG_ATTACKERSTATEUPDATE / etc.) -> ProcessUnitUpdateWithTimingValidation (0x00618c30) -> ProcessUnitActionWithSpellCastingAndValidation (0x0061a820) -> PacketHandler_Wrapper_Generic2 (0x00602d00) -> ProcessComplexSpellCast (0x00629b60) -> ProcessSpellDamageWithLocalization (0x00629d30) -> ProcessEnchantmentApplication -> DisplaySpellLogMessage -> etc. -> LogCombatMessage (0x006268f0) -> FireLuaEvent(0x21e, formatted_text) ``` - `ProcessComplexSpellCast` (0x629b60): `__fastcall(ECX=spellData)`, dispatches by spell flags to sub-handlers. Calls `ValidateSpellCastAndGetObjects` first to resolve source/target GUIDs and spell info. - `ProcessSpellDamageWithLocalization` (0x629d30): `__fastcall(ECX=spellEffectData, EDX=sourceGuid, stack: targetGuid, combatFlags, damageAmount, absorptionAmount, resistAmount)` -- has ALL the structured data before string formatting. - `LogCombatMessage` (0x6268f0): `__fastcall(ECX=msgTypeIndex, EDX=unused, stack: extraText)`. Reads format string from table at `[ESI*4 + 0x862920]`, copies from g_LuaEventData (0x835154), fires `FireLuaEvent(0x21e, ...)`. ### Format String Table at 0x862920 Index -> string (used by LogCombatMessage to select combat result type): ``` [0] INTERRUPT [1] DAMAGE_CRIT [2] DAMAGE [4] BLOCK [5] ABSORB [6] RESIST [7] EVADE [8] DODGE [9] PARRY [10] IMMUNE [11] DEFLECT [12] ENCHANTMENT_REMOVED [31] SPELL_PARRIED [32] SPELL_BLOCKED [33] SPELL_EVADED [34] SPELL_IMMUNE [35] SPELL_DEFLECTED [36] SPELL_REFLECTED [37] SPELL_MISSED [38] SPELL_ACTIVE [39] FACTION ``` --- ## Key Addresses | Address | Function | Calling Convention | Notes | |---------|----------|--------------------|-------| | 0x0049cae0 | AddChatMessageToQueue | __fastcall(ECX,EDX + 11 stack) | Enqueue combat msg | | 0x0049b560 | ProcessChatText | __fastcall | Dequeue + fire Lua events | | 0x0049d560 | ProcessIncomingChatPacket | ?? | SMSG_MESSAGECHAT handler | | 0x00629b60 | ProcessComplexSpellCast | __fastcall(ECX=data) | Main spell dispatch | | 0x00629d30 | ProcessSpellDamageWithLocalization | __fastcall(ECX,EDX + 5 stack) | Structured damage data | | 0x00628100 | ExecuteSpellWithLocalizedText | ?? | Called for periodic | | 0x0062aac0 | ProcessEnvironmentalDamage | ?? | Fall/fire/lava/drown | | 0x0062cd80 | ProcessSpellCastEffect | ?? | Spell cast results | | 0x006268f0 | LogCombatMessage | __fastcall(ECX=index, stack: text) | Format + fire 0x21e | | 0x00703f50 | FireLuaEvent | __cdecl(eventId, eventData) | Generic event fire | | 0x0051ab30 | InitializeGameEventTable | ?? | Registers CHAT_MSG_* names | | 0x00862920 | (data) | -- | Combat result format string table | | 0x00835154 | g_LuaEventData | -- | Global combat msg text buffer | | 0x00b4fdc0 | PTR_00b4fdc0 | -- | Chat message queue head | --- ## Event System - `FireLuaEvent` (0x703f50): `__cdecl(eventId, eventDataFmtStr, ...)` Looks up event handler list at `PTR_00ceef68 + eventId * 0x10`. Pushes event name as Lua global, iterates callback linked list, calls each. - Event 0x21e (542): used by `LogCombatMessage` -- appears to be a general combat log display event. - `CHAT_MSG_COMBAT_SELF_HITS` etc. are registered in `InitializeGameEventTable` with their own event IDs. The mapping from queue message type to event ID happens in `ProcessChatText`. --- ## Hook Strategy Options ### Option A: Hook AddChatMessageToQueue (0x0049cae0) - **Pro**: Single funnel point for ALL chat-type combat messages - **Pro**: Has message type ID + formatted text + GUIDs - **Con**: Text is already formatted -- we'd need to either parse it ourselves (defeats the purpose) or capture structured data from upstream before it reaches here - **Con**: __fastcall with ~13 params, complex signature ### Option B: Hook upstream structured functions - Hook `ProcessSpellDamageWithLocalization` (0x629d30) and siblings - **Pro**: Has raw structured data (source, target, spell, damage, absorb, resist, flags) - **Con**: Multiple functions to hook (damage, periodic, environmental, cast effects) - **Con**: Need to correlate with the eventual CHAT_MSG event type ### Option C: Hook ProcessChatText (0x0049b560) - **Pro**: Where events actually fire to Lua -- can inject additional data here - **Pro**: Has the message type -> event name mapping - **Con**: By this point the text is formatted, structured data is lost ### Option D: Hybrid -- capture structured data upstream, attach at dispatch - Hook the structured functions to capture data into a ring buffer - Hook ProcessChatText to attach the captured data as extra Lua globals when firing the event - **Pro**: Best of both worlds -- structured data + correct event correlation - **Con**: Most complex, need to match upstream captures to downstream events ### Option E: Fire a new custom event alongside existing ones - Hook at the structured data level, fire our own `COMBAT_LOG_EVENT_UNFILTERED` event with structured args (like TBC) in addition to the normal events - **Pro**: Cleanest API, addons opt in, existing behavior unchanged - **Con**: Need to hook multiple upstream functions, need our own event registration --- ## Nampower Reference (reference/nampower/) Nampower already implements partial structured combat events. Key patterns: ### Custom Event Registration via Unused Slots The event table has unused slots. Nampower repurposes them by overwriting the event name string pointer: ``` Event 369 (0x171) -> "SPELL_QUEUE_EVENT" at 0xBE175C Event 540 (0x21C) -> "SPELL_CAST_EVENT" at 0xBE1A08 Event 549 (0x225) -> "SPELL_DAMAGE_EVENT_SELF" at 0xBE1A2C Event 550 (0x226) -> "SPELL_DAMAGE_EVENT_OTHER" at 0xBE1A30 ``` To register events beyond the default max (549), hook `FrameScript_CreateEvents` (0x703D90) and increase `maxEventId`. Nampower bumps 549 -> 551. ### Packet Handler Hooks (from nampower offsets.hpp) | Address | Handler | Packet | Conv | |---------|---------|--------|------| | 0x626DD0 | PeriodicAuraLogHandler | SMSG_PERIODICAURALOG | FastCall(unk,opCode,unk2,CDataStore*) | | 0x5E85E0 | SpellNonMeleeDmgLogHandler | SMSG_SPELLNONMELEEDAMAGELOG | FastCall(unk,opCode,unk2,CDataStore*) | | 0x6E7640 | SpellStartHandler | SMSG_SPELL_START (0x131) | FastCall(unk,opCode,unk2,CDataStore*) | | 0x6E7330 | CastResultHandler | SMSG_CAST_RESULT | PacketHandler(opCode*,CDataStore*) | | 0x6E8D80 | SpellFailedHandler | SMSG_SPELL_FAILURE | PacketHandler(opCode*,CDataStore*) | | 0x6E7550 | SpellChannelStartHandler | SMSG_CHANNEL_START | PacketHandler(opCode*,CDataStore*) | | 0x6E75F0 | SpellChannelUpdateHandler | SMSG_CHANNEL_UPDATE | PacketHandler(opCode*,CDataStore*) | | 0x6E9460 | SpellCooldownHandler | SMSG_SPELL_COOLDOWN | PacketHandler(opCode*,CDataStore*) | ### SMSG_SPELLNONMELEEDAMAGELOG Packet Format ``` targetGuid : PackedGuid casterGuid : PackedGuid spellId : u32 damage : u32 school : u8 absorb : u32 resist : i32 periodicLog : u8 unused : u8 blocked : u32 hitInfo : u32 (flags: 0x1=crit, 0x4000=crushing, etc.) extendData : u8 ``` ### SMSG_PERIODICAURALOG Packet Format ``` targetGuid : PackedGuid casterGuid : PackedGuid spellId : u32 count : u32 auraType : u32 case 3/89 (PERIODIC_DAMAGE/PERIODIC_DAMAGE_PERCENT): amount : u32 spellSchool : u32 absorb : u32 resist : i32 case 8/20 (PERIODIC_HEAL/OBS_MOD_HEALTH): amount : u32 case 21/24 (OBS_MOD_MANA/PERIODIC_ENERGIZE): powerType : u32 amount : u32 case 64 (PERIODIC_MANA_LEECH): powerType : u32 amount : u32 multiplier : u32 ``` ### Nampower TriggerSpellDamageEvent Format Fires via `SignalEventParam(eventId, fmt, args...)`: ``` format: "%s%s%d%d%s%d%d%s" args: targetGuidStr, casterGuidStr, spellId, amount, "absorb,blocked,resist", hitInfo, spellSchool, "effect0,effect1,effect2,auraType" ``` ### SignalEvent Signatures - `SignalEvent` (0x703E50): `__fastcall(eventId)` -- no params, just fires event - `SignalEventParam` (0x703F50): `__cdecl(eventId, fmtStr, ...)` -- variadic, pushes formatted string args as Lua event args (arg1, arg2, ...) --- ## Packets NOT Covered by Nampower (DPSMate needs these too) Based on DPSMate's 54 registered events, we still need handlers for: ### Melee Combat - SMSG_ATTACKERSTATEUPDATE -- melee hits/crits/misses/dodges/parries/etc. Handler likely in the 0x62xxxx range. Contains source/target GUIDs, damage, school, hitInfo flags, blocked/absorbed/resisted amounts. ### Healing - SMSG_SPELLHEALLOG -- direct heals Contains casterGuid, targetGuid, spellId, healAmount, critFlag ### Deaths - SMSG_DESTRUCTOBJ or similar -- unit deaths ### Buffs/Debuffs (aura application/removal) - Aura gain/loss events -- DPSMate tracks buff uptime, dispels ### Environmental Damage - SMSG_ENVIRONMENTALDAMAGELOG -- fall, lava, drown, fire Already partially handled via ProcessEnvironmentalDamage (0x62aac0) --- ## DPSMate Coverage Matrix DPSMate registers 54 events across these categories. Our goal: provide structured data for each so addons skip the 2177-line locale-specific string parser. ### Category 1: Spell Damage (direct) — DONE ✓ **Packet**: SMSG_SPELLNONMELEEDAMAGELOG (handler 0x5E85E0) **Our event**: `COMBAT_LOG_SPELL_DMG` (slot 551) **Fields**: targetGUID, casterGUID, spellId, damage, school, absorb, resist, blocked, hitInfo Covers these DPSMate events: - [x] CHAT_MSG_SPELL_SELF_DAMAGE (spell hits/crits/misses/parries/dodges/resists/absorbs) - [x] CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE - [x] CHAT_MSG_SPELL_PARTY_DAMAGE - [x] CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE - [x] CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE - [x] CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE - [x] CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE - [x] CHAT_MSG_SPELL_PET_DAMAGE - [x] CHAT_MSG_SPELL_DAMAGESHIELDS_ON_SELF (damage shields are spell damage) - [x] CHAT_MSG_SPELL_DAMAGESHIELDS_ON_OTHERS ### Category 2: Periodic Damage/Heal Ticks — DONE ✓ **Packet**: SMSG_PERIODICAURALOG (handler 0x626DD0) **Our event**: `COMBAT_LOG_PERIODIC` (slot 552) **Fields**: targetGUID, casterGUID, spellId, amount, school, absorb, resist, auraType, powerType auraType distinguishes: 3=PERIODIC_DAMAGE, 89=PERIODIC_DAMAGE_PERCENT, 8=PERIODIC_HEAL, 20=OBS_MOD_HEALTH, 21=OBS_MOD_MANA, 24=PERIODIC_ENERGIZE, 64=PERIODIC_MANA_LEECH Covers these DPSMate events: - [x] CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE - [x] CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE - [x] CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE - [x] CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE - [x] CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE - [x] CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS (periodic heal component) - [x] CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS (periodic heal component) - [x] CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS (periodic heal component) - [x] CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS (periodic heal component) ### Category 3: Melee Damage — DONE ✓ **Packet**: SMSG_ATTACKERSTATEUPDATE (opcode 0x14A) **Our event**: `COMBAT_LOG_MELEE` (slot 554) **Fields**: targetGUID, attackerGUID, totalDamage, school, absorb, resist, blocked, hitInfo, victimState **Handler found**: Opcode 0x14A dispatches through a large switch at **0x6255B0** (registered in `UpdateBindLocation`, shared with opcodes 0x143-0x149). Jump table: byte lookup at 0x625B0C, jump at 0x625AEC. Case 5 -> **0x62578D**. The actual **packet parser** is at **0x625C60** -- `__thiscall(ECX=struct_out, stack=CDataStore*)`. After parsing, calls `ClntObjMgrObjectPtr` then **0x625E20** (post-parse handler). **Verified packet wire format** (from Ghidra disassembly of 0x625C60): ``` hitInfo : u32 attackerGuid : PackedGuid targetGuid : PackedGuid totalDamage : u32 subDamageCount : u8 per subDamage (5 fields each): school : u32 damageFP : f32 damage : u32 absorb : u32 resist : u32 victimState : u32 (0=hit,1=dodge,2=parry,3=interrupt,4=block,5=evade,6=immune,7=deflect) unknown1 : u32 unknown2 : u32 spellId : u32 (0 for melee) if hitInfo & 0x1: blocked : u32 ... extended block data ``` **HitInfo flags** (from mangos): 0x00001=NORMALSWING (block data present), 0x00002=OFFHAND, 0x00004=MISS, 0x00008=FULL_ABSORB, 0x00010=FULL_RESIST, 0x00020=CRIT, 0x00200=CRUSHING, 0x00400=GLANCING **CDataStore read functions** (verified byte sizes from disassembly): - 0x418EB0 (ReadPointerFromBuffer): reads 4 bytes (u32) - 0x418E30 (ReadPointerFromStream): reads 4 bytes (u32) - 0x418CB0 (ReadByteFromBuffer): reads 1 byte (u8) - 0x419130 (ReadPointerFromData): reads 4 bytes (f32) - 0x642ED0 (GetPackedGuid): reads packed GUID **Hooking strategy -- IMPORTANT**: Do NOT hook the packet handler (0x6255B0) or parser (0x625C60) directly. Packet handler hooks are sensitive to Warden detection. Prefer hooking slightly further downstream -- e.g., the post-parse handler at **0x625E20** which receives the already-parsed struct and resolved unit pointer. Research needed: verify 0x625E20 signature, what data is accessible, whether it's safe as a hook target. Covers these DPSMate events (the largest group -- 16 events): - [x] CHAT_MSG_COMBAT_SELF_HITS - [x] CHAT_MSG_COMBAT_SELF_MISSES - [x] CHAT_MSG_COMBAT_PARTY_HITS - [x] CHAT_MSG_COMBAT_PARTY_MISSES - [x] CHAT_MSG_COMBAT_FRIENDLYPLAYER_HITS - [x] CHAT_MSG_COMBAT_FRIENDLYPLAYER_MISSES - [x] CHAT_MSG_COMBAT_HOSTILEPLAYER_HITS - [x] CHAT_MSG_COMBAT_HOSTILEPLAYER_MISSES - [x] CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS - [x] CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES - [x] CHAT_MSG_COMBAT_CREATURE_VS_PARTY_HITS - [x] CHAT_MSG_COMBAT_CREATURE_VS_PARTY_MISSES - [x] CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_HITS - [x] CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_MISSES - [x] CHAT_MSG_COMBAT_PET_HITS - [x] CHAT_MSG_COMBAT_PET_MISSES **SMSG_ATTACKERSTATEUPDATE packet format** (from mangos/wowdev): ``` hitInfo : u32 (flags: 0x1=normal, 0x2=offhand, 0x4=miss, etc.) attackerGuid : PackedGuid targetGuid : PackedGuid totalDamage : u32 subDamageCount : u8 per subDamage: school : u32 damageFP : f32 damage : u32 absorb : u32 resist : u32 victimState : u32 (0=hit, 1=dodge, 2=parry, 3=interrupt, 4=block, 5=evade, 6=immune, 7=deflect) unknown1 : u32 spellId : u32 (0 for melee) blocked : u32 (if hitInfo & BLOCK) ``` Tracing approaches: - Search for opcode 0x14A in packet dispatch table - Trace from `ProcessUnitUpdateWithTimingValidation` (0x618c30) - Search for callers of functions that reference melee hit strings ### Category 4: Direct Heals — DONE ✓ **Packet**: SMSG_SPELLHEALLOG (opcode 0x150, handler 0x5E89C0) **Our event**: `COMBAT_LOG_HEAL` (slot 553) **Fields**: targetGUID, casterGUID, spellId, healAmount, isCrit Covers these DPSMate events: - [x] CHAT_MSG_SPELL_SELF_BUFF (heal component: "Your X heals Y for Z") - [x] CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF (heal component: "X's Y heals Z for W") - [x] CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF (heal component) - [x] CHAT_MSG_SPELL_PARTY_BUFF (heal component) ### Category 5: Environmental Damage — DONE ✓ **Handler**: `ProcessEnvironmentalDamage` (0x62aac0) — downstream function, not a packet handler **Our event**: `COMBAT_LOG_ENV_DMG` (slot 555) **Fields**: victimGUID, damageType, baseDamage, absorb damageType values: 0=EXHAUSTED(fatigue), 1=DROWNING, 2=FALL, 3=LAVA, 4=SLIME, 5=FIRE Hook approach: direct hook on ProcessEnvironmentalDamage (called from SMSG_ENVIRONMENTALDAMAGELOG packet handler chain). Already has parsed structured data — no CDataStore parsing needed. __fastcall(ECX=victimGuidPtr, EDX=damageType, stack: damageSource, baseDamage, absorb), returns void. ### Category 6: Aura Gain/Loss — DONE ✓ Hooked via object field update callbacks (no packet handler — vanilla uses SMSG_UPDATE_OBJECT): - [x] SPELL_AURA_APPLIED: SetSpellTarget (0x6123F0) — aura field inactive→active transition - [x] SPELL_AURA_REMOVED: CastSpell (0x612320) — aura field active→inactive transition - [x] SPELL_AURA_APPLIED_DOSE: ValidateSpellSlot (0x612450) — stack count increase - [x] SPELL_AURA_REMOVED_DOSE: ValidateSpellSlot (0x612450) — stack count decrease - [x] SPELL_DISPEL: ProcessAuraDispelMessage (0x62D480) — from SMSG_SPELLDISPELLOG Aura type heuristic: slot < 40 = "BUFF", slot >= 40 = "DEBUFF" (vanilla aura slot layout). Source GUID unknown for field update hooks — passed as GUID_ZERO (no UNIT_FIELD_AURA_CREATOR in vanilla). Still missing: - [ ] SPELL_AURA_REFRESH: no callback for same-spell re-application - [ ] SPELL_AURA_BROKEN / BROKEN_SPELL: CC break detection ### Category 7: Deaths — DONE ✓ - [x] UNIT_DIED: HandleUnitDeath (0x605860) — called from HandleUnitHealthChange on death transition - [x] PARTY_KILL: PartyKillLogHandler (0x628890) — from SMSG_PARTYKILLLOG packet ### Category 8: Interrupts — Embedded in spell damage events DPSMate parses interrupts from SPELL_SELF_DAMAGE and SPELL_CREATURE_VS_SELF_DAMAGE ("You interrupt X's Y"). These are already covered by our `COMBAT_LOG_SPELL_DMG` event since interrupts flow through the same packet handlers. The addon can detect interrupts by checking hitInfo flags. ### Category 9: Power Gains — Partially covered DPSMate parses "You gain X Mana/Rage/Energy from Y" from SPELL_SELF_BUFF and SPELL_PERIODIC_SELF_BUFFS. Our PERIODIC hook already covers periodic energize (auraType 21/24). Direct power gains from spell casts (e.g., Life Tap, mana gems) would need a separate hook or could be left to text events. --- ## FrameScript_CreateEvents Crash — Root Cause (2026-03-10) ### The Crash - Address 0x703E19 inside `FrameScript_CreateEvents` (0x703D90) - ACCESS_VIOLATION reading 0x451C4000 (garbage pointer as event name string) - Deterministic: EAX=0x451C4000, EBX=0x1A0(416), ESI=0x1A(26) in both crashes ### Root Cause (TWO bugs) **Bug 1: Second call site with different event table.** `FrameScript_CreateEvents` is `__fastcall(ECX=eventNameArray, EDX=eventCount)`. There are TWO call sites: - 0x48FEAB: `MOV ECX, 0xBE1198` (main event table, 549 entries) - 0x46A88F: `MOV ECX, 0xB41E70` (secondary event table, much smaller) Our hook bumped maxEventId unconditionally. When the secondary call came through with ECX=0xB41E70 and a small count, we bumped it to 557. The function iterated 557 entries from a much smaller array, reading garbage at index 26. EBX=0x1A0 = 26*0x10 (destination offset), ESI=0x1A = 26 (loop counter). **Bug 2: Stomping float globals beyond the event array.** Slots 551-555 (addresses 0xBE1A34-0xBE1A44) are NOT free space. Ghidra xrefs show: - 0xBE1A34: float written by 0x51C2CC (FSTP) - 0xBE1A40: float written by 0x51BE5C (FSTP) - 0xBE1A44: float written by 0x51C09C (FSTP) All in the 0x51xxxx addon/UI system. Zeroing/overwriting these corrupts game state. ### Fix 1. Guard count bump: `if (param1 == 0xBE1198 and new_max < 551)` — only expand main table 2. Single event slot 549 (verified safe by nampower) instead of 551-555 3. Unified COMBAT_LOG_EVENT with WotLK-style subevent strings, bump to 551 ## Implementation Status ### Done — 32 subevents across 23 hooks (22 hook addresses + wow.zig import) **Infrastructure:** - [x] Unified COMBAT_LOG_EVENT at slot 549 (WotLK-style subevents) - [x] FrameScript_CreateEvents hook — guarded for main table only, bump to 550 - [x] CDataStore read helpers (cdsGet, cdsGetPackedGuid, save/restore m_read) - [x] GUID-to-string conversion (4-buffer rotating hex formatter) - [x] Fire functions: fireCombatLog, fireSwingMissed, fireSpellStr, fireSpell, fireBase, fireSpellStrD, fireSpellDispel - [x] Event string pointer base verified: 0xBE1198 (derived from nampower 549→0xBE1A2C) **Phase 1-2 — Packet handler hooks:** - [x] SpellNonMeleeDmgLogHandler (0x5E85E0) → SPELL_DAMAGE + RANGE_DAMAGE (opcode 0x250) - [x] PeriodicAuraLogHandler (0x626DD0) → SPELL_PERIODIC_DAMAGE/HEAL/ENERGIZE/LEECH (opcode 0x24E) - [x] MeleeDispatcher (0x6255B0) → SWING_DAMAGE / SWING_MISSED (opcode 0x14A filter) - [x] PartyKillLogHandler (0x628890) → PARTY_KILL (opcode 0x1F5) - [x] SpellStartHandler (0x6E7640) → SPELL_CAST_START (0x131) / SPELL_CAST_SUCCESS (0x132) - [x] CastResultHandler (0x6E7330) → SPELL_CAST_FAILED (opcode 0x130, status != 0) **Phase 3 — Downstream hooks (Warden-safe):** - [x] ProcessSpellPowerDrainMessage (0x62C770) → SPELL_HEAL (migrated from packet handler) - [x] ProcessSpellCombatResult (0x62BAB0) → SPELL_MISSED - [x] ProcessSpellDrainEffectMessage (0x62CA20) → DAMAGE_SHIELD - [x] DisplaySpellInterruptMessage (0x626A10) → SPELL_INTERRUPT - [x] ProcessInstaKillSpellMessage (0x62CBE0) → SPELL_INSTAKILL - [x] ~~ProcessEnergize (0x62DC10)~~ → removed, was OPEN_LOCK (effectType 33), not energize - [x] ProcessEnvironmentalDamage (0x62AAC0) → ENVIRONMENTAL_DAMAGE **Phase 4 — Death and aura field change hooks:** - [x] HandleUnitDeath (0x605860) → UNIT_DIED (fastcall, ECX=unit, plain RET) - [x] SetSpellTarget (0x6123F0) → SPELL_AURA_APPLIED (thiscall, RET 0x8) - [x] CastSpell (0x612320) → SPELL_AURA_REMOVED (thiscall, RET 0x8) **Phase 5 — Dose, extra attacks, dispel, ranged detection:** - [x] ValidateSpellSlot (0x612450) → SPELL_AURA_APPLIED_DOSE / SPELL_AURA_REMOVED_DOSE (thiscall, RET 0x8) - [x] ProcessExtraAttacksSpellMessage (0x62D9F0) → SPELL_EXTRA_ATTACKS (fastcall, RET 0x4) - [x] ProcessAuraDispelMessage (0x62D480) → SPELL_DISPEL (fastcall, RET 0x4) - [x] RANGE_DAMAGE — detected via spell ID check (75=Auto Shot, 5019=Shoot) in spell damage hook - [x] RANGE_MISSED — detected via spell ID check (75/5019) in spell missed hook - [x] ProcessSpellEffect (0x62ACE0) → SPELL_SUMMON / SPELL_RESURRECT / SPELL_ENERGIZE (spell DB Effect[] lookup) - ENERGIZE moved here: effectType 30 falls to default case → ProcessSpellEffect, not 0x62DC10 **Phase 6 — Aura refresh + bug fixes:** - [x] SetActionCooldownTimer (0x4E4390) → SPELL_AURA_REFRESH (⚠ LOCAL PLAYER ONLY) - Hooked from SMSG_UPDATE_AURA_DURATION (opcode 0x137) handler - Detects refresh by checking if aura slot already has active spell in descriptors - Vanilla only sends duration updates to the buffed player — other units' refreshes are invisible - [x] SPELL_AURA_REFRESH heuristic for all other units via SPELL_GO hit target parsing - Extended spellStartDetour to parse SMSG_SPELL_GO (0x132) hit target list - For aura spells (APPLY_AURA/APPLY_AREA_AURA effects), checks if hit target already has the aura - Skips local player (handled by SetActionCooldownTimer), fires for all other units - Helpers: hasApplyAuraEffect(spellId), unitHasAura(guid, spellId) → ?slot - [x] SPELL_AURA_BROKEN_SPELL heuristic via damage ring buffer (16 entries) - Damage hooks record {target, source, spellId} into ring buffer - auraRemovedDetour checks SpellRec.AuraInterruptFlags (+0x58) for damage-break flags - If breakable + recent damage → fire SPELL_AURA_BROKEN_SPELL before SPELL_AURA_REMOVED - [x] ENERGIZE false-fire bug fixed: removed 0x62DC10 hook, moved detection to ProcessSpellEffect with spell DB filter - [x] Aura BUFF/DEBUFF detection improved: uses spell DB Attributes flag 0x4000000 (SPELL_ATTR_NEGATIVE) with slot < 40 fallback ### Known Issues — Fixed - ~~**SPELL_ENERGIZE false fires**~~: Fixed. Removed 0x62DC10 hook (was OPEN_LOCK/effectType 33). ENERGIZE now detected via spell DB Effect[] check in ProcessSpellEffect (0x62ACE0). ### Known Limitations - **SPELL_AURA_REFRESH heuristic for other units**: For non-local units, vanilla sends NO refresh notification at all (no packet, no descriptor change). We use a SPELL_GO heuristic: when a buff spell hits a target that already has that aura in their descriptors, we infer a refresh. This is best-effort and has known limitations: - False positives: spell hits target but aura was actually removed and re-applied (rare) - False negatives: procs, channeled refreshes, or any refresh without a SPELL_GO packet - Only works for spells with APPLY_AURA (6) or APPLY_AREA_AURA (27) effects in spell DB - Local player excluded (uses reliable SetActionCooldownTimer path instead) ### SPELL_AURA_BROKEN_SPELL — Heuristic via Damage Ring Buffer - [x] Implemented as heuristic: damage ring buffer (16 entries) + aura removal correlation - Damage hooks (SPELL_DAMAGE, SWING_DAMAGE, SPELL_PERIODIC_DAMAGE, ENVIRONMENTAL_DAMAGE) record {targetGUID, sourceGUID, spellId} into ring buffer via recordDamage() - In auraRemovedDetour: if removed aura has AURA_INTERRUPT_FLAG_DAMAGE (0x02) or AURA_INTERRUPT_FLAG_DIRECT_DAMAGE (0x01000000) in SpellRec.AuraInterruptFlags (+0x58), check ring buffer for recent damage to same target - If match: fire SPELL_AURA_BROKEN_SPELL with breaking source info, then SPELL_AURA_REMOVED - Server confirms: AuraRemoveMode is tracked internally but NEVER sent to client - No timestamp needed: damage packets arrive BEFORE descriptor updates (same server tick) - Known limitations: false positives if damage + unrelated aura removal happen in same batch; no coverage for SPELL_AURA_BROKEN (without spell — melee break) since we pass spellId=0 for melee ### Future — Custom Extensions (beyond WotLK spec) - [ ] **RESURRECT_ACCEPTED**: Current SPELL_RESURRECT fires on cast/effect (ProcessSpellEffect), not when the target accepts. Detect via outgoing CMSG_RESURRECT_RESPONSE (0x15C) or SMSG_UPDATE_OBJECT alive state change. Related: SMSG_RESURRECT_REQUEST (0x15B), SMSG_RESURRECT_FAILED (0x252). - [ ] **UNIT_POSITION**: Periodic or event-driven unit position data in combat log events. Useful for replay/analysis tools. - [ ] **SPELL_CAST dest coordinates**: SMSG_SPELL_START/GO include destination XYZ for area-targeted spells via TARGET_FLAG_DEST_LOCATION (0x40) in SpellCastTargets. - Target data format in packet: uint16 targetMask, then if 0x02: packed GUID (unit), if 0x20: 3x float (source XYZ), if 0x40: 3x float (dest XYZ) - Player ground AoE (Blizzard, Flamestrike, Rain of Fire, Volley): client sends clicked position, server echoes in both SPELL_START and SPELL_GO - Mob ground AoE (Ignite Earth, mob Flamestrike): AI script calls CastSpell(x,y,z,...), server sets 0x40 via setDestination(), coords appear in SPELL_GO - Self-centered AoE (Arcane Explosion, Hellfire): no 0x40, no dest coords - Triggered spells: dest overridden to caster position regardless of original flags - Already hooked at 0x6E7640 (spellStartDetour) -- just need to parse target mask and extract floats when 0x40 is set, emit as extra CLEU params on SPELL_CAST_START/SUCCESS - Server ref: SpellCastTargets::write() in tortoise-wow Spell.cpp lines 223-268 - Enables boss mod positional warnings (e.g. Karazhan 40-man Ignite Earth ground zones) ### Completed reference - [x] RANGE_MISSED: Detected via spell ID check (75/5019) in ProcessSpellCombatResult hook. - [x] SPELL_SUMMON / SPELL_RESURRECT / SPELL_ENERGIZE: Hook ProcessSpellEffect (0x62ACE0), spell DB Effect[] lookup. Summon: 28, 42, 56, 85-88, 104-107, 109. Resurrect: 18, 113. Energize: 30. SpellRec via WowClientDB 0xC0D780: m_recordsById(+8), m_maxId(+C), Effect[3] at +0xF4, School at +0x04, Attributes at +0x18. ### SMSG_UPDATE_AURA_DURATION (opcode 0x137) — Aura Refresh Research - Packet: `uint8 slot` + `uint32 duration_ms` (5 bytes) - Handler: multi-opcode dispatcher at 0x5E38C0, case at 0x5E3BA9 - Calls `SetActionCooldownTimer` (0x4E4390): `__fastcall(ECX=slot, EDX=duration_ms)` - Stores `current_timestamp + duration_ms` into expiry array at 0xBC5F68 (48 entries) - Server sends on both initial application AND refresh (Refresh() in SpellAuras.cpp:362) - Server only sends to the player themselves (GetTarget()->GetTypeId() == TYPEID_PLAYER) - On initial application: SMSG_UPDATE_AURA_DURATION sent BEFORE SMSG_UPDATE_OBJECT (duration sent immediately from AddSpellAuraHolder, descriptors batched at end of tick) → client descriptor slot is still empty when handler fires → detectable as "not refresh" - On refresh: descriptor already has the spell from earlier application → detectable as refresh ### CRITICAL: Wrong opcode labels from Ghidra handler dump Many labels in `/tmp/handlers_output.txt` were user-applied and WRONG. The server source (`reference/server/src/game/Protocol/Opcodes_1_12_1.h`) is the definitive reference: - 0x15B = SMSG_RESURRECT_REQUEST (was labeled "handleSpellMiss" — **WRONG**) - 0x12A = SMSG_INITIAL_SPELLS (was labeled "applySpellAura" — **WRONG**) - 0x129 = SMSG_ACTION_BUTTONS (was labeled "removeSpellAura" — **WRONG**) - 0x1F1 = MSG_SAVE_GUILD_EMBLEM (was labeled "applyDamageShield" — **WRONG**) - The SPELL_MISSED hook on 0x5E7BC0 was removed — it was hooking the resurrect handler ### Melee Hook Research Findings - 0x625E20 (originally targeted as "post-parse handler") is just a flag-setter — too simple - Dispatcher 0x6255B0 handles opcodes 0x143-0x14A via switch/jump table (byte lookup at 0x625B0C, targets at 0x625AEC) - Opcode 0x14A maps to case 5 → 0x62578D, which zeros struct, calls parser 0x625C60, then resolves unit via 0x468460 - Same FastCall(unk, opCode, unk2, CDataStore*) convention as other packet handlers - Decision: hook the shared dispatcher with opcode filter (minimal overhead, proven pattern) - Handler registered from `UpdateBindLocation` (0x625520) via `setClientIndexedPointers` (0x5AB650) ### Heal Handler Discovery - `setClientIndexedPointers(opcode, handler, 0)` is the packet handler registration function at 0x5AB650 - Registration pattern: `PUSH 0x0; MOV EDX,handler; MOV ECX,opcode; CALL 0x5AB650` - Opcode 0x150 (SMSG_SPELLHEALLOG) → handler 0x5E89C0 (registered in applySpellModifiers at 0x5E3010) - Same prologue/convention as SpellNonMeleeDmgLogHandler - Packet format verified from disassembly: targetGuid(PackedGuid), casterGuid(PackedGuid), spellId(u32), healAmount(u32), isCrit(u8) ### Verified Opcode → Handler Map (from server Opcodes_1_12_1.h) | Opcode | Packet Name | Client Handler | Status | |--------|-------------|----------------|--------| | 0x130 | SMSG_CAST_RESULT | 0x6E7330 | Hooked (SPELL_CAST_FAILED) | | 0x131 | SMSG_SPELL_START | 0x6E7640 | Hooked (SPELL_CAST_START) | | 0x132 | SMSG_SPELL_GO | 0x6E7640 | Hooked (SPELL_CAST_SUCCESS) | | 0x14A | SMSG_ATTACKERSTATEUPDATE | 0x6255B0 | Hooked (SWING_DAMAGE/MISSED) | | 0x150 | SMSG_SPELLHEALLOG | 0x5E89C0 | Hooked (SPELL_HEAL) | | 0x1F5 | SMSG_PARTYKILLLOG | 0x628890 | Hooked (PARTY_KILL) | | 0x24B | SMSG_SPELLLOGMISS | 0x5E7E00 | Downstream hooked (0x62BAB0 → SPELL_MISSED) | | 0x24C | SMSG_SPELLLOGEXECUTE | 0x5E7F90 | Downstream: 0x626A10 (INTERRUPT), 0x62DC10 (ENERGIZE), 0x62D9F0 (EXTRA_ATTACKS) | | 0x24E | SMSG_PERIODICAURALOG | 0x626DD0 | Hooked (SPELL_PERIODIC_*) | | 0x24F | SMSG_SPELLDAMAGESHIELD | 0x5E84E0 | Downstream hooked (0x62CA20 → DAMAGE_SHIELD) | | 0x250 | SMSG_SPELLNONMELEEDAMAGELOG | 0x5E85E0 | Hooked (SPELL_DAMAGE) | | 0x260 | SMSG_PROCRESIST | — | Low priority | | 0x262 | SMSG_DISPEL_FAILED | — | Low priority (failed dispels, not successful) | | 0x27B | SMSG_SPELLDISPELLOG | 0x5E8B60 | Downstream hooked (0x62D480 → SPELL_DISPEL) | | 0x32F | SMSG_SPELLINSTAKILLLOG | 0x5E85A0 | Downstream hooked (0x62CBE0 → SPELL_INSTAKILL) | ### Server Packet Formats (from tortoise-wow source) **SMSG_SPELLLOGMISS (0x24B)**: `u32 spellId, u64 casterGuid, u8 unk8, u32 targetCount, [targetCount]: u64 targetGuid, u8 missInfo, (if unk8==1: 2*f32)` **SMSG_SPELLLOGEXECUTE (0x24C)**: `PackedGuid casterGuid, u32 spellId, u32 effectCount, [effectCount]: u32 effectType, u32 entryCount, [entryCount]: (per-effect data)` Effect types (vanilla numbering — differs from WotLK!): INSTAKILL(1), POWER_DRAIN(8), RESURRECT(18), EXTRA_ATTACKS(19), CREATE_ITEM(24), OPEN_LOCK(33), ENERGIZE(59?), INTERRUPT_CAST(68), FEED_PET(101), DISMISS_PET(102), DURABILITY_DAMAGE(111). DISPEL(38), DISPEL_MECHANIC(108) route through generic ProcessSpellEffect. See "SPELLLOGEXECUTE Dispatch Table" section for full mapping. **SMSG_SPELLDAMAGESHIELD (0x24F)**: `u64 victimGuid, u64 casterGuid, u32 damage, u32 school` **SMSG_SPELLINSTAKILLLOG (0x32F)**: `u64 casterGuid, u32 spellId` (no victim — client-only) ### Downstream Hook Targets (Warden-safe) Hooking packet handlers directly risks Warden false flags. Instead hook the downstream functions called AFTER packet parsing with already-parsed data: **ProcessSpellCombatResult (0x62BAB0)** — SPELL_MISSED `__fastcall(missType_ECX, spellId_EDX, casterGuidLo, casterGuidHi, targetGuidLo, targetGuidHi, isFromSpellLogMiss)` Called by SPELLLOGMISS handler (0x5E7E00) per target. Dispatches by missType to GetSpellResistString/DodgeString/etc, then DisplayColoredMessage + LogSpellCastError(0x21E). missType enum: 2=RESIST, 3=DODGE, 4=PARRY, 5=BLOCK, 6=EVADE, 7/8=IMMUNE, 9=DEFLECT, 11=REFLECT, default=MISS **ProcessSpellDrainEffectMessage (0x62CA20)** — DAMAGE_SHIELD `__fastcall(ECX=victimGuid*, EDX=casterGuid*, stack: damage, school)` Called by SPELLDAMAGESHIELD handler (0x5E84E0). Resolves GUIDs via ValidateSpellCastAndGetObjects, displays via DisplayColoredMessage. Also fires UNIT_COMBAT (0xB6) via InitMemoryPoolWrapper. **ProcessInstaKillSpellMessage (0x62CBE0)** — SPELL_INSTAKILL `__fastcall(ECX=casterGuid*, EDX=spellId)` Called by SPELLINSTAKILLLOG handler (0x5E85A0). Only has caster + spellId, no victim. Displays via DisplayColoredMessage, no UNIT_COMBAT event. **DisplaySpellInterruptMessage (0x626A10)** — SPELL_INTERRUPT (from SPELLLOGEXECUTE) `__fastcall(ECX=casterGuid*, EDX=targetGuid*, stack: interruptedSpellId)` RET 0x4 Called from SPELLLOGEXECUTE handler for effectType 32 (INTERRUPT_CAST). Missing the interrupting spell's ID (only has interrupted spell). Ghidra name: DisplaySpellInterruptMessage. Verified: ECX→[EBP-0x10], EDX→[EBP-0xC], [EBP+0x8]=spellId. **ProcessEnergize (0x62DC10)** — SPELL_ENERGIZE (from SPELLLOGEXECUTE) `__fastcall(ECX=casterGuid*, EDX=targetGuid*, stack: spellId)` RET 0x4 Called from SPELLLOGEXECUTE for effectType 64 (TRIGGER_SPELL). Ghidra name: ProcessLockPickingValidationMessage (mislabeled). Unusual prologue: PUSH EBX; MOV EBX,ESP; AND ESP,-8. Stack param via [EBX+0x8]. Amount/powerType not available at this call site. **ProcessSpellCastEffect (0x62CD80)** — existing SPELL_DAMAGE downstream `__fastcall(ECX=targetGuid*, EDX=casterGuid*, stack: spellId, damage, casterId, resist, absorb, blocked, flags)` RET 0x1c Called by SPELLNONMELEEDAMAGELOG handler for direct (non-periodic) damage. Ghidra decompile confirms: [EBP+0x8]=spellId (DB index), [EBP+0xC]=damage, [EBP+0x10]=casterId (passed to GetSpellNameById AND compared to GetCurrentPlayerId for message formatting), [EBP+0x14]=resist, [EBP+0x18]=absorb, [EBP+0x1C]=blocked, [EBP+0x20]=flags (&2=crit). Internally calls ProcessSpellCombatResult(5,...) for full-block case. NOTE: "casterId" is NOT spell school -- it's a caster entity identifier. School is looked up from the spell DB entry, not passed as a parameter. Migration would lose school from our event. DECISION: Do NOT migrate -- uncertain param3, lose school field, existing packet parser works. **ProcessSpellPowerDrainMessage (0x62C770)** — SPELL_HEAL downstream (Ghidra mislabel, actually heal display) `__fastcall(ECX=casterGuid*, EDX=targetGuid*, stack: spellId, healAmount, isCrit)` RET 0xC Called by SpellHealLogHandler (0x5E89C0) after packet parsing. Verified from call site disassembly: ECX=LEA[EBP-0x18]=casterGuid, EDX=LEA[EBP-0x28]=targetGuid, push1=[EBP-0xC]=spellId, push2=[EBP-0x8]=healAmount(EDI), push3=isCrit(ESI, normalized bool). Calls ValidateSpellCastAndGetObjects to resolve GUIDs, then GetHealingMessageConfigString or GetCriticalHealingMessageConfigString based on isCrit. MIGRATED: replaces packet handler hook. ### Phase 3 Migration Assessment Downstream migration for Warden safety was evaluated for all 7 existing packet handler hooks: | Hook | Downstream | Assessment | |------|-----------|------------| | SpellNonMeleeDmgLogHandler | ProcessSpellCastEffect (0x62CD80) | NOT migrated -- lose school field, uncertain casterId param | | SpellHealLogHandler | ProcessSpellPowerDrainMessage (0x62C770) | MIGRATED -- clean 5-param interface | | PeriodicAuraLogHandler | None -- IS the handler | Cannot migrate -- no downstream exists | | MeleeDispatcher | 0x624F30 (thiscall, unit object ptr) | NOT migrated -- takes object pointer, not GUID | | PartyKillLogHandler | Already minimal (display only) | Cannot simplify further | | SpellStartHandler | processSpellImpactResults (0x6E7A70) | NOT migrated -- massive function (50+ vars) | | CastResultHandler | Spell_C_SpellFailed (0x6E1A00) | NOT migrated -- current hook simpler | Other downstream functions discovered during research: - `ProcessSpellSplitDamageMessage` (0x62DE60): fastcall, 2+2 params, RET 0x8. Split damage display. - `ValidatePeriodicSpellExecution` (0x62D9A0): fastcall, 2+5 params, RET 0x14. Periodic damage path. - `ProcessExtraAttacksSpellMessage` (0x62D9F0): Not heal (Ghidra mislabel), actually extra attacks. - `InitMemoryPoolWithFlag` (0x494740): UNIT_COMBAT event firing (separate from COMBAT_LOG_EVENT). ### Aura Events: No Dedicated Packets in Vanilla Vanilla 1.12.1 has NO SMSG_AURA_UPDATE packet. Aura state is communicated via SMSG_UPDATE_OBJECT (0x00A9) field updates to UNIT_FIELD_AURA slots. SPELL_AURA_APPLIED/REMOVED would require hooking the internal aura field change processing, not a packet handler. - Only sent to the casting player — source GUID from getActivePlayerGuid (0x468550) - We fire SPELL_CAST_FAILED only when status != 0; pass generic "FAILED" string (no localized failure reason mapping yet) ### Calling Convention Research All packet handlers registered via `setClientIndexedPointers` (0x5AB650) are dispatched with the same FastCall(unk, opCode, unk2, CDataStore*) → u32 convention. This is confirmed by 10 working hooks across 4 different registration callers: - 0x5E3010: SpellNonMeleeDmgLogHandler, SpellHealLogHandler - 0x626D00: PeriodicAuraLogHandler, PartyKillLogHandler - 0x625520: MeleeDispatcher - 0x6E7150: SpellStartHandler, CastResultHandler Nampower uses two C++ types (FastCallPacketHandlerT and PacketHandlerT) but hadesmem's detour system handles convention translation, so this doesn't reflect the actual dispatch ABI. ### Next Steps (priority order) 1. [x] Migrate existing packet handler hooks to downstream equivalents (Warden safety) - [x] SPELL_HEAL: 0x5E89C0 → ProcessSpellPowerDrainMessage (0x62C770). MIGRATED. - [x] SPELL_DAMAGE: Evaluated, NOT migrated (lose school field, uncertain params) - [x] All others: Evaluated, NOT migrated (see Phase 3 Migration Assessment above) 2. [x] Implement UNIT_DIED: hook HandleUnitDeath (0x605860), fastcall(ECX=unit), plain RET 3. [x] Implement SPELL_AURA_REMOVED: hook CastSpell (0x612320), thiscall(ECX=unit, slot, spellId), RET 0x8 4. [x] Implement SPELL_AURA_APPLIED: hook SetSpellTarget (0x6123F0), thiscall(ECX=unit, slot, spellId), RET 0x8 - Aura type heuristic: slot < 40 = "BUFF", slot >= 40 = "DEBUFF" 5. [x] Implement SPELL_AURA_APPLIED_DOSE/REMOVED_DOSE: hook ValidateSpellSlot (0x612450) - requestedLevel param = OLD count, live descriptor has NEW count - Fires both directions: increase → APPLIED_DOSE, decrease → REMOVED_DOSE 6. [x] Implement SPELL_EXTRA_ATTACKS: hook ProcessExtraAttacksSpellMessage (0x62D9F0) - effectType 19 in vanilla's SPELLLOGEXECUTE dispatch (NOT 33 as in WotLK) - Extra attack count not available from downstream — passed as 0 7. [x] Implement SPELL_DISPEL: hook ProcessAuraDispelMessage (0x62D480) - Dedicated SMSG_SPELLDISPELLOG handler at 0x5E8B60 calls this per dispelled aura - Has caster GUID, target GUID, dispelled aura spell ID - Dispelling spell ID unknown — passed as 0 8. [x] Implement RANGE_DAMAGE: detect Auto Shot (75) / Shoot (5019) in spell damage hook 9. [ ] Implement SPELL_AURA_REFRESH: no callback for same-spell-ID aura re-application 10. [ ] Implement SPELL_AURA_BROKEN/BROKEN_SPELL: CC break detection — deep aura system research 11. [ ] Implement SPELL_SUMMON: effectType 28+ → ProcessSpellEffect (0x62ACE0, generic — no effect type param) 12. [ ] Implement SPELL_RESURRECT: effectType 18/32/113 → ProcessSpellEffect (generic) 13. [ ] Implement RANGE_MISSED: detect Auto Shot/Shoot in SPELLLOGMISS downstream 14. [ ] Test in-game: verify all subevents fire correctly 15. [ ] Nampower conflict detection via GetModuleHandleA("nampower.dll") 16. [ ] Example addon code for COMBAT_LOG_EVENT consumption ### Phase 4: UNIT_DIED, SPELL_AURA_APPLIED/REMOVED, RANGE_DAMAGE #### Object Field Update Callback System The client registers field change callbacks via `ObjectData_UpdateField` (0x468070): ``` ObjectData_UpdateField(objectType, fieldOffset, dataSize, callbackFunc, context, flags) ``` Key registrations in `InitializeUnitEventHandlers` (0x6041F0): - `(3, 0x40, 4, 0x6046F0, 0, 0)` -- UNIT_FIELD_HEALTH -> HandleUnitHealthChange - `(3, 0xA4, 0xD8, 0x604D00, 0, 1)` -- UNIT_FIELD_AURA range -> compareAndUpdateObjectArrays - `(3, eventId+0x1AC, 1, 0x604EA0, 0, 0)` -- UNIT_FIELD_AURAAPPLICATIONS -> updateObjectWithByteValue Callbacks are `__stdcall(guidLo, guidHi, oldDataPtr, ???)` with RET 0x10 (4 stack params). The oldDataPtr contains the PREVIOUS field values before the update was applied. #### UNIT_DIED -- HandleUnitDeath (0x605860) - `__fastcall(ECX=unitObject)`, plain RET (0 stack params) - Called from `HandleUnitHealthChange` (0x6046F0) when `newHealth < 1 && oldHealth > 0` - Single xref from HandleUnitHealthChange -- only fires on actual death transition - GUID access: `*(*(unitObject+8))` = GUID lo, `*(*(unitObject+8)+4)` = GUID hi - Also handles: spell interruption, target clearing, action queue cleanup, event notifications ``` HandleUnitHealthChange (0x6046F0) __stdcall(guidLo, guidHi, oldHealthPtr, ???) -> resolves unit object via ClntObjMgrObjectPtr -> if newHealth < 1 && oldHealth > 0: HandleUnitDeath(unitObject) -> if newHealth > 0 && oldHealth < 1: HandleUnitResurrection(unitObject) ``` Hook target: `HandleUnitDeath` (0x605860) -- fire `UNIT_DIED` with target GUID, no source. #### SPELL_AURA_APPLIED/REMOVED -- Aura Field Change Callback The aura callback at 0x604D00 (`compareAndUpdateObjectArrays`) is called when UNIT_FIELD_AURA descriptor fields change (offsets 0xA4-0x17B, 48 spell ID slots + flags). It receives: - `dataArray` = pointer to OLD aura data (before update was applied) - Current (NEW) data is in the live descriptor at `*(unitObject+0x110)` The callback iterates all 48 slots in two passes: 1. **Pass 1 (removal)**: old aura active + new NOT active -> `CastSpell(unit, slot, oldSpellId)` 2. **Pass 2 (application)**: old NOT active + new IS active -> `SetSpellTarget(unit, slot, newSpellId)` Active check: spellId != 0 AND aura flags byte has bits set. Aura flags at old offset 0xC0 relative to dataArray, at 0x164 relative to live descriptor. Flags are packed: 2 aura slots per byte, 4 bits each, mask 0x0E for flag bits. **CastSpell (0x612320)** -- AURA REMOVED notification - `__thiscall(ECX=unitObject, stack: slotIndex, spellId)`, RET 0x8 - Single xref from compareAndUpdateObjectArrays (safe to hook) - Does: IsActivePlayer check, camera FOV reset (for certain effects), UpdateActionsBySpellId **SetSpellTarget (0x6123F0)** -- AURA APPLIED notification - `__thiscall(ECX=unitObject, stack: slotIndex, spellId)`, RET 0x8 - Single xref from compareAndUpdateObjectArrays (safe to hook) - Does: SetUnitTargetById, CGUnit_ProcessAuraSlot, HandleSpellManaCost, UpdateActionsBySpellId - slotIndex 0x20-0x2F (32-47) treated as "valid spell range" for mana cost handling **ValidateSpellSlot (0x612450)** -- AURA DOSE change (from aura count callback 0x604EA0) - `__thiscall(ECX=unitObject, stack: slotIndex, requestedLevel)`, called when stack count changes - Could be used for SPELL_AURA_APPLIED_DOSE/REMOVED_DOSE **Limitation**: Source GUID (who applied the aura) is NOT available from field updates. No UNIT_FIELD_AURA_CREATOR equivalent in vanilla 1.12.1. Fire with source=GUID_ZERO. #### RANGE_DAMAGE -- Ranged Auto-Attack Detection Vanilla ranged auto-attacks use the same SMSG_ATTACKERSTATEUPDATE packet as melee. No distinct opcode or hitInfo flag for ranged. To distinguish: - Check attacker's UNIT_VIRTUAL_ITEM_SLOT_DISPLAY[2] (ranged weapon slot) - Field index 0x14, byte offset 0x50 from descriptor start - From stored ptr at obj+0x110: offset 0x38 - If non-zero, the unit has a ranged weapon equipped - Cross-reference with the attack being an auto-attack (no spellId in the packet) Lower priority -- most addons treat ranged auto-attacks as SWING_DAMAGE. ### SPELLLOGEXECUTE Dispatch Table (0x5E7F90 — Phase 5 Ghidra Analysis) Jump table at 0x5E8430 with index byte table at 0x5E845B (126 entries, guard CMP ECX,0x7D). | Case | effectTypes | Downstream Function | |------|------------|-------------------| | 0 | 0(NONE), 1(INSTAKILL) | ProcessSpellEffect (0x62ACE0) with target | | 1 | 8(POWER_DRAIN) | ProcessStandardPowerDrainMessage (0x62DBF0) | | 2 | 18(RESURRECT), 38(DISPEL), 63, 69, 79, 91, 108(DISPEL_MECHANIC), 113, 114, 116, 125 | ProcessSpellEffect (0x62ACE0) with target | | 3 | 19(EXTRA_ATTACKS in vanilla!) | ProcessExtraAttacksSpellMessage (0x62D9F0) | | 4 | 24(CREATE_ITEM) | ProcessEnchantItemValidationMessage (0x62D8D0) | | 5 | 33(OPEN_LOCK in vanilla), 59 | 0x62DC10 (OPEN_LOCK display — NOT energize) | | 6 | 68(INTERRUPT_CAST) | DisplaySpellInterruptMessage (0x626A10) | | 7 | 101(FEED_PET) | ProcessFeedPetValidationMessage (0x62D800) | | 8 | 102(DISMISS_PET) | ProcessPetDismissMessage (0x62E360) | | 9 | 111(DURABILITY_DAMAGE) | ProcessAllDurabilityDamageMessage (0x62E190) | | 10 | Everything else (default) | ProcessSpellEffect (0x62ACE0) with NULL target | **CRITICAL**: Vanilla effectType numbering differs from WotLK! effectType 19 = EXTRA_ATTACKS (not 33). effectType 33 = OPEN_LOCK (calls 0x62DC10, which we hook as SPELL_ENERGIZE — potential false fires). effectType 30 (ENERGIZE) falls to default → ProcessSpellEffect, NOT 0x62DC10. ProcessSpellEffect (0x62ACE0) is a generic display function: checks spell DB entry flags, filters by effect slot types, then calls ProcessHealingSpellEffect (with target) or CalculateSpellPower (without target). No effect type parameter — cannot distinguish DISPEL/SUMMON/RESURRECT from inside this function. ### SMSG_SPELLDISPELLOG — Dedicated Dispel Handler **Handler**: 0x5E8B60 (registered in applySpellModifiers 0x5E3010) **Convention**: __stdcall(opcode, CDataStore*), RET 0x8 **Packet format**: PackedGuid casterGuid, PackedGuid targetGuid, u32 count, [count × u32 spellId] **Downstream**: ProcessAuraDispelMessage (0x62D480) per dispelled aura **ProcessAuraDispelMessage (0x62D480)**: `__fastcall(ECX=casterGUID_ptr, EDX=targetGUID_ptr, stack: dispelledSpellId)`, RET 0x4 Displays AURADISPELSELF/AURADISPELOTHER. Only has the dispelled aura's spell ID, not the dispelling spell. Hooked for SPELL_DISPEL. ### CDataStore Structure ``` +0x00: vtable ptr +0x04: m_buffer (u8 pointer) +0x08: m_base +0x0C: m_alloc +0x10: m_size +0x14: m_read (current read position) ``` ### Event Slot Layout ``` Nampower: 549 → SPELL_DAMAGE_EVENT_SELF (0xBE1A2C) [overwritten by us] 550 → SPELL_DAMAGE_EVENT_OTHER (0xBE1A30) Ours: 549 → COMBAT_LOG_EVENT (0xBE1A2C) [unified, overwrites nampower 549] FrameScript_CreateEvents maxEventId bumped to 550 (NOT 551 — slot 550 is a float global). 0xBE1A30 (slot 550) = float 0.25 (0x3E800000). Addresses 0xBE1A30+ are float globals — DO NOT USE. ``` --- ## Additional Function Map (from latest Ghidra analysis) ### Combat Message Formatting Chain ``` ProcessSpellDamageWithLocalization (0x629d30) -> GetCombatSchoolHitMessage (0x62a080) -- hit message key -> GetCombatMissMessage (0x62a0d0) -- miss message key -> LogCombatMessage (0x6268f0) -- fires event 0x21e ``` ### Healing Functions | Address | Function | Called By | |---------|----------|-----------| | 0x622420 | ProcessHealingSpell | UpdateSpellEffects, HandleCombatAction, ProcessSpellDamage, etc. | | 0x62adc0 | ProcessHealingSpellEffect | ProcessSpellEffect (0x62ace0) | | 0x62c940 | GetHealingMessageConfigString | | | 0x62c9a0 | GetCriticalHealingMessageConfigString | | | 0x627440 | GetPeriodicHealMessageKey | | ### Spell Effect Processing | Address | Function | Notes | |---------|----------|-------| | 0x62ace0 | ProcessSpellEffect | Calls ProcessHealingSpellEffect | | 0x629d30 | ProcessSpellDamageWithLocalization | __fastcall(spellData, sourceGuid, targetGuid, flags, dmg, absorb, resist) | | 0x628100 | ExecuteSpellWithLocalizedText | Called for periodic effects | | 0x62aac0 | ProcessEnvironmentalDamage | Fall/fire/lava/drown | | 0x62cd80 | ProcessSpellCastEffect | | | 0x626a10 | DisplaySpellInterruptMessage | | | 0x62a710 | DisplayAdvancedSpellLogMessage | | ### Combat Message System Init | Address | Function | |---------|----------| | 0x626d00 | InitializeCombatMessageSystem | | 0x626810 | GetSpellTextByIndex | | 0x626850 | DisplayColoredMessage | | 0x6264b0 | GetSpellNameById | | 0x6264e0 | GetObjectName | | 0x626630 | ValidateSpellCastAndGetObjects | ### Melee Combat - `ProcessPlayerAutoAttack` (0x5ecb70) -- called from SetPlayerTarget, UpdatePlayerState - `processAutoAttack` (0x6e6900) - Melee hits are part of SMSG_ATTACKERSTATEUPDATE (opcode 0x14A) -- handler not yet identified - The packet comes through the general update pipeline: `ProcessComplexUnitUpdateWithCameraSync` (0x619320) -> `ProcessUnitUpdateWithTimingValidation` (0x618c30) -> `PacketHandler_Wrapper_Generic2` (0x602d00) ### Key Nampower-Confirmed Address Cross-References | Our Name | Nampower Name | Address | |----------|--------------|---------| | FireLuaEvent | SignalEventParam | 0x703F50 | | SignalEvent (no params) | SignalEvent | 0x703E50 | | GetObjectByGUID | GetObjectPtr | 0x464870 | | ClntObjMgrGetActivePlayer | GetActivePlayer | 0x468550 | | FrameScript_CreateEvents | FrameScript_CreateEvents | 0x703D90 | | FrameScript_RegisterFunction | FrameScript_RegisterFunction | 0x704120 | | GetSpellNameById | (SpellDb at 0xC0D780) | 0x6264b0 | --- ## Missing WotLK CLEU Subevents Events present in WotLK 3.3.5 COMBAT_LOG_EVENT_UNFILTERED but not yet implemented. ### Loot Events (novel -- never existed in any WoW combat log) LOOT was never a COMBAT_LOG_EVENT subevent in any WoW expansion. Loot tracking addons use separate Lua events (LOOT_OPENED, CHAT_MSG_LOOT, etc.), not the combat log. Adding loot to our combat log would be a novel extension, not WotLK parity. Relevant vanilla packets if we ever want to add this: | Packet | Opcode | Format | |--------|--------|--------| | SMSG_ITEM_PUSH_RESULT | 0x166 | playerGUID(8), received(4), created(4), showInChat(4), bagSlot(1), itemSlot(4), itemID(4), suffix(4), randomProp(4), count(4) | | SMSG_LOOT_ROLL | 0x2A2 | targetGUID(8), slot(4), rollerGUID(8), itemID(4), suffix(4), randomProp(4), rollNum(1), rollType(1) | | SMSG_LOOT_ROLL_WON | 0x29F | targetGUID(8), slot(4), itemID(4), suffix(4), randomProp(4), winnerGUID(8), rollNum(1), rollType(1) | | SMSG_LOOT_ALL_PASSED | 0x29E | targetGUID(8), slot(4), itemID(4), randomProp(4), suffix(4) | | SMSG_LOOT_MONEY_NOTIFY | 0x163 | amount(4) | ### Enchant Events (verified in WotLK/Cata logs) Confirmed present in real WotLK combat logs. No vanilla packet equivalent -- would need to hook the client enchant application/removal functions. | Subevent | Description | |----------|-------------| | ENCHANT_APPLIED | Item enchanted (temp or permanent) | | ENCHANT_REMOVED | Enchant removed/expired | Verified WotLK format: ``` ENCHANT_APPLIED,srcGUID,srcName,srcFlags,srcRaidFlags,dstGUID,dstName,dstFlags,dstRaidFlags,"enchantName",itemID,"itemName" ENCHANT_REMOVED,0x0000000000000000,nil,0x80000000,0x80000000,dstGUID,dstName,dstFlags,dstRaidFlags,"enchantName",itemID,"itemName" ``` Note: ENCHANT_REMOVED source is always null GUID (no "remover"). ### SPELL_CREATE (verified in WotLK/Cata logs) Fires when a player creates a game object (trap, totem, feast, cauldron). Uses standard spell prefix. Dest is the created GO's GUID. ``` SPELL_CREATE,srcGUID,srcName,srcFlags,srcRaidFlags,goGUID,"goName",goFlags,goRaidFlags,spellID,"spellName",spellSchool ``` ### Combat Events (low priority) | Subevent | Description | Notes | |----------|-------------|-------| | UNIT_DISSIPATES | Pet/totem/guardian despawn | Variant of UNIT_DIED, not in WotLK sample but in retail | | SPELL_ABSORBED | Damage absorbed by shield | Present in Shadowlands+, not in WotLK sample | | SPELL_HEAL_ABSORBED | Healing absorbed by anti-heal | Shadowlands+, no vanilla mechanic | | SWING_DAMAGE_LANDED | Melee hit after absorb | Shadowlands+, not in WotLK | | SPELL_BUILDING_* | Structure damage prefix | WotLK siege content, N/A in vanilla | | *_DURABILITY_DAMAGE | Equipment durability loss | Rarely useful | ### Metadata Events (Shadowlands+ only, NOT in WotLK) These do NOT appear in WotLK/Cata combat logs. They were added in later expansions (combat log version 16+). Listed for reference only. | Subevent | Description | First seen | |----------|-------------|------------| | ENCOUNTER_START | Boss encounter begins | Shadowlands+ | | ENCOUNTER_END | Boss encounter ends | Shadowlands+ | | ZONE_CHANGE | Player changes zone | Shadowlands+ | | COMBATANT_INFO | Gear/talent snapshot | Shadowlands+ | | CHALLENGE_MODE_START/END | M+ key start/end | Shadowlands+ | | EMOTE | Boss emote text | Shadowlands+ | | MAP_CHANGE | Map transition | Shadowlands+ | | ARENA_MATCH_START/END | Arena match | Shadowlands+ | ### Current Implementation Status 37 subevents implemented across 23 hooks (Phases 1-5 complete): - Damage: SWING/RANGE/SPELL/PERIODIC/SHIELD/SPLIT/ENVIRONMENTAL (7) - Missed: SWING/RANGE/SPELL/PERIODIC/SHIELD (5) - Heal: SPELL/PERIODIC (2) - Power: ENERGIZE/DRAIN/PERIODIC_ENERGIZE/PERIODIC_DRAIN/PERIODIC_LEECH (5) - Aura: APPLIED/REMOVED/DOSE(x2)/REFRESH/BROKEN/BROKEN_SPELL (7) - Cast: START/SUCCESS/FAILED (3) - Misc: INTERRUPT/DISPEL/DISPEL_FAILED/STOLEN/EXTRA_ATTACKS/SUMMON/RESURRECT/INSTAKILL (8) - Death: UNIT_DIED/UNIT_DESTROYED/PARTY_KILL (3)