Files
WeirdUtils/src/dpslog/RESEARCH.md
T
MarcelineVQ b12fc820ef Publish source: Unlicense, public README, repo hygiene
The remote was previously a distribution-only point for pre-built DLLs.
This opens the source.

- LICENSE: Unlicense, with a GPL-3.0 carve-out for src/dpslog/WeirdDPSMate
  (a DPSMate fork that keeps its own license)
- README.md replaces the stale internal one with the user-facing docs from
  DLL_README.md, swapping the 'Why No Source Code?' section for build and
  layout notes. DLL_README.md is dropped; one README now serves both.
- RELEASING.md: drop the trim-the-README-per-release dance and the
  remote/WeirdUtils/ distribution clone, both obsolete now
- gitignore agent/editor scratch, build caches, the vendored WSBT addon,
  and the WeirdThreat/uwu-logs checkouts (separate upstream repos)
- Commit outstanding module work: superweirdo, clickthrough portal visuals,
  transform44 decompiles, worldmarkers demo presets, tools/
2026-07-27 21:47:55 -07:00

55 KiB
Raw Blame History

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:

  • CHAT_MSG_SPELL_SELF_DAMAGE (spell hits/crits/misses/parries/dodges/resists/absorbs)
  • CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE
  • CHAT_MSG_SPELL_PARTY_DAMAGE
  • CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE
  • CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE
  • CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE
  • CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE
  • CHAT_MSG_SPELL_PET_DAMAGE
  • CHAT_MSG_SPELL_DAMAGESHIELDS_ON_SELF (damage shields are spell damage)
  • 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:

  • CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE
  • CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE
  • CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE
  • CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE
  • CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE
  • CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS (periodic heal component)
  • CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS (periodic heal component)
  • CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS (periodic heal component)
  • 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):

  • CHAT_MSG_COMBAT_SELF_HITS
  • CHAT_MSG_COMBAT_SELF_MISSES
  • CHAT_MSG_COMBAT_PARTY_HITS
  • CHAT_MSG_COMBAT_PARTY_MISSES
  • CHAT_MSG_COMBAT_FRIENDLYPLAYER_HITS
  • CHAT_MSG_COMBAT_FRIENDLYPLAYER_MISSES
  • CHAT_MSG_COMBAT_HOSTILEPLAYER_HITS
  • CHAT_MSG_COMBAT_HOSTILEPLAYER_MISSES
  • CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS
  • CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES
  • CHAT_MSG_COMBAT_CREATURE_VS_PARTY_HITS
  • CHAT_MSG_COMBAT_CREATURE_VS_PARTY_MISSES
  • CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_HITS
  • CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_MISSES
  • CHAT_MSG_COMBAT_PET_HITS
  • 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:

  • CHAT_MSG_SPELL_SELF_BUFF (heal component: "Your X heals Y for Z")
  • CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF (heal component: "X's Y heals Z for W")
  • CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF (heal component)
  • 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):

  • SPELL_AURA_APPLIED: SetSpellTarget (0x6123F0) — aura field inactive→active transition
  • SPELL_AURA_REMOVED: CastSpell (0x612320) — aura field active→inactive transition
  • SPELL_AURA_APPLIED_DOSE: ValidateSpellSlot (0x612450) — stack count increase
  • SPELL_AURA_REMOVED_DOSE: ValidateSpellSlot (0x612450) — stack count decrease
  • 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 ✓

  • UNIT_DIED: HandleUnitDeath (0x605860) — called from HandleUnitHealthChange on death transition
  • 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:

  • Unified COMBAT_LOG_EVENT at slot 549 (WotLK-style subevents)
  • FrameScript_CreateEvents hook — guarded for main table only, bump to 550
  • CDataStore read helpers (cdsGet, cdsGetPackedGuid, save/restore m_read)
  • GUID-to-string conversion (4-buffer rotating hex formatter)
  • Fire functions: fireCombatLog, fireSwingMissed, fireSpellStr, fireSpell, fireBase, fireSpellStrD, fireSpellDispel
  • Event string pointer base verified: 0xBE1198 (derived from nampower 549→0xBE1A2C)

Phase 1-2 — Packet handler hooks:

  • SpellNonMeleeDmgLogHandler (0x5E85E0) → SPELL_DAMAGE + RANGE_DAMAGE (opcode 0x250)
  • PeriodicAuraLogHandler (0x626DD0) → SPELL_PERIODIC_DAMAGE/HEAL/ENERGIZE/LEECH (opcode 0x24E)
  • MeleeDispatcher (0x6255B0) → SWING_DAMAGE / SWING_MISSED (opcode 0x14A filter)
  • PartyKillLogHandler (0x628890) → PARTY_KILL (opcode 0x1F5)
  • SpellStartHandler (0x6E7640) → SPELL_CAST_START (0x131) / SPELL_CAST_SUCCESS (0x132)
  • CastResultHandler (0x6E7330) → SPELL_CAST_FAILED (opcode 0x130, status != 0)

Phase 3 — Downstream hooks (Warden-safe):

  • ProcessSpellPowerDrainMessage (0x62C770) → SPELL_HEAL (migrated from packet handler)
  • ProcessSpellCombatResult (0x62BAB0) → SPELL_MISSED
  • ProcessSpellDrainEffectMessage (0x62CA20) → DAMAGE_SHIELD
  • DisplaySpellInterruptMessage (0x626A10) → SPELL_INTERRUPT
  • ProcessInstaKillSpellMessage (0x62CBE0) → SPELL_INSTAKILL
  • ProcessEnergize (0x62DC10) → removed, was OPEN_LOCK (effectType 33), not energize
  • ProcessEnvironmentalDamage (0x62AAC0) → ENVIRONMENTAL_DAMAGE

Phase 4 — Death and aura field change hooks:

  • HandleUnitDeath (0x605860) → UNIT_DIED (fastcall, ECX=unit, plain RET)
  • SetSpellTarget (0x6123F0) → SPELL_AURA_APPLIED (thiscall, RET 0x8)
  • CastSpell (0x612320) → SPELL_AURA_REMOVED (thiscall, RET 0x8)

Phase 5 — Dose, extra attacks, dispel, ranged detection:

  • ValidateSpellSlot (0x612450) → SPELL_AURA_APPLIED_DOSE / SPELL_AURA_REMOVED_DOSE (thiscall, RET 0x8)
  • ProcessExtraAttacksSpellMessage (0x62D9F0) → SPELL_EXTRA_ATTACKS (fastcall, RET 0x4)
  • ProcessAuraDispelMessage (0x62D480) → SPELL_DISPEL (fastcall, RET 0x4)
  • RANGE_DAMAGE — detected via spell ID check (75=Auto Shot, 5019=Shoot) in spell damage hook
  • RANGE_MISSED — detected via spell ID check (75/5019) in spell missed hook
  • 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:

  • 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
  • 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
  • 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
  • ENERGIZE false-fire bug fixed: removed 0x62DC10 hook, moved detection to ProcessSpellEffect with spell DB filter
  • 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

  • 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

  • RANGE_MISSED: Detected via spell ID check (75/5019) in ProcessSpellCombatResult hook.
  • 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. Migrate existing packet handler hooks to downstream equivalents (Warden safety)
    • SPELL_HEAL: 0x5E89C0 → ProcessSpellPowerDrainMessage (0x62C770). MIGRATED.
    • SPELL_DAMAGE: Evaluated, NOT migrated (lose school field, uncertain params)
    • All others: Evaluated, NOT migrated (see Phase 3 Migration Assessment above)
  2. Implement UNIT_DIED: hook HandleUnitDeath (0x605860), fastcall(ECX=unit), plain RET
  3. Implement SPELL_AURA_REMOVED: hook CastSpell (0x612320), thiscall(ECX=unit, slot, spellId), RET 0x8
  4. Implement SPELL_AURA_APPLIED: hook SetSpellTarget (0x6123F0), thiscall(ECX=unit, slot, spellId), RET 0x8
    • Aura type heuristic: slot < 40 = "BUFF", slot >= 40 = "DEBUFF"
  5. 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. 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. 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. 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)