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:
+473
-68
@@ -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.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user