Add dpslog module: unified COMBAT_LOG_EVENT with 35 subevents

Hook 23 packet handlers / internal functions to fire a single
COMBAT_LOG_EVENT with WotLK-style subevent strings and structured
args (spellId, amount, school, etc.) for addon consumption.

Event registration writes directly into the internal FrameScript
event table at slot 650, with a resize_lua_event_array hook that
expands capacity to 700 when needed (compatible with SuperWoW).

Includes Lua tracker addon (WeirdUtils_DPSLog) with popup checklist
UI, per-subevent chat output, and /dpslog slash command.
This commit is contained in:
MarcelineVQ
2026-03-11 21:45:24 -07:00
parent 3fdda58f09
commit 65bc234c08
4 changed files with 2374 additions and 306 deletions
+473 -68
View File
@@ -319,8 +319,8 @@ Covers these DPSMate events:
### Category 3: Melee Damage — DONE ✓
**Packet**: SMSG_ATTACKERSTATEUPDATE (opcode 0x14A)
**Needed event**: `COMBAT_LOG_MELEE` (allocate slot 554)
**Fields needed**: targetGUID, casterGUID, damage, school, hitInfo, blocked, absorbed, resisted, victimState
**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).
@@ -371,22 +371,22 @@ Research needed: verify 0x625E20 signature, what data is accessible, whether it'
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
- [x] CHAT_MSG_COMBAT_SELF_HITS
- [x] CHAT_MSG_COMBAT_SELF_MISSES
- [x] CHAT_MSG_COMBAT_PARTY_HITS
- [x] CHAT_MSG_COMBAT_PARTY_MISSES
- [x] CHAT_MSG_COMBAT_FRIENDLYPLAYER_HITS
- [x] CHAT_MSG_COMBAT_FRIENDLYPLAYER_MISSES
- [x] CHAT_MSG_COMBAT_HOSTILEPLAYER_HITS
- [x] CHAT_MSG_COMBAT_HOSTILEPLAYER_MISSES
- [x] CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS
- [x] CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES
- [x] CHAT_MSG_COMBAT_CREATURE_VS_PARTY_HITS
- [x] CHAT_MSG_COMBAT_CREATURE_VS_PARTY_MISSES
- [x] CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_HITS
- [x] CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_MISSES
- [x] CHAT_MSG_COMBAT_PET_HITS
- [x] CHAT_MSG_COMBAT_PET_MISSES
**SMSG_ATTACKERSTATEUPDATE packet format** (from mangos/wowdev):
```
@@ -412,51 +412,50 @@ Tracing approaches:
- Trace from `ProcessUnitUpdateWithTimingValidation` (0x618c30)
- Search for callers of functions that reference melee hit strings
### Category 4: Direct Heals — TODO
### Category 4: Direct Heals — DONE ✓
**Packet**: SMSG_SPELLHEALLOG
**Handler**: Trace from `ProcessHealingSpell` (0x622420) callers
**Our event**: `COMBAT_LOG_HEAL` (slot 553 — already registered, no hook yet)
**Fields needed**: targetGUID, casterGUID, spellId, healAmount, isCrit
**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)
- [x] CHAT_MSG_SPELL_SELF_BUFF (heal component: "Your X heals Y for Z")
- [x] CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF (heal component: "X's Y heals Z for W")
- [x] CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF (heal component)
- [x] CHAT_MSG_SPELL_PARTY_BUFF (heal component)
### Category 5: Environmental Damage — TODO (low priority)
### Category 5: Environmental Damage — DONE ✓
**Handler**: `ProcessEnvironmentalDamage` (0x62aac0)
**Fields needed**: targetGUID, damageType (fall/fire/lava/drown/slime), amount
**Handler**: `ProcessEnvironmentalDamage` (0x62aac0) — downstream function, not a packet handler
**Our event**: `COMBAT_LOG_ENV_DMG` (slot 555)
**Fields**: victimGUID, damageType, baseDamage, absorb
DPSMate currently infers these from CHAT_MSG_COMBAT_SELF_HITS / FRIENDLYPLAYER_HITS
by matching strings like "fall and lose", "swimming in lava", "suffer X points of
fire damage", "drowning and lose". Our melee/spell hooks won't cover these since
they come from a separate opcode (SMSG_ENVIRONMENTALDAMAGELOG).
damageType values: 0=EXHAUSTED(fatigue), 1=DROWNING, 2=FALL, 3=LAVA, 4=SLIME, 5=FIRE
### Category 6: Aura Gain/Loss — TODO (low priority)
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.
No single packet handler — these are derived from SMSG_AURA_UPDATE and various
spell effect handlers. DPSMate uses:
- [ ] CHAT_MSG_SPELL_AURA_GONE_SELF
- [ ] CHAT_MSG_SPELL_AURA_GONE_OTHER
- [ ] CHAT_MSG_SPELL_AURA_GONE_PARTY
- [ ] CHAT_MSG_SPELL_BREAK_AURA (dispel confirmation)
- [ ] PLAYER_AURAS_CHANGED
### Category 6: Aura Gain/Loss — DONE ✓
These track: buff uptime, debuff uptime, proc tracking, shield absorb tracking,
CC tracking, HoT application/removal. Complex — may be better to let addons
continue using the existing text events for these until we have more bandwidth.
Hooked via object field update callbacks (no packet handler — vanilla uses SMSG_UPDATE_OBJECT):
- [x] SPELL_AURA_APPLIED: SetSpellTarget (0x6123F0) — aura field inactive→active transition
- [x] SPELL_AURA_REMOVED: CastSpell (0x612320) — aura field active→inactive transition
- [x] SPELL_AURA_APPLIED_DOSE: ValidateSpellSlot (0x612450) — stack count increase
- [x] SPELL_AURA_REMOVED_DOSE: ValidateSpellSlot (0x612450) — stack count decrease
- [x] SPELL_DISPEL: ProcessAuraDispelMessage (0x62D480) — from SMSG_SPELLDISPELLOG
### Category 7: Deaths — TODO (low priority)
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).
- [ ] CHAT_MSG_COMBAT_FRIENDLY_DEATH ("X dies.", "X is slain by Y")
- [ ] CHAT_MSG_COMBAT_HOSTILE_DEATH ("X dies.")
Still missing:
- [ ] SPELL_AURA_REFRESH: no callback for same-spell re-application
- [ ] SPELL_AURA_BROKEN / BROKEN_SPELL: CC break detection
DPSMate uses these to call `DB:UnregisterDeath(source)`. Could fire from
SMSG_DESTROYOBJECT or unit flag changes, but low impact — death events are
infrequent and the string parsing is trivial.
### Category 7: Deaths — DONE ✓
- [x] UNIT_DIED: HandleUnitDeath (0x605860) — called from HandleUnitHealthChange on death transition
- [x] PARTY_KILL: PartyKillLogHandler (0x628890) — from SMSG_PARTYKILLLOG packet
### Category 8: Interrupts — Embedded in spell damage events
@@ -474,19 +473,153 @@ 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
- [x] Custom event registration (slots 551-554, FrameScript_CreateEvents hook, maxId 556)
### Done — 32 subevents across 23 hooks (22 hook addresses + wow.zig import)
**Infrastructure:**
- [x] Unified COMBAT_LOG_EVENT at slot 549 (WotLK-style subevents)
- [x] FrameScript_CreateEvents hook — guarded for main table only, bump to 550
- [x] CDataStore read helpers (cdsGet, cdsGetPackedGuid, save/restore m_read)
- [x] GUID-to-string conversion
- [x] SignalEventParam typed wrappers
- [x] SpellNonMeleeDmgLogHandler hook (0x5E85E0) → COMBAT_LOG_SPELL_DMG
- [x] PeriodicAuraLogHandler hook (0x626DD0) → COMBAT_LOG_PERIODIC
- [x] SpellHealLogHandler hook (0x5E89C0) → COMBAT_LOG_HEAL (opcode 0x150)
- [x] MeleeDispatcher hook (0x6255B0) → COMBAT_LOG_MELEE (opcode 0x14A filter)
- [x] GUID-to-string conversion (4-buffer rotating hex formatter)
- [x] Fire functions: fireCombatLog, fireSwingMissed, fireSpellStr, fireSpell, fireBase, fireSpellStrD, fireSpellDispel
- [x] Event string pointer base verified: 0xBE1198 (derived from nampower 549→0xBE1A2C)
**Phase 1-2 — Packet handler hooks:**
- [x] SpellNonMeleeDmgLogHandler (0x5E85E0) → SPELL_DAMAGE + RANGE_DAMAGE (opcode 0x250)
- [x] PeriodicAuraLogHandler (0x626DD0) → SPELL_PERIODIC_DAMAGE/HEAL/ENERGIZE/LEECH (opcode 0x24E)
- [x] MeleeDispatcher (0x6255B0) → SWING_DAMAGE / SWING_MISSED (opcode 0x14A filter)
- [x] PartyKillLogHandler (0x628890) → PARTY_KILL (opcode 0x1F5)
- [x] SpellStartHandler (0x6E7640) → SPELL_CAST_START (0x131) / SPELL_CAST_SUCCESS (0x132)
- [x] CastResultHandler (0x6E7330) → SPELL_CAST_FAILED (opcode 0x130, status != 0)
**Phase 3 — Downstream hooks (Warden-safe):**
- [x] ProcessSpellPowerDrainMessage (0x62C770) → SPELL_HEAL (migrated from packet handler)
- [x] ProcessSpellCombatResult (0x62BAB0) → SPELL_MISSED
- [x] ProcessSpellDrainEffectMessage (0x62CA20) → DAMAGE_SHIELD
- [x] DisplaySpellInterruptMessage (0x626A10) → SPELL_INTERRUPT
- [x] ProcessInstaKillSpellMessage (0x62CBE0) → SPELL_INSTAKILL
- [x] ~~ProcessEnergize (0x62DC10)~~ → removed, was OPEN_LOCK (effectType 33), not energize
- [x] ProcessEnvironmentalDamage (0x62AAC0) → ENVIRONMENTAL_DAMAGE
**Phase 4 — Death and aura field change hooks:**
- [x] HandleUnitDeath (0x605860) → UNIT_DIED (fastcall, ECX=unit, plain RET)
- [x] SetSpellTarget (0x6123F0) → SPELL_AURA_APPLIED (thiscall, RET 0x8)
- [x] CastSpell (0x612320) → SPELL_AURA_REMOVED (thiscall, RET 0x8)
**Phase 5 — Dose, extra attacks, dispel, ranged detection:**
- [x] ValidateSpellSlot (0x612450) → SPELL_AURA_APPLIED_DOSE / SPELL_AURA_REMOVED_DOSE (thiscall, RET 0x8)
- [x] ProcessExtraAttacksSpellMessage (0x62D9F0) → SPELL_EXTRA_ATTACKS (fastcall, RET 0x4)
- [x] ProcessAuraDispelMessage (0x62D480) → SPELL_DISPEL (fastcall, RET 0x4)
- [x] RANGE_DAMAGE — detected via spell ID check (75=Auto Shot, 5019=Shoot) in spell damage hook
- [x] RANGE_MISSED — detected via spell ID check (75/5019) in spell missed hook
- [x] ProcessSpellEffect (0x62ACE0) → SPELL_SUMMON / SPELL_RESURRECT / SPELL_ENERGIZE (spell DB Effect[] lookup)
- ENERGIZE moved here: effectType 30 falls to default case → ProcessSpellEffect, not 0x62DC10
**Phase 6 — Aura refresh + bug fixes:**
- [x] SetActionCooldownTimer (0x4E4390) → SPELL_AURA_REFRESH (⚠ LOCAL PLAYER ONLY)
- Hooked from SMSG_UPDATE_AURA_DURATION (opcode 0x137) handler
- Detects refresh by checking if aura slot already has active spell in descriptors
- Vanilla only sends duration updates to the buffed player — other units' refreshes are invisible
- [x] SPELL_AURA_REFRESH heuristic for all other units via SPELL_GO hit target parsing
- Extended spellStartDetour to parse SMSG_SPELL_GO (0x132) hit target list
- For aura spells (APPLY_AURA/APPLY_AREA_AURA effects), checks if hit target already has the aura
- Skips local player (handled by SetActionCooldownTimer), fires for all other units
- Helpers: hasApplyAuraEffect(spellId), unitHasAura(guid, spellId) → ?slot
- [x] SPELL_AURA_BROKEN_SPELL heuristic via damage ring buffer (16 entries)
- Damage hooks record {target, source, spellId} into ring buffer
- auraRemovedDetour checks SpellRec.AuraInterruptFlags (+0x58) for damage-break flags
- If breakable + recent damage → fire SPELL_AURA_BROKEN_SPELL before SPELL_AURA_REMOVED
- [x] ENERGIZE false-fire bug fixed: removed 0x62DC10 hook, moved detection to ProcessSpellEffect with spell DB filter
- [x] Aura BUFF/DEBUFF detection improved: uses spell DB Attributes flag 0x4000000 (SPELL_ATTR_NEGATIVE) with slot < 40 fallback
### Known Issues — Fixed
- ~~**SPELL_ENERGIZE false fires**~~: Fixed. Removed 0x62DC10 hook (was OPEN_LOCK/effectType 33).
ENERGIZE now detected via spell DB Effect[] check in ProcessSpellEffect (0x62ACE0).
### Known Limitations
- **SPELL_AURA_REFRESH heuristic for other units**: For non-local units, vanilla sends NO
refresh notification at all (no packet, no descriptor change). We use a SPELL_GO heuristic:
when a buff spell hits a target that already has that aura in their descriptors, we infer
a refresh. This is best-effort and has known limitations:
- False positives: spell hits target but aura was actually removed and re-applied (rare)
- False negatives: procs, channeled refreshes, or any refresh without a SPELL_GO packet
- Only works for spells with APPLY_AURA (6) or APPLY_AREA_AURA (27) effects in spell DB
- Local player excluded (uses reliable SetActionCooldownTimer path instead)
### SPELL_AURA_BROKEN_SPELL — Heuristic via Damage Ring Buffer
- [x] Implemented as heuristic: damage ring buffer (16 entries) + aura removal correlation
- Damage hooks (SPELL_DAMAGE, SWING_DAMAGE, SPELL_PERIODIC_DAMAGE, ENVIRONMENTAL_DAMAGE)
record {targetGUID, sourceGUID, spellId} into ring buffer via recordDamage()
- In auraRemovedDetour: if removed aura has AURA_INTERRUPT_FLAG_DAMAGE (0x02) or
AURA_INTERRUPT_FLAG_DIRECT_DAMAGE (0x01000000) in SpellRec.AuraInterruptFlags (+0x58),
check ring buffer for recent damage to same target
- If match: fire SPELL_AURA_BROKEN_SPELL with breaking source info, then SPELL_AURA_REMOVED
- Server confirms: AuraRemoveMode is tracked internally but NEVER sent to client
- No timestamp needed: damage packets arrive BEFORE descriptor updates (same server tick)
- Known limitations: false positives if damage + unrelated aura removal happen in same batch;
no coverage for SPELL_AURA_BROKEN (without spell — melee break) since we pass spellId=0 for melee
### Completed reference
- [x] RANGE_MISSED: Detected via spell ID check (75/5019) in ProcessSpellCombatResult hook.
- [x] SPELL_SUMMON / SPELL_RESURRECT / SPELL_ENERGIZE: Hook ProcessSpellEffect (0x62ACE0),
spell DB Effect[] lookup. Summon: 28, 42, 56, 85-88, 104-107, 109. Resurrect: 18, 113.
Energize: 30. SpellRec via WowClientDB 0xC0D780: m_recordsById(+8), m_maxId(+C),
Effect[3] at +0xF4, School at +0x04, Attributes at +0x18.
### SMSG_UPDATE_AURA_DURATION (opcode 0x137) — Aura Refresh Research
- Packet: `uint8 slot` + `uint32 duration_ms` (5 bytes)
- Handler: multi-opcode dispatcher at 0x5E38C0, case at 0x5E3BA9
- Calls `SetActionCooldownTimer` (0x4E4390): `__fastcall(ECX=slot, EDX=duration_ms)`
- Stores `current_timestamp + duration_ms` into expiry array at 0xBC5F68 (48 entries)
- Server sends on both initial application AND refresh (Refresh() in SpellAuras.cpp:362)
- Server only sends to the player themselves (GetTarget()->GetTypeId() == TYPEID_PLAYER)
- On initial application: SMSG_UPDATE_AURA_DURATION sent BEFORE SMSG_UPDATE_OBJECT
(duration sent immediately from AddSpellAuraHolder, descriptors batched at end of tick)
→ client descriptor slot is still empty when handler fires → detectable as "not refresh"
- On refresh: descriptor already has the spell from earlier application → detectable as refresh
### CRITICAL: Wrong opcode labels from Ghidra handler dump
Many labels in `/tmp/handlers_output.txt` were user-applied and WRONG. The server source
(`reference/server/src/game/Protocol/Opcodes_1_12_1.h`) is the definitive reference:
- 0x15B = SMSG_RESURRECT_REQUEST (was labeled "handleSpellMiss" — **WRONG**)
- 0x12A = SMSG_INITIAL_SPELLS (was labeled "applySpellAura" — **WRONG**)
- 0x129 = SMSG_ACTION_BUTTONS (was labeled "removeSpellAura" — **WRONG**)
- 0x1F1 = MSG_SAVE_GUILD_EMBLEM (was labeled "applyDamageShield" — **WRONG**)
- The SPELL_MISSED hook on 0x5E7BC0 was removed — it was hooking the resurrect handler
### Melee Hook Research Findings
- 0x625E20 (originally targeted as "post-parse handler") is just a flag-setter — too simple
- Dispatcher 0x6255B0 handles opcodes 0x143-0x14A via switch/jump table (byte lookup at 0x625B0C, targets at 0x625AEC)
@@ -502,10 +635,284 @@ would need a separate hook or could be left to text events.
- 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. [ ] Environmental damage hook via ProcessEnvironmentalDamage (0x62aac0)
2. [ ] Addon-side example code showing how to register for our events
3. [ ] Nampower conflict detection via GetModuleHandleA("nampower.dll") (low priority)
1. [x] Migrate existing packet handler hooks to downstream equivalents (Warden safety)
- [x] SPELL_HEAL: 0x5E89C0 → ProcessSpellPowerDrainMessage (0x62C770). MIGRATED.
- [x] SPELL_DAMAGE: Evaluated, NOT migrated (lose school field, uncertain params)
- [x] All others: Evaluated, NOT migrated (see Phase 3 Migration Assessment above)
2. [x] Implement UNIT_DIED: hook HandleUnitDeath (0x605860), fastcall(ECX=unit), plain RET
3. [x] Implement SPELL_AURA_REMOVED: hook CastSpell (0x612320), thiscall(ECX=unit, slot, spellId), RET 0x8
4. [x] Implement SPELL_AURA_APPLIED: hook SetSpellTarget (0x6123F0), thiscall(ECX=unit, slot, spellId), RET 0x8
- Aura type heuristic: slot < 40 = "BUFF", slot >= 40 = "DEBUFF"
5. [x] Implement SPELL_AURA_APPLIED_DOSE/REMOVED_DOSE: hook ValidateSpellSlot (0x612450)
- requestedLevel param = OLD count, live descriptor has NEW count
- Fires both directions: increase → APPLIED_DOSE, decrease → REMOVED_DOSE
6. [x] Implement SPELL_EXTRA_ATTACKS: hook ProcessExtraAttacksSpellMessage (0x62D9F0)
- effectType 19 in vanilla's SPELLLOGEXECUTE dispatch (NOT 33 as in WotLK)
- Extra attack count not available from downstream — passed as 0
7. [x] Implement SPELL_DISPEL: hook ProcessAuraDispelMessage (0x62D480)
- Dedicated SMSG_SPELLDISPELLOG handler at 0x5E8B60 calls this per dispelled aura
- Has caster GUID, target GUID, dispelled aura spell ID
- Dispelling spell ID unknown — passed as 0
8. [x] Implement RANGE_DAMAGE: detect Auto Shot (75) / Shoot (5019) in spell damage hook
9. [ ] Implement SPELL_AURA_REFRESH: no callback for same-spell-ID aura re-application
10. [ ] Implement SPELL_AURA_BROKEN/BROKEN_SPELL: CC break detection — deep aura system research
11. [ ] Implement SPELL_SUMMON: effectType 28+ → ProcessSpellEffect (0x62ACE0, generic — no effect type param)
12. [ ] Implement SPELL_RESURRECT: effectType 18/32/113 → ProcessSpellEffect (generic)
13. [ ] Implement RANGE_MISSED: detect Auto Shot/Shoot in SPELLLOGMISS downstream
14. [ ] Test in-game: verify all subevents fire correctly
15. [ ] Nampower conflict detection via GetModuleHandleA("nampower.dll")
16. [ ] Example addon code for COMBAT_LOG_EVENT consumption
### Phase 4: UNIT_DIED, SPELL_AURA_APPLIED/REMOVED, RANGE_DAMAGE
#### Object Field Update Callback System
The client registers field change callbacks via `ObjectData_UpdateField` (0x468070):
```
ObjectData_UpdateField(objectType, fieldOffset, dataSize, callbackFunc, context, flags)
```
Key registrations in `InitializeUnitEventHandlers` (0x6041F0):
- `(3, 0x40, 4, 0x6046F0, 0, 0)` -- UNIT_FIELD_HEALTH -> HandleUnitHealthChange
- `(3, 0xA4, 0xD8, 0x604D00, 0, 1)` -- UNIT_FIELD_AURA range -> compareAndUpdateObjectArrays
- `(3, eventId+0x1AC, 1, 0x604EA0, 0, 0)` -- UNIT_FIELD_AURAAPPLICATIONS -> updateObjectWithByteValue
Callbacks are `__stdcall(guidLo, guidHi, oldDataPtr, ???)` with RET 0x10 (4 stack params).
The oldDataPtr contains the PREVIOUS field values before the update was applied.
#### UNIT_DIED -- HandleUnitDeath (0x605860)
- `__fastcall(ECX=unitObject)`, plain RET (0 stack params)
- Called from `HandleUnitHealthChange` (0x6046F0) when `newHealth < 1 && oldHealth > 0`
- Single xref from HandleUnitHealthChange -- only fires on actual death transition
- GUID access: `*(*(unitObject+8))` = GUID lo, `*(*(unitObject+8)+4)` = GUID hi
- Also handles: spell interruption, target clearing, action queue cleanup, event notifications
```
HandleUnitHealthChange (0x6046F0) __stdcall(guidLo, guidHi, oldHealthPtr, ???)
-> resolves unit object via ClntObjMgrObjectPtr
-> if newHealth < 1 && oldHealth > 0:
HandleUnitDeath(unitObject)
-> if newHealth > 0 && oldHealth < 1:
HandleUnitResurrection(unitObject)
```
Hook target: `HandleUnitDeath` (0x605860) -- fire `UNIT_DIED` with target GUID, no source.
#### SPELL_AURA_APPLIED/REMOVED -- Aura Field Change Callback
The aura callback at 0x604D00 (`compareAndUpdateObjectArrays`) is called when UNIT_FIELD_AURA
descriptor fields change (offsets 0xA4-0x17B, 48 spell ID slots + flags). It receives:
- `dataArray` = pointer to OLD aura data (before update was applied)
- Current (NEW) data is in the live descriptor at `*(unitObject+0x110)`
The callback iterates all 48 slots in two passes:
1. **Pass 1 (removal)**: old aura active + new NOT active -> `CastSpell(unit, slot, oldSpellId)`
2. **Pass 2 (application)**: old NOT active + new IS active -> `SetSpellTarget(unit, slot, newSpellId)`
Active check: spellId != 0 AND aura flags byte has bits set.
Aura flags at old offset 0xC0 relative to dataArray, at 0x164 relative to live descriptor.
Flags are packed: 2 aura slots per byte, 4 bits each, mask 0x0E for flag bits.
**CastSpell (0x612320)** -- AURA REMOVED notification
- `__thiscall(ECX=unitObject, stack: slotIndex, spellId)`, RET 0x8
- Single xref from compareAndUpdateObjectArrays (safe to hook)
- Does: IsActivePlayer check, camera FOV reset (for certain effects), UpdateActionsBySpellId
**SetSpellTarget (0x6123F0)** -- AURA APPLIED notification
- `__thiscall(ECX=unitObject, stack: slotIndex, spellId)`, RET 0x8
- Single xref from compareAndUpdateObjectArrays (safe to hook)
- Does: SetUnitTargetById, CGUnit_ProcessAuraSlot, HandleSpellManaCost, UpdateActionsBySpellId
- slotIndex 0x20-0x2F (32-47) treated as "valid spell range" for mana cost handling
**ValidateSpellSlot (0x612450)** -- AURA DOSE change (from aura count callback 0x604EA0)
- `__thiscall(ECX=unitObject, stack: slotIndex, requestedLevel)`, called when stack count changes
- Could be used for SPELL_AURA_APPLIED_DOSE/REMOVED_DOSE
**Limitation**: Source GUID (who applied the aura) is NOT available from field updates.
No UNIT_FIELD_AURA_CREATOR equivalent in vanilla 1.12.1. Fire with source=GUID_ZERO.
#### RANGE_DAMAGE -- Ranged Auto-Attack Detection
Vanilla ranged auto-attacks use the same SMSG_ATTACKERSTATEUPDATE packet as melee.
No distinct opcode or hitInfo flag for ranged. To distinguish:
- Check attacker's UNIT_VIRTUAL_ITEM_SLOT_DISPLAY[2] (ranged weapon slot)
- Field index 0x14, byte offset 0x50 from descriptor start
- From stored ptr at obj+0x110: offset 0x38
- If non-zero, the unit has a ranged weapon equipped
- Cross-reference with the attack being an auto-attack (no spellId in the packet)
Lower priority -- most addons treat ranged auto-attacks as SWING_DAMAGE.
### SPELLLOGEXECUTE Dispatch Table (0x5E7F90 — Phase 5 Ghidra Analysis)
Jump table at 0x5E8430 with index byte table at 0x5E845B (126 entries, guard CMP ECX,0x7D).
| Case | effectTypes | Downstream Function |
|------|------------|-------------------|
| 0 | 0(NONE), 1(INSTAKILL) | ProcessSpellEffect (0x62ACE0) with target |
| 1 | 8(POWER_DRAIN) | ProcessStandardPowerDrainMessage (0x62DBF0) |
| 2 | 18(RESURRECT), 38(DISPEL), 63, 69, 79, 91, 108(DISPEL_MECHANIC), 113, 114, 116, 125 | ProcessSpellEffect (0x62ACE0) with target |
| 3 | 19(EXTRA_ATTACKS in vanilla!) | ProcessExtraAttacksSpellMessage (0x62D9F0) |
| 4 | 24(CREATE_ITEM) | ProcessEnchantItemValidationMessage (0x62D8D0) |
| 5 | 33(OPEN_LOCK in vanilla), 59 | 0x62DC10 (OPEN_LOCK display — NOT energize) |
| 6 | 68(INTERRUPT_CAST) | DisplaySpellInterruptMessage (0x626A10) |
| 7 | 101(FEED_PET) | ProcessFeedPetValidationMessage (0x62D800) |
| 8 | 102(DISMISS_PET) | ProcessPetDismissMessage (0x62E360) |
| 9 | 111(DURABILITY_DAMAGE) | ProcessAllDurabilityDamageMessage (0x62E190) |
| 10 | Everything else (default) | ProcessSpellEffect (0x62ACE0) with NULL target |
**CRITICAL**: Vanilla effectType numbering differs from WotLK! effectType 19 = EXTRA_ATTACKS (not 33).
effectType 33 = OPEN_LOCK (calls 0x62DC10, which we hook as SPELL_ENERGIZE — potential false fires).
effectType 30 (ENERGIZE) falls to default → ProcessSpellEffect, NOT 0x62DC10.
ProcessSpellEffect (0x62ACE0) is a generic display function: checks spell DB entry flags,
filters by effect slot types, then calls ProcessHealingSpellEffect (with target) or
CalculateSpellPower (without target). No effect type parameter — cannot distinguish
DISPEL/SUMMON/RESURRECT from inside this function.
### SMSG_SPELLDISPELLOG — Dedicated Dispel Handler
**Handler**: 0x5E8B60 (registered in applySpellModifiers 0x5E3010)
**Convention**: __stdcall(opcode, CDataStore*), RET 0x8
**Packet format**: PackedGuid casterGuid, PackedGuid targetGuid, u32 count, [count × u32 spellId]
**Downstream**: ProcessAuraDispelMessage (0x62D480) per dispelled aura
**ProcessAuraDispelMessage (0x62D480)**:
`__fastcall(ECX=casterGUID_ptr, EDX=targetGUID_ptr, stack: dispelledSpellId)`, RET 0x4
Displays AURADISPELSELF/AURADISPELOTHER. Only has the dispelled aura's spell ID,
not the dispelling spell. Hooked for SPELL_DISPEL.
### CDataStore Structure
@@ -521,13 +928,11 @@ would need a separate hook or could be left to text events.
### Event Slot Layout
```
Nampower: 549 → SPELL_DAMAGE_EVENT_SELF (0xBE1A2C)
Nampower: 549 → SPELL_DAMAGE_EVENT_SELF (0xBE1A2C) [overwritten by us]
550 → SPELL_DAMAGE_EVENT_OTHER (0xBE1A30)
Ours: 551 → COMBAT_LOG_SPELL_DMG (0xBE1A34)
552 → COMBAT_LOG_PERIODIC (0xBE1A38)
553 → COMBAT_LOG_HEAL (0xBE1A3C)
554 → (reserved for COMBAT_LOG_MELEE)
FrameScript_CreateEvents maxEventId bumped to 555.
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.
```
---
+524
View File
@@ -0,0 +1,524 @@
-- DPS Log Tracker — popup checklist for verifying all 35 COMBAT_LOG_EVENT subevents
--
-- All events share: arg1=subevent, arg2=sourceGUID, arg3=destGUID
-- Remaining args are subevent-specific (see layouts below).
-- ============================================================================
-- Reference tables
-- ============================================================================
-- Spell school bitmask (matches WotLK):
-- 0x01 = Physical, 0x02 = Holy, 0x04 = Fire, 0x08 = Nature,
-- 0x10 = Frost, 0x20 = Shadow, 0x40 = Arcane
-- Common combos: 0x03 = Holystrike, 0x05 = Flamestrike, 0x1C = Elemental,
-- 0x7E = Magic (all non-physical), 0x7F = Chaos (all 7)
-- Power types: 0=Mana, 1=Rage, 2=Focus, 3=Energy, 4=Combo Points
local powerNames = { [0]="Mana", "Rage", "Focus", "Energy", "Combo Points" }
-- Miss types (string): "MISS", "DODGE", "PARRY", "BLOCK", "EVADE", "IMMUNE",
-- "DEFLECT", "RESIST", "ABSORB", "REFLECT"
-- Aura types (string): "BUFF", "DEBUFF"
-- Environmental types (string): "EXHAUSTED", "DROWNING", "FALLING", "LAVA",
-- "SLIME", "FIRE"
-- SWING_DAMAGE arg10 flags bitmask:
-- bit 0 (value 1) = glancing hit
-- bit 1 (value 2) = crushing blow
-- bit 2 (value 4) = offhand swing
-- Extract: glancing = (math.mod(flags, 2) == 1)
-- crushing = (math.mod(math.floor(flags / 2), 2) == 1)
-- offhand = (math.mod(math.floor(flags / 4), 2) == 1)
-- Combined: offhand glancing = 5, offhand crushing = 6, etc.
-- ============================================================================
-- Helpers
-- ============================================================================
local function schoolName(school)
if school == 1 then return "Physical"
elseif school == 2 then return "Holy"
elseif school == 4 then return "Fire"
elseif school == 8 then return "Nature"
elseif school == 16 then return "Frost"
elseif school == 32 then return "Shadow"
elseif school == 64 then return "Arcane"
else return "School:" .. (school or "?") end
end
local function s(v)
if v == nil then return "nil" end
return tostring(v)
end
-- ============================================================================
-- Subevent definitions — ordered list of all 34
-- ============================================================================
local SUBEVENTS = {
-- Pattern A: fireCombatLog (sub, src, dst, n1..n7)
"SPELL_DAMAGE",
"RANGE_DAMAGE",
"SPELL_PERIODIC_DAMAGE",
"DAMAGE_SHIELD",
"SWING_DAMAGE",
"SPELL_HEAL",
"SPELL_PERIODIC_HEAL",
"SPELL_ENERGIZE",
"SPELL_PERIODIC_ENERGIZE",
"SPELL_PERIODIC_LEECH",
"SPELL_PERIODIC_DRAIN",
"SPELL_EXTRA_ATTACKS",
-- Pattern B: fireSwingMissed (sub, src, dst, missType)
"SWING_MISSED",
-- Pattern C: fireSpellStr (sub, src, dst, spellId, school, strArg)
"SPELL_MISSED",
"SPELL_PERIODIC_MISSED",
"DAMAGE_SHIELD_MISSED",
"RANGE_MISSED",
"SPELL_AURA_APPLIED",
"SPELL_AURA_REMOVED",
"SPELL_AURA_REFRESH",
"SPELL_AURA_BROKEN",
"SPELL_CAST_FAILED",
-- Pattern D: fireSpell (sub, src, dst, spellId, school)
"SPELL_CAST_START",
"SPELL_CAST_SUCCESS",
"SPELL_SUMMON",
"SPELL_RESURRECT",
"SPELL_INSTAKILL",
-- Pattern E: fireBase (sub, src, dst)
"UNIT_DIED",
"PARTY_KILL",
-- Pattern F: fireSpellStrD (sub, src, dst, spellId, school, strArg, amount)
"SPELL_AURA_APPLIED_DOSE",
"SPELL_AURA_REMOVED_DOSE",
-- Pattern G: fireSpellDispel (sub, src, dst, spellId, school, extraId, extraSchool, auraType)
"SPELL_DISPEL",
"SPELL_INTERRUPT",
"SPELL_AURA_BROKEN_SPELL",
-- Pattern H: fireEnvDamage (sub, src, dst, envStr, amount, school, absorbed, 0, 0)
"ENVIRONMENTAL_DAMAGE",
}
local NUM_SUBEVENTS = 35
-- Reverse lookup: subevent name -> index
local subeventIndex = {}
for i = 1, NUM_SUBEVENTS do
subeventIndex[SUBEVENTS[i]] = i
end
-- Tracking state
local seen = {} -- [index] = true
local seenArgs = {} -- [index] = formatted arg string
local seenCount = 0
-- ============================================================================
-- Arg formatters per subevent (return a short summary string)
-- ============================================================================
local function fmtSpellDmg(spellId, amount, school, resisted, absorbed, blocked, critical)
local c = (critical == 1) and " CRIT" or ""
return format("spell=%s amt=%s %s res=%s abs=%s blk=%s%s",
s(spellId), s(amount), schoolName(school or 0), s(resisted), s(absorbed), s(blocked), c)
end
local function fmtSwingDmg(amount, school, resisted, absorbed, blocked, critical, flags)
local c = (critical == 1) and " CRIT" or ""
local f = ""
if flags and flags > 0 then
if math.mod(flags, 2) == 1 then f = f .. " GLANC" end
if math.mod(math.floor(flags / 2), 2) == 1 then f = f .. " CRUSH" end
if math.mod(math.floor(flags / 4), 2) == 1 then f = f .. " OH" end
end
return format("amt=%s %s res=%s abs=%s blk=%s%s%s",
s(amount), schoolName(school or 0), s(resisted), s(absorbed), s(blocked), c, f)
end
local argFormatters = {}
-- Pattern A — numeric args
argFormatters["SPELL_DAMAGE"] = function(a4,a5,a6,a7,a8,a9,a10) return fmtSpellDmg(a4,a5,a6,a7,a8,a9,a10) end
argFormatters["RANGE_DAMAGE"] = function(a4,a5,a6,a7,a8,a9,a10) return fmtSpellDmg(a4,a5,a6,a7,a8,a9,a10) end
argFormatters["SPELL_PERIODIC_DAMAGE"] = function(a4,a5,a6,a7,a8,a9,a10) return fmtSpellDmg(a4,a5,a6,a7,a8,a9,a10) end
argFormatters["DAMAGE_SHIELD"] = function(a4,a5,a6,a7,a8,a9,a10) return fmtSpellDmg(a4,a5,a6,a7,a8,a9,a10) end
argFormatters["SWING_DAMAGE"] = function(a4,a5,a6,a7,a8,a9,a10) return fmtSwingDmg(a4,a5,a6,a7,a8,a9,a10) end
argFormatters["SPELL_HEAL"] = function(a4,a5,a6,a7,a8)
local c = (a8 == 1) and " CRIT" or ""
return format("spell=%s amt=%s oh=%s abs=%s%s", s(a4), s(a5), s(a6), s(a7), c)
end
argFormatters["SPELL_PERIODIC_HEAL"] = function(a4,a5) return format("spell=%s amt=%s", s(a4), s(a5)) end
argFormatters["SPELL_ENERGIZE"] = function(a4,a5,a6)
return format("spell=%s amt=%s type=%s", s(a4), s(a5), powerNames[a6] or s(a6))
end
argFormatters["SPELL_PERIODIC_ENERGIZE"] = function(a4,a5,a6,a7)
return format("spell=%s amt=%s over=%s type=%s", s(a4), s(a5), s(a6), powerNames[a7] or s(a7))
end
argFormatters["SPELL_PERIODIC_LEECH"] = function(a4,a5,a6,a7,a8,a9)
return format("spell=%s amt=%s %s gain=%s abs=%s res=%s", s(a4), s(a5), schoolName(a6 or 0), s(a7), s(a8), s(a9))
end
argFormatters["SPELL_PERIODIC_DRAIN"] = function(a4,a5,a6,a7)
return format("spell=%s amt=%s type=%s extra=%s", s(a4), s(a5), powerNames[a6] or s(a6), s(a7))
end
argFormatters["SPELL_EXTRA_ATTACKS"] = function(a4,a5,a6)
return format("spell=%s school=%s amt=%s", s(a4), s(a5), s(a6))
end
-- Pattern B — swing missed
argFormatters["SWING_MISSED"] = function(a4) return s(a4) end
-- Pattern C — spell + string
argFormatters["SPELL_MISSED"] = function(a4,a5,a6) return format("spell=%s %s %s", s(a4), schoolName(a5 or 0), s(a6)) end
argFormatters["SPELL_PERIODIC_MISSED"] = function(a4,a5,a6) return format("spell=%s %s %s", s(a4), schoolName(a5 or 0), s(a6)) end
argFormatters["DAMAGE_SHIELD_MISSED"] = function(a4,a5,a6) return format("spell=%s %s %s", s(a4), schoolName(a5 or 0), s(a6)) end
argFormatters["RANGE_MISSED"] = function(a4,a5,a6) return format("spell=%s %s %s", s(a4), schoolName(a5 or 0), s(a6)) end
argFormatters["SPELL_AURA_APPLIED"] = function(a4,a5,a6) return format("spell=%s %s %s", s(a4), schoolName(a5 or 0), s(a6)) end
argFormatters["SPELL_AURA_REMOVED"] = function(a4,a5,a6) return format("spell=%s %s %s", s(a4), schoolName(a5 or 0), s(a6)) end
argFormatters["SPELL_AURA_REFRESH"] = function(a4,a5,a6) return format("spell=%s %s %s", s(a4), schoolName(a5 or 0), s(a6)) end
argFormatters["SPELL_AURA_BROKEN"] = function(a4,a5,a6) return format("spell=%s %s %s", s(a4), schoolName(a5 or 0), s(a6)) end
argFormatters["SPELL_CAST_FAILED"] = function(a4,a5,a6) return format("spell=%s %s reason=%s", s(a4), schoolName(a5 or 0), s(a6)) end
-- Pattern D — spell only
argFormatters["SPELL_CAST_START"] = function(a4,a5) return format("spell=%s %s", s(a4), schoolName(a5 or 0)) end
argFormatters["SPELL_CAST_SUCCESS"] = function(a4,a5) return format("spell=%s %s", s(a4), schoolName(a5 or 0)) end
argFormatters["SPELL_SUMMON"] = function(a4,a5) return format("spell=%s %s", s(a4), schoolName(a5 or 0)) end
argFormatters["SPELL_RESURRECT"] = function(a4,a5) return format("spell=%s %s", s(a4), schoolName(a5 or 0)) end
argFormatters["SPELL_INSTAKILL"] = function(a4,a5) return format("spell=%s %s", s(a4), schoolName(a5 or 0)) end
-- Pattern E — base (no extra args)
argFormatters["UNIT_DIED"] = function() return "" end
argFormatters["PARTY_KILL"] = function() return "" end
-- Pattern F — spell + string + amount
argFormatters["SPELL_AURA_APPLIED_DOSE"] = function(a4,a5,a6,a7) return format("spell=%s %s %s x%s", s(a4), schoolName(a5 or 0), s(a6), s(a7)) end
argFormatters["SPELL_AURA_REMOVED_DOSE"] = function(a4,a5,a6,a7) return format("spell=%s %s %s x%s", s(a4), schoolName(a5 or 0), s(a6), s(a7)) end
-- Pattern G — dispel/interrupt
argFormatters["SPELL_DISPEL"] = function(a4,a5,a6,a7,a8)
return format("spell=%s extra=%s %s %s", s(a4), s(a6), schoolName(a7 or 0), s(a8))
end
argFormatters["SPELL_INTERRUPT"] = function(a4,a5,a6,a7)
return format("spell=%s extra=%s %s", s(a4), s(a6), schoolName(a7 or 0))
end
argFormatters["SPELL_AURA_BROKEN_SPELL"] = function(a4,a5,a6,a7,a8)
return format("spell=%s extra=%s %s %s", s(a4), s(a6), schoolName(a7 or 0), s(a8))
end
-- Pattern H — environmental
argFormatters["ENVIRONMENTAL_DAMAGE"] = function(a4,a5,a6,a7)
return format("env=%s amt=%s %s abs=%s", s(a4), s(a5), schoolName(a6 or 0), s(a7))
end
-- ============================================================================
-- Chat output handlers (preserved from original)
-- ============================================================================
local chatHandlers = {}
chatHandlers["SPELL_DAMAGE"] = function(src, dst, spellId, amount, school, resisted, absorbed, blocked, critical)
local crit = critical == 1 and " (CRIT)" or ""
DEFAULT_CHAT_FRAME:AddMessage(format("|cffff0000SPELL_DAMAGE|r %s -> %s: spell %d for %d %s%s", src, dst, spellId, amount, schoolName(school), crit))
end
chatHandlers["RANGE_DAMAGE"] = function(src, dst, spellId, amount, school, resisted, absorbed, blocked, critical)
local crit = critical == 1 and " (CRIT)" or ""
DEFAULT_CHAT_FRAME:AddMessage(format("|cffff0000RANGE_DAMAGE|r %s -> %s: spell %d for %d %s%s", src, dst, spellId, amount, schoolName(school), crit))
end
chatHandlers["SWING_DAMAGE"] = function(src, dst, amount, school, resisted, absorbed, blocked, critical, flags)
local extra = ""
if critical == 1 then extra = extra .. " CRIT" end
if flags then
if math.mod(flags, 2) == 1 then extra = extra .. " GLANCING" end
if math.mod(math.floor(flags / 2), 2) == 1 then extra = extra .. " CRUSHING" end
if math.mod(math.floor(flags / 4), 2) == 1 then extra = extra .. " (OH)" end
end
DEFAULT_CHAT_FRAME:AddMessage(format("|cffff0000SWING_DAMAGE|r %s -> %s: %d%s", src, dst, amount, extra))
end
chatHandlers["SWING_MISSED"] = function(src, dst, missType)
DEFAULT_CHAT_FRAME:AddMessage(format("|cffaaaaaaSWING_MISSED|r %s -> %s: %s", src, dst, missType))
end
chatHandlers["SPELL_MISSED"] = function(src, dst, spellId, spellSchool, missType)
DEFAULT_CHAT_FRAME:AddMessage(format("|cffaaaaaaSPELL_MISSED|r %s -> %s: spell %d %s", src, dst, spellId, missType))
end
chatHandlers["RANGE_MISSED"] = function(src, dst, spellId, spellSchool, missType)
DEFAULT_CHAT_FRAME:AddMessage(format("|cfffaaaaaaRANGE_MISSED|r %s -> %s: spell %d %s", src, dst, spellId, missType))
end
chatHandlers["ENVIRONMENTAL_DAMAGE"] = function(src, dst, envType, amount, school, absorbed)
DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800ENVIRONMENTAL_DAMAGE|r %s: %s for %d", dst, envType, amount))
end
chatHandlers["DAMAGE_SHIELD"] = function(src, dst, spellId, amount, school)
DEFAULT_CHAT_FRAME:AddMessage(format("|cffff4400DAMAGE_SHIELD|r %s -> %s: %d %s", src, dst, amount, schoolName(school)))
end
chatHandlers["SPELL_HEAL"] = function(src, dst, spellId, amount, overheal, absorbed, critical)
local crit = critical == 1 and " (CRIT)" or ""
DEFAULT_CHAT_FRAME:AddMessage(format("|cff00ff00SPELL_HEAL|r %s -> %s: spell %d for %d%s", src, dst, spellId, amount, crit))
end
chatHandlers["SPELL_PERIODIC_HEAL"] = function(src, dst, spellId, amount)
DEFAULT_CHAT_FRAME:AddMessage(format("|cff00cc00SPELL_PERIODIC_HEAL|r %s -> %s: spell %d for %d", src, dst, spellId, amount))
end
chatHandlers["SPELL_PERIODIC_DAMAGE"] = function(src, dst, spellId, amount, school)
DEFAULT_CHAT_FRAME:AddMessage(format("|cffcc0000SPELL_PERIODIC_DAMAGE|r %s -> %s: spell %d for %d %s", src, dst, spellId, amount, schoolName(school)))
end
chatHandlers["SPELL_ENERGIZE"] = function(src, dst, spellId, amount, powerType)
local pname = powerNames[powerType] or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(format("|cff4488ffSPELL_ENERGIZE|r %s -> %s: spell %d +%d %s", src, dst, spellId, amount, pname))
end
chatHandlers["SPELL_PERIODIC_ENERGIZE"] = function(src, dst, spellId, amount, overEnergize, powerType)
local pname = powerNames[powerType] or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(format("|cff4488ffSPELL_PERIODIC_ENERGIZE|r %s -> %s: spell %d +%d %s", src, dst, spellId, amount, pname))
end
chatHandlers["SPELL_PERIODIC_LEECH"] = function(src, dst, spellId, amount, school, gained)
DEFAULT_CHAT_FRAME:AddMessage(format("|cff8844ffSPELL_PERIODIC_LEECH|r %s -> %s: spell %d drained %d, gained %d", src, dst, spellId, amount, gained or 0))
end
chatHandlers["SPELL_PERIODIC_DRAIN"] = function(src, dst, spellId, amount, powerType, extraAmount)
local pname = powerNames[powerType] or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(format("|cff8844ffSPELL_PERIODIC_DRAIN|r %s -> %s: spell %d drained %d %s, gained %d", src, dst, spellId, amount, pname, extraAmount or 0))
end
chatHandlers["SPELL_AURA_APPLIED"] = function(src, dst, spellId, spellSchool, auraType)
DEFAULT_CHAT_FRAME:AddMessage(format("|cff8888ffSPELL_AURA_APPLIED|r %s: spell %d (%s, %s)", dst, spellId, auraType, schoolName(spellSchool)))
end
chatHandlers["SPELL_AURA_REMOVED"] = function(src, dst, spellId, spellSchool, auraType)
DEFAULT_CHAT_FRAME:AddMessage(format("|cff6666ccSPELL_AURA_REMOVED|r %s: spell %d (%s)", dst, spellId, auraType))
end
chatHandlers["SPELL_AURA_APPLIED_DOSE"] = function(src, dst, spellId, spellSchool, auraType, stacks)
DEFAULT_CHAT_FRAME:AddMessage(format("|cff8888ffSPELL_AURA_APPLIED_DOSE|r %s: spell %d x%d (%s)", dst, spellId, stacks, auraType))
end
chatHandlers["SPELL_AURA_REMOVED_DOSE"] = function(src, dst, spellId, spellSchool, auraType, stacks)
DEFAULT_CHAT_FRAME:AddMessage(format("|cff6666ccSPELL_AURA_REMOVED_DOSE|r %s: spell %d x%d (%s)", dst, spellId, stacks, auraType))
end
chatHandlers["SPELL_AURA_REFRESH"] = function(src, dst, spellId, spellSchool, auraType)
DEFAULT_CHAT_FRAME:AddMessage(format("|cff7777ddSPELL_AURA_REFRESH|r %s: spell %d (%s)", dst, spellId, auraType))
end
chatHandlers["SPELL_AURA_BROKEN"] = function(src, dst, spellId, spellSchool, auraType)
DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800SPELL_AURA_BROKEN|r %s -> %s: spell %d broken by melee (%s)", src, dst, spellId, auraType))
end
chatHandlers["SPELL_AURA_BROKEN_SPELL"] = function(src, dst, spellId, spellSchool, extraSpellId, extraSchool, auraType)
DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800SPELL_AURA_BROKEN_SPELL|r %s -> %s: spell %d broken by spell %d (%s)", src, dst, spellId, extraSpellId, auraType))
end
chatHandlers["SPELL_CAST_START"] = function(src, dst, spellId, spellSchool)
DEFAULT_CHAT_FRAME:AddMessage(format("|cffffff00SPELL_CAST_START|r %s: spell %d (%s)", src, spellId, schoolName(spellSchool)))
end
chatHandlers["SPELL_CAST_SUCCESS"] = function(src, dst, spellId)
DEFAULT_CHAT_FRAME:AddMessage(format("|cffffff00SPELL_CAST_SUCCESS|r %s: spell %d", src, spellId))
end
chatHandlers["SPELL_CAST_FAILED"] = function(src, dst, spellId, spellSchool, failedType)
DEFAULT_CHAT_FRAME:AddMessage(format("|cffff4444SPELL_CAST_FAILED|r %s: spell %d (%s)", src, spellId, failedType))
end
chatHandlers["SPELL_INTERRUPT"] = function(src, dst, spellId, spellSchool, extraSpellId, extraSchool)
DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800SPELL_INTERRUPT|r %s interrupted %s: spell %d (%s)", src, dst, extraSpellId, schoolName(extraSchool)))
end
chatHandlers["SPELL_DISPEL"] = function(src, dst, spellId, spellSchool, extraSpellId, extraSchool, auraType)
DEFAULT_CHAT_FRAME:AddMessage(format("|cff88ffffSPELL_DISPEL|r %s dispelled %s: spell %d (%s, %s)", src, dst, extraSpellId, auraType, schoolName(extraSchool)))
end
chatHandlers["SPELL_EXTRA_ATTACKS"] = function(src, dst, spellId)
DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800SPELL_EXTRA_ATTACKS|r %s: spell %d", src, spellId))
end
chatHandlers["SPELL_SUMMON"] = function(src, dst, spellId)
DEFAULT_CHAT_FRAME:AddMessage(format("|cff88ff88SPELL_SUMMON|r %s summoned %s: spell %d", src, dst, spellId))
end
chatHandlers["SPELL_RESURRECT"] = function(src, dst, spellId)
DEFAULT_CHAT_FRAME:AddMessage(format("|cff88ff88SPELL_RESURRECT|r %s resurrected %s: spell %d", src, dst, spellId))
end
chatHandlers["SPELL_INSTAKILL"] = function(src, dst, spellId)
DEFAULT_CHAT_FRAME:AddMessage(format("|cffff0000SPELL_INSTAKILL|r %s killed %s: spell %d", src, dst, spellId))
end
chatHandlers["UNIT_DIED"] = function(src, dst)
DEFAULT_CHAT_FRAME:AddMessage(format("|cff888888UNIT_DIED|r %s", dst))
end
chatHandlers["PARTY_KILL"] = function(src, dst)
DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800PARTY_KILL|r %s killed %s", src, dst))
end
chatHandlers["SPELL_PERIODIC_MISSED"] = function(src, dst, spellId, spellSchool, missType)
DEFAULT_CHAT_FRAME:AddMessage(format("|cffaaaaaaSPELL_PERIODIC_MISSED|r %s -> %s: spell %d %s", src, dst, spellId, missType))
end
chatHandlers["DAMAGE_SHIELD_MISSED"] = function(src, dst, spellId, spellSchool, missType)
DEFAULT_CHAT_FRAME:AddMessage(format("|cffaaaaaaDAMAGE_SHIELD_MISSED|r %s -> %s: spell %d %s", src, dst, spellId, missType))
end
-- ============================================================================
-- UI — Draggable popup tracker
-- ============================================================================
local ROW_HEIGHT = 14
local FRAME_WIDTH = 420
local VISIBLE_ROWS = 20
local FRAME_HEIGHT = ROW_HEIGHT * VISIBLE_ROWS + 52 -- title + counter + padding
local main = CreateFrame("Frame", "DPSLogTracker", UIParent)
main:SetWidth(FRAME_WIDTH)
main:SetHeight(FRAME_HEIGHT)
main:SetPoint("CENTER", UIParent, "CENTER", 300, 0)
main:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true, tileSize = 32, edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
main:SetBackdropColor(0, 0, 0, 0.85)
main:EnableMouse(true)
main:SetMovable(true)
main:RegisterForDrag("LeftButton")
main:SetScript("OnDragStart", function() main:StartMoving() end)
main:SetScript("OnDragStop", function() main:StopMovingOrSizing() end)
main:SetFrameStrata("DIALOG")
main:SetClampedToScreen(true)
-- Title
local title = main:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
title:SetPoint("TOPLEFT", main, "TOPLEFT", 12, -10)
title:SetText("DPS Log Tracker")
title:SetTextColor(1, 0.82, 0)
-- Close button
local closeBtn = CreateFrame("Button", nil, main, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", main, "TOPRIGHT", -2, -2)
closeBtn:SetScript("OnClick", function() main:Hide() end)
-- Counter
local counter = main:CreateFontString(nil, "OVERLAY", "GameFontNormal")
counter:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -2)
counter:SetTextColor(0.7, 0.7, 0.7)
local function updateCounter()
counter:SetText(format("Seen: %d/%d", seenCount, NUM_SUBEVENTS))
end
updateCounter()
-- Scroll frame
local scrollFrame = CreateFrame("ScrollFrame", "DPSLogTrackerScroll", main, "FauxScrollFrameTemplate")
scrollFrame:SetPoint("TOPLEFT", main, "TOPLEFT", 8, -48)
scrollFrame:SetPoint("BOTTOMRIGHT", main, "BOTTOMRIGHT", -28, 8)
-- Row frames
local rows = {}
for i = 1, VISIBLE_ROWS do
local row = CreateFrame("Frame", nil, main)
row:SetWidth(FRAME_WIDTH - 40)
row:SetHeight(ROW_HEIGHT)
row:SetPoint("TOPLEFT", scrollFrame, "TOPLEFT", 2, -((i - 1) * ROW_HEIGHT))
local check = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
check:SetPoint("LEFT", row, "LEFT", 0, 0)
check:SetWidth(20)
check:SetJustifyH("LEFT")
local nameStr = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
nameStr:SetPoint("LEFT", check, "RIGHT", 0, 0)
nameStr:SetWidth(FRAME_WIDTH - 60)
nameStr:SetJustifyH("LEFT")
row.check = check
row.nameStr = nameStr
rows[i] = row
end
local function updateRows()
local offset = FauxScrollFrame_GetOffset(scrollFrame)
for i = 1, VISIBLE_ROWS do
local idx = offset + i
local row = rows[i]
if idx <= NUM_SUBEVENTS then
row:Show()
if seen[idx] then
row.check:SetText("|cff00ff00x|r")
local argStr = seenArgs[idx]
if argStr and argStr ~= "" then
row.nameStr:SetText("|cffffffff" .. SUBEVENTS[idx] .. "|r |cffaaaaaa" .. argStr .. "|r")
else
row.nameStr:SetText("|cffffffff" .. SUBEVENTS[idx] .. "|r")
end
else
row.check:SetText("|cff666666-|r")
row.nameStr:SetText("|cff666666" .. SUBEVENTS[idx] .. "|r")
end
else
row:Hide()
end
end
FauxScrollFrame_Update(scrollFrame, NUM_SUBEVENTS, VISIBLE_ROWS, ROW_HEIGHT)
end
scrollFrame:SetScript("OnVerticalScroll", function()
FauxScrollFrame_OnVerticalScroll(ROW_HEIGHT, updateRows)
end)
updateRows()
-- Reset button
local resetBtn = CreateFrame("Button", nil, main, "UIPanelButtonTemplate")
resetBtn:SetWidth(60)
resetBtn:SetHeight(18)
resetBtn:SetPoint("TOPRIGHT", closeBtn, "TOPLEFT", -4, -6)
resetBtn:SetText("Reset")
resetBtn:SetScript("OnClick", function()
for i = 1, NUM_SUBEVENTS do
seen[i] = nil
seenArgs[i] = nil
end
seenCount = 0
updateCounter()
updateRows()
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00DPSLog Tracker|r reset.")
end)
-- ============================================================================
-- Event dispatcher
-- ============================================================================
local function print(msg)
DEFAULT_CHAT_FRAME:AddMessage(msg)
end
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("COMBAT_LOG_EVENT")
eventFrame:SetScript("OnEvent", function()
-- if event == "PLAYER_LOGIN" then
-- eventFrame:RegisterEvent("COMBAT_LOG_EVENT")
-- DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00DPSLog|r registered COMBAT_LOG_EVENT")
-- return
-- end
local subevent = arg1
if not subevent then return end
-- Chat output
local chatHandler = chatHandlers[subevent]
if chatHandler then
chatHandler(arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10)
else
DEFAULT_CHAT_FRAME:AddMessage(format("|cffccccccUNHANDLED|r %s", subevent))
end
-- Tracker update (first encounter only)
local idx = subeventIndex[subevent]
if idx and not seen[idx] then
seen[idx] = true
seenCount = seenCount + 1
local formatter = argFormatters[subevent]
if formatter then
seenArgs[idx] = formatter(arg4, arg5, arg6, arg7, arg8, arg9, arg10)
else
seenArgs[idx] = ""
end
updateCounter()
updateRows()
end
end)
-- ============================================================================
-- Slash command
-- ============================================================================
SLASH_DPSLOG1 = "/dpslog"
SlashCmdList["DPSLOG"] = function(msg)
if main:IsVisible() then
main:Hide()
else
main:Show()
end
end
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00DPSLog Tracker|r loaded — /dpslog to toggle window")
@@ -0,0 +1,6 @@
## Interface: 11200
## Title: [WeirdUtils] DPS Log
## Notes: WotLK-style COMBAT_LOG_EVENT for vanilla 1.12.1
## Version: 1.0
DPSLog.lua
+1371 -238
View File
File diff suppressed because it is too large Load Diff