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/
55 KiB
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 + GUIDsProcessChatText(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. CallsValidateSpellCastAndGetObjectsfirst 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), firesFireLuaEvent(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 atPTR_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_HITSetc. are registered inInitializeGameEventTablewith their own event IDs. The mapping from queue message type to event ID happens inProcessChatText.
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_UNFILTEREDevent 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 eventSignalEventParam(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
- Guard count bump:
if (param1 == 0xBE1198 and new_max < 551)— only expand main table - Single event slot 549 (verified safe by nampower) instead of 551-555
- 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_msinto 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) viasetClientIndexedPointers(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)
- 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)
- Implement UNIT_DIED: hook HandleUnitDeath (0x605860), fastcall(ECX=unit), plain RET
- Implement SPELL_AURA_REMOVED: hook CastSpell (0x612320), thiscall(ECX=unit, slot, spellId), RET 0x8
- Implement SPELL_AURA_APPLIED: hook SetSpellTarget (0x6123F0), thiscall(ECX=unit, slot, spellId), RET 0x8
- Aura type heuristic: slot < 40 = "BUFF", slot >= 40 = "DEBUFF"
- 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
- 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
- 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
- Implement RANGE_DAMAGE: detect Auto Shot (75) / Shoot (5019) in spell damage hook
- Implement SPELL_AURA_REFRESH: no callback for same-spell-ID aura re-application
- Implement SPELL_AURA_BROKEN/BROKEN_SPELL: CC break detection — deep aura system research
- Implement SPELL_SUMMON: effectType 28+ → ProcessSpellEffect (0x62ACE0, generic — no effect type param)
- Implement SPELL_RESURRECT: effectType 18/32/113 → ProcessSpellEffect (generic)
- Implement RANGE_MISSED: detect Auto Shot/Shoot in SPELLLOGMISS downstream
- Test in-game: verify all subevents fire correctly
- Nampower conflict detection via GetModuleHandleA("nampower.dll")
- 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) whennewHealth < 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:
- Pass 1 (removal): old aura active + new NOT active ->
CastSpell(unit, slot, oldSpellId) - 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, UpdatePlayerStateprocessAutoAttack(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)