diff --git a/src/dpslog/WARDEN_CONCERNS.md b/src/dpslog/WARDEN_CONCERNS.md new file mode 100644 index 0000000..fc92311 --- /dev/null +++ b/src/dpslog/WARDEN_CONCERNS.md @@ -0,0 +1,141 @@ +# DPSLog Warden Concerns + +Hooks and patterns that could trigger Warden false positives. Our addon is legitimate +(combat log enhancement), but we should understand the detection surface to reduce +headaches for server admins reviewing client modifications. + +## What Warden Covers + +Warden is Blizzard's anti-cheat system. On vanilla private servers, implementations vary +(some use stock mangos Warden, some extend it, some disable it). The main detection +vectors are: + +1. **Memory scanning**: Warden reads specific addresses and compares against known-good + values. Patched bytes at scanned addresses trigger detection. +2. **Module scanning**: Warden enumerates loaded DLLs and checks against blocklists + (by name or hash). Our DLL loads via injection, so it shows up in the module list. +3. **Window enumeration**: Checks for known cheat tool windows (not relevant to us). +4. **Driver checks**: Checks for known cheat drivers (not relevant). +5. **Timing checks**: Detects speed hacks via tick count validation (not relevant). + +### What We Should Avoid + +- **Patching packet handler dispatch tables**: The opcode-to-handler mapping tables are + commonly scanned. Modifying entries there is high risk. +- **Patching Warden itself**: Obvious detection surface; we don't touch it. +- **Known DLL names**: Some servers blocklist specific DLL names. Our DLL name + ("weirdutils") is not on any known list. +- **Hooking at addresses that overlap with known Warden scan points**: If Warden reads + the first N bytes of a function and we've overwritten them with a JMP, it detects the + modification. We use zhook's Detour which patches the function prologue. + +### What's Generally Safe + +- **Calling game functions** via indirect CALL: No memory modification, just normal + function calls. GetSpellNameById, getNameByGUID, etc. +- **Reading game memory**: Simple pointer dereferences. Descriptor reads, object manager + traversal, aura slot reads. +- **Hooking internal processing functions** (not packet handlers): These are deep inside + the call chain, less likely to be scanned. +- **Registering custom Lua events**: We modify the event count but don't patch any + Warden-monitored jump tables. + +## Current Hook Strategy + +We hook **downstream processing functions** (0x62xxxx range) rather than packet handlers +(0x5Exxxx range) where possible. Downstream hooks are lower risk because: +- They're internal client functions, not dispatch table entries +- Warden scans typically check packet handler jump tables +- The downstream functions are called after packet validation + +## Hooks That Are Packet Handlers (Higher Risk) + +| Address | Function | Why packet-level | +|---------|----------|-----------------| +| 0x5E85E0 | SpellNonMeleeDmgLogHandler | Downstream loses school field; no viable alternative | +| 0x626DD0 | PeriodicAuraLogHandler | IS the handler; no downstream exists | +| 0x6255B0 | MeleeDispatcher | Shared handler for opcode range 0x143-0x14A | +| 0x6E7640 | SpellStartHandler | SPELL_CAST_START/SUCCESS | +| 0x6E7330 | CastResultHandler | SPELL_CAST_FAILED | + +**Mitigation**: These are all registered via `setClientIndexedPointers` (0x5AB650), not +in a flat dispatch table. Warden typically scans flat opcode tables, not the indexed +handler registration system. Risk is moderate, not high. + +## Hooks That Are Safe (Downstream) + +| Address | Function | Subevent | +|---------|----------|----------| +| 0x62C770 | ProcessSpellPowerDrainMessage | SPELL_HEAL | +| 0x62BAB0 | ProcessSpellCombatResult | SPELL_MISSED | +| 0x62CA20 | ProcessSpellDrainEffectMessage | DAMAGE_SHIELD | +| 0x626A10 | DisplaySpellInterruptMessage | SPELL_INTERRUPT | +| 0x62CBE0 | ProcessInstaKillSpellMessage | SPELL_INSTAKILL | +| 0x62D9F0 | (extra attacks handler) | SPELL_EXTRA_ATTACKS | +| 0x62AAC0 | ProcessEnvironmentalDamage | ENVIRONMENTAL_DAMAGE | +| 0x628890 | PartyKillLogHandler | PARTY_KILL | +| 0x605860 | HandleUnitDeath | UNIT_DIED | +| 0x6123F0 | SetSpellTarget | SPELL_AURA_APPLIED | +| 0x612320 | CastSpell (aura remove) | SPELL_AURA_REMOVED | +| 0x628C20 | ProcessMultipleSpellInterrupts | SPELL_DISPEL_FAILED | +| 0x6E75F0 | HandleSpellInterruptUpdate | SPELL_CAST_FAILED (others) | +| 0x62CA00 | ProcessStandardPowerGainMessage | SPELL_ENERGIZE | + +## Event Registration + +We register COMBAT_LOG_EVENT at slot 549 by hooking FrameScript_CreateEvents (0x703D90) +and bumping the event count. This modifies the event table size but doesn't patch any +Warden-monitored code. + +## Memory Reads + +All descriptor reads (health, power, aura slots) use `readMem` which is a simple +pointer dereference -- no API hooking involved. + +## GetSpellNameById (0x6264B0) / getNameByGUID (0x55F080) + +Called via `hook.call` (indirect CALL instruction). Not hooked/patched -- we just call +them as normal functions. No Warden concern. + +## Remaining WotLK Parity Gaps + +Features not yet implemented, with notes on Warden implications: + +### amountMissed (partial absorb/block/resist on misses) + +The downstream hook ProcessSpellCombatResult (0x62BAB0) doesn't receive the miss amount. +Options: +1. Hook SMSG_SPELLLOGMISS packet handler (0x5E7E00) -- **higher Warden risk** +2. Access the CDS buffer from the downstream hook -- needs investigation, the buffer + may still be readable if the packet handler hasn't cleaned it up +3. Track state: when we see a partial absorb/block in a damage event that also has a + miss component, carry the amount forward + +**Recommendation**: Option 2 first (no new hooks). If CDS is stale, option 3. + +### sourceFlags / destFlags (COMBATLOG_OBJECT_* bitfield) + +Constructable from GUID type bits and descriptor flags. No new hooks needed -- pure +memory reads. Requires defining the WotLK COMBATLOG_OBJECT_* constants and mapping +vanilla unit types to them. + +### Boolean 1/nil (critical, glancing, crushing) + +SignalEventParam always pushes numbers via `%d`. To push nil for false booleans, we'd +need to bypass SignalEventParam and push args directly onto the Lua stack using +`lua_pushnil` / `lua_pushnumber`. Requires finding the Lua state pointer and calling +Lua C API functions directly. No Warden concern (just function calls), but invasive +change to the event-push mechanism. + +### Interrupter / Dispeller / Damage Shield spell IDs + +Solvable with a cast tracking ring buffer -- no new hooks. On SPELL_CAST_SUCCESS, record +(casterGUID, spellId, timestamp) for interrupt-type, dispel-type, and damage-shield-type +spells. When the corresponding event fires, look up the most recent matching cast by the +same caster. Pure memory tracking, no Warden concern. + +### SPELL_ENERGIZE amount (RESOLVED) + +Now hooked via ProcessStandardPowerGainMessage (0x62CA00), downstream of +SMSG_SPELLENERGIZELOG (opcode 0x151). Receives spellId, powerType, and display amount +directly. Safe downstream hook -- no Warden concern. diff --git a/src/dpslog/WOTLK_CLEU_SPEC.md b/src/dpslog/WOTLK_CLEU_SPEC.md new file mode 100644 index 0000000..a8e8796 --- /dev/null +++ b/src/dpslog/WOTLK_CLEU_SPEC.md @@ -0,0 +1,201 @@ +# WotLK 3.3.5 COMBAT_LOG_EVENT_UNFILTERED — Full Arg Specification + +Sources: +- `Blizzard_CombatLog.lua` from tekkub/wow-ui-source 3.3.5 branch (authoritative) +- Validated against `bkader/Skada-WoTLK` parser (Core/Functions.lua suffix tables) + +## Base Args (all events) + +| Pos | Field | Type | +|-----|-------|------| +| 1 | timestamp | number | +| 2 | subevent | string | +| 3 | sourceGUID | string | +| 4 | sourceName | string | +| 5 | sourceFlags | number | +| 6 | destGUID | string | +| 7 | destName | string | +| 8 | destFlags | number | + +After base args, events have prefix args then suffix args. + +## Prefixes + +**Spell prefix** (SPELL_, RANGE_, SPELL_PERIODIC_): +- spellId (number) +- spellName (string) +- spellSchool (number) + +**Environmental prefix** (ENVIRONMENTAL_): +- environmentalType (string) — "Drowning", "Falling", "Fatigue", "Fire", "Lava", "Slime" + +**Swing prefix** (SWING_): +- (none) + +**Inline prefix** (DAMAGE_SHIELD, DAMAGE_SHIELD_MISSED, DAMAGE_SPLIT): +- spellId, spellName, spellSchool inlined before suffix (not a standard prefix) + +## Suffixes + +### _DAMAGE +Used by: SPELL_DAMAGE, RANGE_DAMAGE, SPELL_PERIODIC_DAMAGE, DAMAGE_SHIELD, DAMAGE_SPLIT, +SWING_DAMAGE, ENVIRONMENTAL_DAMAGE + +| # | Field | Type | Notes | +|---|-------|------|-------| +| 1 | amount | number | damage dealt | +| 2 | overkill | number | -1 if target alive | +| 3 | school | number | damage school bitmask | +| 4 | resisted | number | 0 if none | +| 5 | blocked | number | 0 if none | +| 6 | absorbed | number | 0 if none | +| 7 | critical | 1/nil | | +| 8 | glancing | 1/nil | | +| 9 | crushing | 1/nil | | + +### _MISSED +Used by: SPELL_MISSED, RANGE_MISSED, SPELL_PERIODIC_MISSED, DAMAGE_SHIELD_MISSED, +SWING_MISSED + +| # | Field | Type | Notes | +|---|-------|------|-------| +| 1 | missType | string | ABSORB, BLOCK, DEFLECT, DODGE, EVADE, IMMUNE, MISS, PARRY, REFLECT, RESIST | +| 2 | amountMissed | number/nil | only for ABSORB, BLOCK, RESIST | + +Note: isOffHand was added in a later client version (post-3.3.5), not present in 3.3.5. + +### _HEAL +Used by: SPELL_HEAL, SPELL_PERIODIC_HEAL + +| # | Field | Type | Notes | +|---|-------|------|-------| +| 1 | amount | number | total heal | +| 2 | overhealing | number | overheal amount | +| 3 | absorbed | number | heal absorb (e.g. Necrotic Aura) | +| 4 | critical | 1/nil | | + +### _ENERGIZE +Used by: SPELL_ENERGIZE, SPELL_PERIODIC_ENERGIZE + +| # | Field | Type | Notes | +|---|-------|------|-------| +| 1 | amount | number | power gained | +| 2 | powerType | number | 0=mana, 1=rage, 2=focus, 3=energy, 4=combo, 5=runes, 6=runic | + +### _DRAIN +Used by: SPELL_DRAIN, SPELL_PERIODIC_DRAIN + +| # | Field | Type | Notes | +|---|-------|------|-------| +| 1 | amount | number | amount drained | +| 2 | powerType | number | power type | +| 3 | extraAmount | number | amount gained by drainer (typically 0) | + +### _LEECH +Used by: SPELL_LEECH, SPELL_PERIODIC_LEECH + +| # | Field | Type | Notes | +|---|-------|------|-------| +| 1 | amount | number | amount leeched | +| 2 | powerType | number | -2=health, 0=mana, etc. | +| 3 | extraAmount | number | amount gained by leecher | + +### _INTERRUPT +Used by: SPELL_INTERRUPT + +| # | Field | Type | Notes | +|---|-------|------|-------| +| 1 | extraSpellId | number | interrupted spell | +| 2 | extraSpellName | string | | +| 3 | extraSchool | number | | + +### _DISPEL +Used by: SPELL_DISPEL + +| # | Field | Type | Notes | +|---|-------|------|-------| +| 1 | extraSpellId | number | dispelled aura | +| 2 | extraSpellName | string | | +| 3 | extraSchool | number | | +| 4 | auraType | string | "BUFF" or "DEBUFF" | + +### _STOLEN +Used by: SPELL_STOLEN (Spellsteal) + +Same as _DISPEL: extraSpellId, extraSpellName, extraSchool, auraType + +### _DISPEL_FAILED +Used by: SPELL_DISPEL_FAILED + +| # | Field | Type | Notes | +|---|-------|------|-------| +| 1 | extraSpellId | number | aura that resisted | +| 2 | extraSpellName | string | | +| 3 | extraSchool | number | | + +### _EXTRA_ATTACKS +Used by: SPELL_EXTRA_ATTACKS + +| # | Field | Type | Notes | +|---|-------|------|-------| +| 1 | amount | number | number of extra attacks | + +### _AURA_APPLIED / _AURA_REMOVED / _AURA_REFRESH +Used by: SPELL_AURA_APPLIED, SPELL_AURA_REMOVED, SPELL_AURA_REFRESH + +| # | Field | Type | Notes | +|---|-------|------|-------| +| 1 | auraType | string | "BUFF" or "DEBUFF" | + +### _AURA_APPLIED_DOSE / _AURA_REMOVED_DOSE + +| # | Field | Type | Notes | +|---|-------|------|-------| +| 1 | auraType | string | "BUFF" or "DEBUFF" | +| 2 | amount | number | stack count | + +### _AURA_BROKEN + +| # | Field | Type | Notes | +|---|-------|------|-------| +| 1 | auraType | string | "BUFF" or "DEBUFF" | + +### _AURA_BROKEN_SPELL + +| # | Field | Type | Notes | +|---|-------|------|-------| +| 1 | extraSpellId | number | breaking spell | +| 2 | extraSpellName | string | | +| 3 | extraSchool | number | | +| 4 | auraType | string | "BUFF" or "DEBUFF" | + +### No-suffix events +_CAST_START, _CAST_SUCCESS, _INSTAKILL, _SUMMON, _RESURRECT, _CREATE: +(no suffix args — spell prefix only) + +### _CAST_FAILED + +| # | Field | Type | Notes | +|---|-------|------|-------| +| 1 | failedType | string | localized failure reason | + +### UNIT_DIED, PARTY_KILL, UNIT_DESTROYED +(base args only, no prefix, no suffix) + +## Power Types +0=Mana, 1=Rage, 2=Focus, 3=Energy, 4=Combo Points, 5=Runes, 6=Runic Power, -2=Health + +## School Bitmask +1=Physical, 2=Holy, 4=Fire, 8=Nature, 16=Frost, 32=Shadow, 64=Arcane + +## Notes +- overkill added in WotLK (not in TBC) +- overhealing on _HEAL added in 3.2/3.3 +- absorbed on _HEAL (heal absorb) added in 3.3 +- Boolean fields are 1 or nil, not true/false +- DAMAGE_SPLIT exists as a distinct subevent, uses inline prefix + _DAMAGE suffix +- DAMAGE_SHIELD and DAMAGE_SHIELD_MISSED use inline prefix (not standard spell prefix) +- spellName is part of the prefix; our vanilla DLL omits it (addon can look up from spellId) +- hideCaster was added in 4.1.0 (Cata), NOT present in WotLK 3.3.5 +- Skada aliases DAMAGE_SHIELD and DAMAGE_SPLIT to SPELL_DAMAGE layout (prefix+suffix) +- Skada reads _AURA_APPLIED/REMOVED/REFRESH with (auraType, amount) — amount is nil for non-dose diff --git a/src/dpslog/WeirdUtils_DPSLog/DPSLog.lua b/src/dpslog/WeirdUtils_DPSLog/DPSLog.lua index 39b8272..e4fa2a8 100644 --- a/src/dpslog/WeirdUtils_DPSLog/DPSLog.lua +++ b/src/dpslog/WeirdUtils_DPSLog/DPSLog.lua @@ -1,42 +1,29 @@ --- DPS Log Tracker — popup checklist for verifying all 35 COMBAT_LOG_EVENT subevents +-- DPS Log Tracker -- WotLK CLEU parity for vanilla 1.12.1 -- --- All events share: arg1=subevent, arg2=sourceGUID, arg3=destGUID --- Remaining args are subevent-specific (see layouts below). +-- Event layout matches WotLK 3.3.5 COMBAT_LOG_EVENT_UNFILTERED (see WOTLK_CLEU_SPEC.md). +-- Base: arg1=subevent, arg2=sourceGUID, arg3=sourceName, arg4=destGUID, arg5=destName +-- Spell prefix: spellId, spellName, spellSchool +-- Env prefix: envType (string) +-- Swing prefix: (none) -- ============================================================================ --- Reference tables +-- Reference -- ============================================================================ --- 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) +-- School bitmask: 1=Physical, 2=Holy, 4=Fire, 8=Nature, 16=Frost, 32=Shadow, 64=Arcane +-- Power types: 0=Mana, 1=Rage, 2=Focus, 3=Energy, 4=Combo, -2=Health +-- Miss types: MISS, DODGE, PARRY, BLOCK, EVADE, IMMUNE, DEFLECT, RESIST, ABSORB, REFLECT +-- Aura types: BUFF, DEBUFF +-- Env types: EXHAUSTED, DROWNING, FALLING, LAVA, SLIME, FIRE --- 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 format = string.format + local function schoolName(school) if school == 1 then return "Physical" elseif school == 2 then return "Holy" @@ -53,68 +40,118 @@ local function s(v) return tostring(v) end +local function displayName(name, guid) + if name and name ~= "" then return name end + return guid or "?" +end + -- ============================================================================ --- Subevent definitions — ordered list of all 34 +-- Arg position constants -- WotLK CLEU parity -- ============================================================================ --- Each entry: { displayName, subevent, [variantField], [variantValue] } --- variantField: which arg to check for the variant (nil = any occurrence) --- variantValue: specific value to match (string or number) +-- Base: arg1=subevent, arg2=srcGUID, arg3=srcName, arg4=dstGUID, arg5=dstName +-- Spell prefix events: arg6=spellId, arg7=spellName, arg8=spellSchool +-- Suffix starts at arg9 for spell-prefix events +-- Swing events: suffix starts at arg6 (no prefix) +-- Env events: arg6=envType, suffix starts at arg7 + +-- _DAMAGE suffix (9 fields): amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing +-- Spell damage: arg6-8=prefix, arg9-17=suffix +-- Swing damage: arg6-14=suffix +-- Env damage: arg6=envType, arg7-15=suffix + +-- _MISSED suffix: missType, amountMissed +-- Spell missed: arg6-8=prefix, arg9=missType, arg10=amountMissed +-- Swing missed: arg6=missType, arg7=amountMissed + +-- _HEAL suffix: amount, overheal, absorbed, critical +-- arg6-8=prefix, arg9-12=suffix + +-- _ENERGIZE suffix: amount, powerType +-- arg6-8=prefix, arg9-10=suffix + +-- _LEECH/_DRAIN suffix: amount, powerType, extraAmount +-- arg6-8=prefix, arg9-11=suffix + +-- _EXTRA_ATTACKS suffix: amount +-- arg6-8=prefix, arg9=amount + +-- _INTERRUPT/_DISPEL_FAILED suffix: extraSpellId, extraSpellName, extraSchool +-- arg6-8=prefix, arg9-11=suffix + +-- _DISPEL/_STOLEN/_AURA_BROKEN_SPELL suffix: extraSpellId, extraSpellName, extraSchool, auraType +-- arg6-8=prefix, arg9-12=suffix + +-- _AURA_APPLIED/REMOVED/REFRESH/BROKEN: auraType +-- arg6-8=prefix, arg9=auraType + +-- _AURA_DOSE: auraType, amount +-- arg6-8=prefix, arg9=auraType, arg10=amount + +-- _CAST_FAILED: failedType +-- arg6-8=prefix, arg9=failedType + +-- ============================================================================ +-- Subevent definitions -- variant checklist +-- ============================================================================ + +-- { displayName, subevent, [variantField], [variantValue] } local SUBEVENTS = { - -- Damage + -- Damage (spell prefix: arg9=amount ... arg15=critical, arg16=glancing, arg17=crushing) { "SPELL_DAMAGE", "SPELL_DAMAGE" }, - { "SPELL_DAMAGE (crit)", "SPELL_DAMAGE", 10, 1 }, + { "SPELL_DAMAGE (crit)", "SPELL_DAMAGE", 15, 1 }, { "RANGE_DAMAGE", "RANGE_DAMAGE" }, - { "RANGE_DAMAGE (crit)", "RANGE_DAMAGE", 10, 1 }, + { "RANGE_DAMAGE (crit)", "RANGE_DAMAGE", 15, 1 }, { "SPELL_PERIODIC_DAMAGE", "SPELL_PERIODIC_DAMAGE" }, { "DAMAGE_SHIELD", "DAMAGE_SHIELD" }, { "DAMAGE_SPLIT", "DAMAGE_SPLIT" }, + -- Swing damage: arg6=amount ... arg12=critical, arg13=glancing, arg14=crushing { "SWING_DAMAGE", "SWING_DAMAGE" }, - { "SWING_DAMAGE (crit)", "SWING_DAMAGE", 9, 1 }, - { "SWING_DAMAGE (glancing)", "SWING_DAMAGE", 10, 1 }, -- flags bit 0 - { "SWING_DAMAGE (crushing)", "SWING_DAMAGE", 10, 2 }, -- flags bit 1 - { "SWING_DAMAGE (offhand)", "SWING_DAMAGE", 10, 4 }, -- flags bit 2 - { "ENVIRONMENTAL (DROWNING)", "ENVIRONMENTAL_DAMAGE", 4, "DROWNING" }, - { "ENVIRONMENTAL (FALLING)", "ENVIRONMENTAL_DAMAGE", 4, "FALLING" }, - { "ENVIRONMENTAL (LAVA)", "ENVIRONMENTAL_DAMAGE", 4, "LAVA" }, - { "ENVIRONMENTAL (SLIME)", "ENVIRONMENTAL_DAMAGE", 4, "SLIME" }, - { "ENVIRONMENTAL (FIRE)", "ENVIRONMENTAL_DAMAGE", 4, "FIRE" }, - { "ENVIRONMENTAL (EXHAUSTED)", "ENVIRONMENTAL_DAMAGE", 4, "EXHAUSTED" }, -- twow does not implement fatigue damage - -- Misses — swing - { "SWING_MISSED (MISS)", "SWING_MISSED", 4, "MISS" }, - { "SWING_MISSED (DODGE)", "SWING_MISSED", 4, "DODGE" }, - { "SWING_MISSED (PARRY)", "SWING_MISSED", 4, "PARRY" }, - { "SWING_MISSED (BLOCK)", "SWING_MISSED", 4, "BLOCK" }, - { "SWING_MISSED (EVADE)", "SWING_MISSED", 4, "EVADE" }, - { "SWING_MISSED (IMMUNE)", "SWING_MISSED", 4, "IMMUNE" }, - { "SWING_MISSED (RESIST)", "SWING_MISSED", 4, "RESIST" }, - { "SWING_MISSED (ABSORB)", "SWING_MISSED", 4, "ABSORB" }, - -- Misses — spell/range - { "SPELL_MISSED (MISS)", "SPELL_MISSED", 6, "MISS" }, - { "SPELL_MISSED (DODGE)", "SPELL_MISSED", 6, "DODGE" }, - { "SPELL_MISSED (PARRY)", "SPELL_MISSED", 6, "PARRY" }, - { "SPELL_MISSED (IMMUNE)", "SPELL_MISSED", 6, "IMMUNE" }, - { "SPELL_MISSED (RESIST)", "SPELL_MISSED", 6, "RESIST" }, - { "SPELL_MISSED (ABSORB)", "SPELL_MISSED", 6, "ABSORB" }, - { "SPELL_MISSED (DEFLECT)", "SPELL_MISSED", 6, "DEFLECT" }, -- dead in vanilla, kept for completeness - { "SPELL_MISSED (REFLECT)", "SPELL_MISSED", 6, "REFLECT" }, + { "SWING_DAMAGE (crit)", "SWING_DAMAGE", 12, 1 }, + { "SWING_DAMAGE (glancing)", "SWING_DAMAGE", 13, 1 }, + { "SWING_DAMAGE (crushing)", "SWING_DAMAGE", 14, 1 }, + -- Env damage: arg6=envType, arg7=amount ... arg15=crushing + { "ENVIRONMENTAL (DROWNING)", "ENVIRONMENTAL_DAMAGE", 6, "DROWNING" }, + { "ENVIRONMENTAL (FALLING)", "ENVIRONMENTAL_DAMAGE", 6, "FALLING" }, + { "ENVIRONMENTAL (LAVA)", "ENVIRONMENTAL_DAMAGE", 6, "LAVA" }, + { "ENVIRONMENTAL (SLIME)", "ENVIRONMENTAL_DAMAGE", 6, "SLIME" }, + { "ENVIRONMENTAL (FIRE)", "ENVIRONMENTAL_DAMAGE", 6, "FIRE" }, + { "ENVIRONMENTAL (EXHAUSTED)", "ENVIRONMENTAL_DAMAGE", 6, "EXHAUSTED" }, -- twow: no fatigue + -- Misses -- swing: arg6=missType + { "SWING_MISSED (MISS)", "SWING_MISSED", 6, "MISS" }, + { "SWING_MISSED (DODGE)", "SWING_MISSED", 6, "DODGE" }, + { "SWING_MISSED (PARRY)", "SWING_MISSED", 6, "PARRY" }, + { "SWING_MISSED (BLOCK)", "SWING_MISSED", 6, "BLOCK" }, + { "SWING_MISSED (EVADE)", "SWING_MISSED", 6, "EVADE" }, + { "SWING_MISSED (IMMUNE)", "SWING_MISSED", 6, "IMMUNE" }, + { "SWING_MISSED (RESIST)", "SWING_MISSED", 6, "RESIST" }, + { "SWING_MISSED (ABSORB)", "SWING_MISSED", 6, "ABSORB" }, + -- Misses -- spell: arg9=missType (after prefix) + { "SPELL_MISSED (MISS)", "SPELL_MISSED", 9, "MISS" }, + { "SPELL_MISSED (DODGE)", "SPELL_MISSED", 9, "DODGE" }, + { "SPELL_MISSED (PARRY)", "SPELL_MISSED", 9, "PARRY" }, + { "SPELL_MISSED (IMMUNE)", "SPELL_MISSED", 9, "IMMUNE" }, + { "SPELL_MISSED (RESIST)", "SPELL_MISSED", 9, "RESIST" }, + { "SPELL_MISSED (ABSORB)", "SPELL_MISSED", 9, "ABSORB" }, + { "SPELL_MISSED (DEFLECT)", "SPELL_MISSED", 9, "DEFLECT" }, -- dead in vanilla + { "SPELL_MISSED (REFLECT)", "SPELL_MISSED", 9, "REFLECT" }, { "RANGE_MISSED", "RANGE_MISSED" }, { "SPELL_PERIODIC_MISSED", "SPELL_PERIODIC_MISSED" }, { "DAMAGE_SHIELD_MISSED", "DAMAGE_SHIELD_MISSED" }, - -- Heals + -- Heals: arg9=amount, arg10=overheal, arg11=absorbed, arg12=critical { "SPELL_HEAL", "SPELL_HEAL" }, - { "SPELL_HEAL (crit)", "SPELL_HEAL", 8, 1 }, + { "SPELL_HEAL (crit)", "SPELL_HEAL", 12, 1 }, { "SPELL_PERIODIC_HEAL", "SPELL_PERIODIC_HEAL" }, - -- Power - { "SPELL_ENERGIZE (Mana)", "SPELL_ENERGIZE" }, -- amount=0 from hook, can't distinguish + -- Power: arg9=amount, arg10=powerType + { "SPELL_ENERGIZE", "SPELL_ENERGIZE" }, { "SPELL_PERIODIC_ENERGIZE", "SPELL_PERIODIC_ENERGIZE" }, { "SPELL_PERIODIC_DRAIN", "SPELL_PERIODIC_DRAIN" }, { "SPELL_PERIODIC_LEECH", "SPELL_PERIODIC_LEECH" }, - -- Auras - { "SPELL_AURA_APPLIED (BUFF)", "SPELL_AURA_APPLIED", 6, "BUFF" }, - { "SPELL_AURA_APPLIED (DEBUFF)", "SPELL_AURA_APPLIED", 6, "DEBUFF" }, - { "SPELL_AURA_REMOVED (BUFF)", "SPELL_AURA_REMOVED", 6, "BUFF" }, - { "SPELL_AURA_REMOVED (DEBUFF)", "SPELL_AURA_REMOVED", 6, "DEBUFF" }, + -- Auras: arg9=auraType + { "SPELL_AURA_APPLIED (BUFF)", "SPELL_AURA_APPLIED", 9, "BUFF" }, + { "SPELL_AURA_APPLIED (DEBUFF)", "SPELL_AURA_APPLIED", 9, "DEBUFF" }, + { "SPELL_AURA_REMOVED (BUFF)", "SPELL_AURA_REMOVED", 9, "BUFF" }, + { "SPELL_AURA_REMOVED (DEBUFF)", "SPELL_AURA_REMOVED", 9, "DEBUFF" }, { "SPELL_AURA_APPLIED_DOSE", "SPELL_AURA_APPLIED_DOSE" }, { "SPELL_AURA_REMOVED_DOSE", "SPELL_AURA_REMOVED_DOSE" }, { "SPELL_AURA_REFRESH", "SPELL_AURA_REFRESH" }, @@ -124,7 +161,7 @@ local SUBEVENTS = { { "SPELL_CAST_START", "SPELL_CAST_START" }, { "SPELL_CAST_SUCCESS", "SPELL_CAST_SUCCESS" }, { "SPELL_CAST_FAILED", "SPELL_CAST_FAILED" }, - -- Misc spell + -- Misc { "SPELL_INTERRUPT", "SPELL_INTERRUPT" }, { "SPELL_DISPEL", "SPELL_DISPEL" }, { "SPELL_DISPEL_FAILED", "SPELL_DISPEL_FAILED" }, @@ -140,9 +177,7 @@ local SUBEVENTS = { local NUM_SUBEVENTS = table.getn(SUBEVENTS) --- Build reverse lookup: for each subevent name, list all indices that match it --- (one subevent may have multiple variant entries) -local subeventIndices = {} -- [subeventName] = { idx1, idx2, ... } +local subeventIndices = {} for i = 1, NUM_SUBEVENTS do local name = SUBEVENTS[i][2] if not subeventIndices[name] then @@ -151,254 +186,295 @@ for i = 1, NUM_SUBEVENTS do table.insert(subeventIndices[name], i) end --- Tracking state -local seen = {} -- [index] = true -local seenArgs = {} -- [index] = formatted arg string +local seen = {} +local seenArgs = {} local seenCount = 0 -- ============================================================================ --- Arg formatters per subevent (return a short summary string) +-- Arg formatters (summary string for tracker display) -- ============================================================================ +-- Formatters receive args starting from arg6 (after base: sub, srcGUID, srcName, dstGUID, dstName) -local function fmtSpellDmg(spellId, amount, school, resisted, absorbed, blocked, critical) +-- Spell-prefix damage: spellId, spellName, spellSchool, amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing +local function fmtSpellDmg(spellId, spellName, spellSchool, amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing) 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) + local ok = (overkill and overkill >= 0) and format(" OK=%s", s(overkill)) or "" + local g = (glancing == 1) and " GLANC" or "" + local cr = (crushing == 1) and " CRUSH" or "" + return format("[%s] %s amt=%s %s res=%s blk=%s abs=%s%s%s%s%s", + s(spellId), s(spellName), s(amount), schoolName(school or 0), s(resisted), s(blocked), s(absorbed), c, ok, g, cr) end -local function fmtSwingDmg(amount, school, resisted, absorbed, blocked, critical, flags) +-- Swing damage: amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing +local function fmtSwingDmg(amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing) 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) + local ok = (overkill and overkill >= 0) and format(" OK=%s", s(overkill)) or "" + local g = (glancing == 1) and " GLANC" or "" + local cr = (crushing == 1) and " CRUSH" or "" + return format("amt=%s %s res=%s blk=%s abs=%s%s%s%s%s", + s(amount), schoolName(school or 0), s(resisted), s(blocked), s(absorbed), c, ok, g, cr) +end + +-- Spell prefix helper +local function spellPrefix(spellId, spellName, spellSchool) + return format("[%s] %s %s", s(spellId), s(spellName), schoolName(spellSchool or 0)) 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)) +-- Spell-prefix damage (args start at a6=spellId) +argFormatters["SPELL_DAMAGE"] = function(...) return fmtSpellDmg(arg[1],arg[2],arg[3],arg[4],arg[5],arg[6],arg[7],arg[8],arg[9],arg[10],arg[11],arg[12]) end +argFormatters["RANGE_DAMAGE"] = argFormatters["SPELL_DAMAGE"] +argFormatters["SPELL_PERIODIC_DAMAGE"] = argFormatters["SPELL_DAMAGE"] +argFormatters["DAMAGE_SHIELD"] = argFormatters["SPELL_DAMAGE"] +argFormatters["DAMAGE_SPLIT"] = argFormatters["SPELL_DAMAGE"] + +-- Swing damage (no prefix, args start at a6=amount) +argFormatters["SWING_DAMAGE"] = function(...) return fmtSwingDmg(arg[1],arg[2],arg[3],arg[4],arg[5],arg[6],arg[7],arg[8],arg[9]) end + +-- Env damage: a6=envType, a7-a15=damage suffix +argFormatters["ENVIRONMENTAL_DAMAGE"] = function(...) + local ok = (arg[3] and arg[3] >= 0) and format(" OK=%s", s(arg[3])) or "" + return format("env=%s amt=%s %s abs=%s%s", s(arg[1]), s(arg[2]), schoolName(arg[4] or 0), s(arg[7]), ok) end --- Pattern B — swing missed -argFormatters["SWING_MISSED"] = function(a4) return s(a4) end +-- Swing missed: a6=missType, a7=amountMissed +argFormatters["SWING_MISSED"] = function(...) return format("%s amt=%s", s(arg[1]), s(arg[2])) 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 +-- Spell missed: a6-a8=prefix, a9=missType, a10=amountMissed +argFormatters["SPELL_MISSED"] = function(...) return format("%s %s amt=%s", spellPrefix(arg[1],arg[2],arg[3]), s(arg[4]), s(arg[5])) end +argFormatters["SPELL_PERIODIC_MISSED"] = argFormatters["SPELL_MISSED"] +argFormatters["DAMAGE_SHIELD_MISSED"] = argFormatters["SPELL_MISSED"] +argFormatters["RANGE_MISSED"] = argFormatters["SPELL_MISSED"] --- 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)) +-- Spell heal: a6-a8=prefix, a9=amount, a10=overheal, a11=absorbed, a12=critical +argFormatters["SPELL_HEAL"] = function(...) + local c = (arg[7] == 1) and " CRIT" or "" + local oh = (arg[5] and arg[5] > 0) and format(" OH=%s", s(arg[5])) or "" + return format("%s amt=%s abs=%s%s%s", spellPrefix(arg[1],arg[2],arg[3]), s(arg[4]), s(arg[6]), c, oh) end -argFormatters["SPELL_INTERRUPT"] = function(a4,a5,a6,a7) - return format("spell=%s extra=%s %s", s(a4), s(a6), schoolName(a7 or 0)) +argFormatters["SPELL_PERIODIC_HEAL"] = argFormatters["SPELL_HEAL"] + +-- Spell energize: a6-a8=prefix, a9=amount, a10=powerType +argFormatters["SPELL_ENERGIZE"] = function(...) + return format("%s amt=%s type=%s", spellPrefix(arg[1],arg[2],arg[3]), s(arg[4]), powerNames[arg[5]] or s(arg[5])) 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)) +argFormatters["SPELL_PERIODIC_ENERGIZE"] = argFormatters["SPELL_ENERGIZE"] + +-- Spell leech/drain: a6-a8=prefix, a9=amount, a10=powerType, a11=extraAmount +argFormatters["SPELL_PERIODIC_LEECH"] = function(...) + return format("%s amt=%s type=%s gain=%s", spellPrefix(arg[1],arg[2],arg[3]), s(arg[4]), s(arg[5]), s(arg[6])) +end +argFormatters["SPELL_PERIODIC_DRAIN"] = argFormatters["SPELL_PERIODIC_LEECH"] + +-- Extra attacks: a6-a8=prefix, a9=amount +argFormatters["SPELL_EXTRA_ATTACKS"] = function(...) + return format("%s amt=%s", spellPrefix(arg[1],arg[2],arg[3]), s(arg[4])) 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 +-- Aura applied/removed/refresh/broken: a6-a8=prefix, a9=auraType +argFormatters["SPELL_AURA_APPLIED"] = function(...) return format("%s %s", spellPrefix(arg[1],arg[2],arg[3]), s(arg[4])) end +argFormatters["SPELL_AURA_REMOVED"] = argFormatters["SPELL_AURA_APPLIED"] +argFormatters["SPELL_AURA_REFRESH"] = argFormatters["SPELL_AURA_APPLIED"] +argFormatters["SPELL_AURA_BROKEN"] = argFormatters["SPELL_AURA_APPLIED"] --- New subevents -argFormatters["DAMAGE_SPLIT"] = function(a4,a5,a6,a7,a8,a9,a10) return fmtSpellDmg(a4,a5,a6,a7,a8,a9,a10) end -argFormatters["SPELL_DISPEL_FAILED"] = function(a4,a5) return format("spell=%s %s", s(a4), schoolName(a5 or 0)) end +-- Aura dose: a6-a8=prefix, a9=auraType, a10=amount +argFormatters["SPELL_AURA_APPLIED_DOSE"] = function(...) return format("%s %s x%s", spellPrefix(arg[1],arg[2],arg[3]), s(arg[4]), s(arg[5])) end +argFormatters["SPELL_AURA_REMOVED_DOSE"] = argFormatters["SPELL_AURA_APPLIED_DOSE"] + +-- Cast: a6-a8=prefix only +argFormatters["SPELL_CAST_START"] = function(...) return spellPrefix(arg[1],arg[2],arg[3]) end +argFormatters["SPELL_CAST_SUCCESS"] = argFormatters["SPELL_CAST_START"] +argFormatters["SPELL_SUMMON"] = argFormatters["SPELL_CAST_START"] +argFormatters["SPELL_RESURRECT"] = argFormatters["SPELL_CAST_START"] +argFormatters["SPELL_INSTAKILL"] = argFormatters["SPELL_CAST_START"] + +-- Cast failed: a6-a8=prefix, a9=failedType +argFormatters["SPELL_CAST_FAILED"] = function(...) return format("%s reason=%s", spellPrefix(arg[1],arg[2],arg[3]), s(arg[4])) end + +-- Interrupt/dispel_failed: a6-a8=prefix, a9=extraSpellId, a10=extraSpellName, a11=extraSchool +argFormatters["SPELL_INTERRUPT"] = function(...) + return format("%s -> extra=[%s] %s %s", spellPrefix(arg[1],arg[2],arg[3]), s(arg[4]), s(arg[5]), schoolName(arg[6] or 0)) +end +argFormatters["SPELL_DISPEL_FAILED"] = argFormatters["SPELL_INTERRUPT"] + +-- Dispel/stolen/aura_broken_spell: a6-a8=prefix, a9=extraSpellId, a10=extraSpellName, a11=extraSchool, a12=auraType +argFormatters["SPELL_DISPEL"] = function(...) + return format("%s -> extra=[%s] %s %s %s", spellPrefix(arg[1],arg[2],arg[3]), s(arg[4]), s(arg[5]), schoolName(arg[6] or 0), s(arg[7])) +end +argFormatters["SPELL_AURA_BROKEN_SPELL"] = argFormatters["SPELL_DISPEL"] + +-- Base (no args) +argFormatters["UNIT_DIED"] = function() return "" end +argFormatters["PARTY_KILL"] = function() return "" end argFormatters["UNIT_DESTROYED"] = function() return "" end -- ============================================================================ --- Chat output handlers (preserved from original) +-- Chat output handlers -- ============================================================================ +-- All chatHandlers receive: (src, srcName, dst, dstName, ...) where ... starts at arg6 local chatHandlers = {} -chatHandlers["SPELL_DAMAGE"] = function(src, dst, spellId, amount, school, resisted, absorbed, blocked, critical) +-- Spell damage: src, srcName, dst, dstName, spellId, spellName, spellSchool, amount, overkill, school, resisted, blocked, absorbed, critical +chatHandlers["SPELL_DAMAGE"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, amount, overkill, school, resisted, blocked, absorbed, 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)) + local ok = (overkill and overkill >= 0) and format(" (OK %d)", overkill) or "" + DEFAULT_CHAT_FRAME:AddMessage(format("|cffff0000SPELL_DAMAGE|r %s -> %s: %s[%d] for %d %s%s%s", displayName(srcName, src), displayName(dstName, dst), spellName or "", spellId, amount, schoolName(school), crit, ok)) 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)) +chatHandlers["RANGE_DAMAGE"] = chatHandlers["SPELL_DAMAGE"] +chatHandlers["DAMAGE_SPLIT"] = chatHandlers["SPELL_DAMAGE"] + +chatHandlers["SPELL_PERIODIC_DAMAGE"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, amount, overkill, school) + DEFAULT_CHAT_FRAME:AddMessage(format("|cffcc0000SPELL_PERIODIC_DAMAGE|r %s -> %s: %s[%d] for %d %s", displayName(srcName, src), displayName(dstName, dst), spellName or "", spellId, amount, schoolName(school))) end -chatHandlers["SWING_DAMAGE"] = function(src, dst, amount, school, resisted, absorbed, blocked, critical, flags) + +chatHandlers["DAMAGE_SHIELD"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, amount, overkill, school) + DEFAULT_CHAT_FRAME:AddMessage(format("|cffff4400DAMAGE_SHIELD|r %s -> %s: %d %s", displayName(srcName, src), displayName(dstName, dst), amount, schoolName(school))) +end + +-- Swing damage: src, srcName, dst, dstName, amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing +chatHandlers["SWING_DAMAGE"] = function(src, srcName, dst, dstName, amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing) 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)) + if overkill and overkill >= 0 then extra = extra .. format(" OK=%d", overkill) end + if glancing == 1 then extra = extra .. " GLANCING" end + if crushing == 1 then extra = extra .. " CRUSHING" end + DEFAULT_CHAT_FRAME:AddMessage(format("|cffff0000SWING_DAMAGE|r %s -> %s: %d%s", displayName(srcName, src), displayName(dstName, 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)) + +-- Swing missed: src, srcName, dst, dstName, missType, amountMissed +chatHandlers["SWING_MISSED"] = function(src, srcName, dst, dstName, missType, amountMissed) + DEFAULT_CHAT_FRAME:AddMessage(format("|cffaaaaaaSWING_MISSED|r %s -> %s: %s", displayName(srcName, src), displayName(dstName, 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)) + +-- Spell missed: src, srcName, dst, dstName, spellId, spellName, spellSchool, missType, amountMissed +chatHandlers["SPELL_MISSED"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, missType, amountMissed) + DEFAULT_CHAT_FRAME:AddMessage(format("|cffaaaaaaSPELL_MISSED|r %s -> %s: %s[%d] %s", displayName(srcName, src), displayName(dstName, dst), spellName or "", 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)) +chatHandlers["RANGE_MISSED"] = chatHandlers["SPELL_MISSED"] +chatHandlers["SPELL_PERIODIC_MISSED"] = chatHandlers["SPELL_MISSED"] +chatHandlers["DAMAGE_SHIELD_MISSED"] = chatHandlers["SPELL_MISSED"] + +-- Env damage: src, srcName, dst, dstName, envType, amount, overkill, school, resisted, blocked, absorbed +chatHandlers["ENVIRONMENTAL_DAMAGE"] = function(src, srcName, dst, dstName, envType, amount) + DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800ENVIRONMENTAL_DAMAGE|r %s: %s for %d", displayName(dstName, dst), envType, amount)) 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) + +-- Spell heal: src, srcName, dst, dstName, spellId, spellName, spellSchool, amount, overheal, absorbed, critical +chatHandlers["SPELL_HEAL"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, 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)) + local oh = (overheal and overheal > 0) and format(" (OH %d)", overheal) or "" + DEFAULT_CHAT_FRAME:AddMessage(format("|cff00ff00SPELL_HEAL|r %s -> %s: %s[%d] for %d%s%s", displayName(srcName, src), displayName(dstName, dst), spellName or "", spellId, amount, crit, oh)) 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)) +chatHandlers["SPELL_PERIODIC_HEAL"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, amount, overheal) + local oh = (overheal and overheal > 0) and format(" (OH %d)", overheal) or "" + DEFAULT_CHAT_FRAME:AddMessage(format("|cff00cc00SPELL_PERIODIC_HEAL|r %s -> %s: %s[%d] for %d%s", displayName(srcName, src), displayName(dstName, dst), spellName or "", spellId, amount, oh)) 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))) + +-- Energize: src, srcName, dst, dstName, spellId, spellName, spellSchool, amount, powerType +chatHandlers["SPELL_ENERGIZE"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, amount, powerType) + DEFAULT_CHAT_FRAME:AddMessage(format("|cff4488ffSPELL_ENERGIZE|r %s -> %s: %s[%d] +%d %s", displayName(srcName, src), displayName(dstName, dst), spellName or "", spellId, amount, powerNames[powerType] or "?")) 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)) +chatHandlers["SPELL_PERIODIC_ENERGIZE"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, amount, powerType) + DEFAULT_CHAT_FRAME:AddMessage(format("|cff4488ffSPELL_PERIODIC_ENERGIZE|r %s -> %s: %s[%d] +%d %s", displayName(srcName, src), displayName(dstName, dst), spellName or "", spellId, amount, powerNames[powerType] or "?")) 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)) + +-- Leech: src, srcName, dst, dstName, spellId, spellName, spellSchool, amount, powerType, extraAmount +chatHandlers["SPELL_PERIODIC_LEECH"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, amount, powerType, extraAmount) + DEFAULT_CHAT_FRAME:AddMessage(format("|cff8844ffSPELL_PERIODIC_LEECH|r %s -> %s: %s[%d] drained %d, gained %d", displayName(srcName, src), displayName(dstName, dst), spellName or "", spellId, amount, extraAmount or 0)) 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)) + +-- Drain: src, srcName, dst, dstName, spellId, spellName, spellSchool, amount, powerType, extraAmount +chatHandlers["SPELL_PERIODIC_DRAIN"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, amount, powerType, extraAmount) + local pname = powerNames[powerType] or "?" + DEFAULT_CHAT_FRAME:AddMessage(format("|cff8844ffSPELL_PERIODIC_DRAIN|r %s -> %s: %s[%d] drained %d %s", displayName(srcName, src), displayName(dstName, dst), spellName or "", spellId, amount, pname)) 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)) + +-- Aura: src, srcName, dst, dstName, spellId, spellName, spellSchool, auraType +chatHandlers["SPELL_AURA_APPLIED"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, auraType) + DEFAULT_CHAT_FRAME:AddMessage(format("|cff8888ffSPELL_AURA_APPLIED|r %s: %s[%d] (%s)", displayName(dstName, dst), spellName or "", spellId, auraType)) 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))) +chatHandlers["SPELL_AURA_REMOVED"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, auraType) + DEFAULT_CHAT_FRAME:AddMessage(format("|cff6666ccSPELL_AURA_REMOVED|r %s: %s[%d] (%s)", displayName(dstName, dst), spellName or "", spellId, auraType)) 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)) +chatHandlers["SPELL_AURA_REFRESH"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, auraType) + DEFAULT_CHAT_FRAME:AddMessage(format("|cff7777ddSPELL_AURA_REFRESH|r %s: %s[%d] (%s)", displayName(dstName, dst), spellName or "", 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)) +chatHandlers["SPELL_AURA_BROKEN"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, auraType) + DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800SPELL_AURA_BROKEN|r %s -> %s: %s[%d] broken (%s)", displayName(srcName, src), displayName(dstName, dst), spellName or "", spellId, 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)) +chatHandlers["SPELL_AURA_APPLIED_DOSE"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, auraType, stacks) + DEFAULT_CHAT_FRAME:AddMessage(format("|cff8888ffSPELL_AURA_APPLIED_DOSE|r %s: %s[%d] x%d (%s)", displayName(dstName, dst), spellName or "", 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)) +chatHandlers["SPELL_AURA_REMOVED_DOSE"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, auraType, stacks) + DEFAULT_CHAT_FRAME:AddMessage(format("|cff6666ccSPELL_AURA_REMOVED_DOSE|r %s: %s[%d] x%d (%s)", displayName(dstName, dst), spellName or "", spellId, stacks, 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)) +chatHandlers["SPELL_AURA_BROKEN_SPELL"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, extraSpellId, extraSpellName, extraSchool, auraType) + DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800SPELL_AURA_BROKEN_SPELL|r %s -> %s: %s[%d] broken by %s[%d]", displayName(srcName, src), displayName(dstName, dst), spellName or "", spellId, extraSpellName or "", extraSpellId)) 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)) + +-- Cast +chatHandlers["SPELL_CAST_START"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool) + DEFAULT_CHAT_FRAME:AddMessage(format("|cffffff00SPELL_CAST_START|r %s: %s[%d]", displayName(srcName, src), spellName or "", spellId)) 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))) +chatHandlers["SPELL_CAST_SUCCESS"] = function(src, srcName, dst, dstName, spellId, spellName) + DEFAULT_CHAT_FRAME:AddMessage(format("|cffffff00SPELL_CAST_SUCCESS|r %s: %s[%d]", displayName(srcName, src), spellName or "", spellId)) end -chatHandlers["SPELL_CAST_SUCCESS"] = function(src, dst, spellId) - DEFAULT_CHAT_FRAME:AddMessage(format("|cffffff00SPELL_CAST_SUCCESS|r %s: spell %d", src, spellId)) +chatHandlers["SPELL_CAST_FAILED"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, failedType) + DEFAULT_CHAT_FRAME:AddMessage(format("|cffff4444SPELL_CAST_FAILED|r %s: %s[%d] (%s)", displayName(srcName, src), spellName or "", spellId, failedType)) 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)) + +-- Interrupt: src, srcName, dst, dstName, spellId, spellName, spellSchool, extraSpellId, extraSpellName, extraSchool +chatHandlers["SPELL_INTERRUPT"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, extraSpellId, extraSpellName, extraSchool) + DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800SPELL_INTERRUPT|r %s interrupted %s: %s[%d]", displayName(srcName, src), displayName(dstName, dst), extraSpellName or "", extraSpellId)) 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))) + +-- Dispel: src, srcName, dst, dstName, spellId, spellName, spellSchool, extraSpellId, extraSpellName, extraSchool, auraType +chatHandlers["SPELL_DISPEL"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, extraSpellId, extraSpellName, extraSchool, auraType) + DEFAULT_CHAT_FRAME:AddMessage(format("|cff88ffffSPELL_DISPEL|r %s dispelled %s: %s[%d] (%s)", displayName(srcName, src), displayName(dstName, dst), extraSpellName or "", extraSpellId, auraType)) 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))) + +-- Dispel failed: src, srcName, dst, dstName, spellId, spellName, spellSchool, extraSpellId, extraSpellName, extraSchool +chatHandlers["SPELL_DISPEL_FAILED"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, extraSpellId, extraSpellName, extraSchool) + DEFAULT_CHAT_FRAME:AddMessage(format("|cff888888SPELL_DISPEL_FAILED|r %s -> %s: %s[%d] resisted", displayName(srcName, src), displayName(dstName, dst), extraSpellName or "", extraSpellId)) end -chatHandlers["SPELL_EXTRA_ATTACKS"] = function(src, dst, spellId) - DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800SPELL_EXTRA_ATTACKS|r %s: spell %d", src, spellId)) + +-- Extra attacks: src, srcName, dst, dstName, spellId, spellName, spellSchool, amount +chatHandlers["SPELL_EXTRA_ATTACKS"] = function(src, srcName, dst, dstName, spellId, spellName, spellSchool, amount) + DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800SPELL_EXTRA_ATTACKS|r %s: %s[%d] x%d", displayName(srcName, src), spellName or "", spellId, amount)) end -chatHandlers["SPELL_SUMMON"] = function(src, dst, spellId) - DEFAULT_CHAT_FRAME:AddMessage(format("|cff88ff88SPELL_SUMMON|r %s summoned %s: spell %d", src, dst, spellId)) + +-- Summon/resurrect/instakill +chatHandlers["SPELL_SUMMON"] = function(src, srcName, dst, dstName, spellId, spellName) + DEFAULT_CHAT_FRAME:AddMessage(format("|cff88ff88SPELL_SUMMON|r %s summoned %s: %s[%d]", displayName(srcName, src), displayName(dstName, dst), spellName or "", 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)) +chatHandlers["SPELL_RESURRECT"] = function(src, srcName, dst, dstName, spellId, spellName) + DEFAULT_CHAT_FRAME:AddMessage(format("|cff88ff88SPELL_RESURRECT|r %s resurrected %s: %s[%d]", displayName(srcName, src), displayName(dstName, dst), spellName or "", 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)) +chatHandlers["SPELL_INSTAKILL"] = function(src, srcName, dst, dstName, spellId, spellName) + DEFAULT_CHAT_FRAME:AddMessage(format("|cffff0000SPELL_INSTAKILL|r %s killed %s: %s[%d]", displayName(srcName, src), displayName(dstName, dst), spellName or "", spellId)) end -chatHandlers["UNIT_DIED"] = function(src, dst) - DEFAULT_CHAT_FRAME:AddMessage(format("|cff888888UNIT_DIED|r %s", dst)) + +-- Deaths +chatHandlers["UNIT_DIED"] = function(src, srcName, dst, dstName) + DEFAULT_CHAT_FRAME:AddMessage(format("|cff888888UNIT_DIED|r %s", displayName(dstName, dst))) end -chatHandlers["PARTY_KILL"] = function(src, dst) - DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800PARTY_KILL|r %s killed %s", src, dst)) +chatHandlers["PARTY_KILL"] = function(src, srcName, dst, dstName) + DEFAULT_CHAT_FRAME:AddMessage(format("|cffff8800PARTY_KILL|r %s killed %s", displayName(srcName, src), displayName(dstName, 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 -chatHandlers["DAMAGE_SPLIT"] = function(src, dst, spellId, amount, school) - DEFAULT_CHAT_FRAME:AddMessage(format("|cffff6600DAMAGE_SPLIT|r %s -> %s: spell %d for %d %s", src, dst, spellId, amount, schoolName(school))) -end -chatHandlers["SPELL_DISPEL_FAILED"] = function(src, dst, spellId, spellSchool) - DEFAULT_CHAT_FRAME:AddMessage(format("|cff888888SPELL_DISPEL_FAILED|r %s -> %s: spell %d resisted", src, dst, spellId)) -end -chatHandlers["UNIT_DESTROYED"] = function(src, dst) - DEFAULT_CHAT_FRAME:AddMessage(format("|cff666666UNIT_DESTROYED|r %s", dst)) +chatHandlers["UNIT_DESTROYED"] = function(src, srcName, dst, dstName) + DEFAULT_CHAT_FRAME:AddMessage(format("|cff666666UNIT_DESTROYED|r %s", displayName(dstName, dst))) end -- ============================================================================ --- UI — Draggable popup tracker +-- UI -- Draggable popup tracker -- ============================================================================ local ROW_HEIGHT = 14 local FRAME_WIDTH = 420 local VISIBLE_ROWS = 25 -local FRAME_HEIGHT = ROW_HEIGHT * VISIBLE_ROWS + 52 -- title + counter + padding +local FRAME_HEIGHT = ROW_HEIGHT * VISIBLE_ROWS + 52 local main = CreateFrame("Frame", "DPSLogTracker", UIParent) main:SetWidth(FRAME_WIDTH) @@ -419,18 +495,15 @@ 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) @@ -440,29 +513,24 @@ local function updateCounter() 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 @@ -475,18 +543,18 @@ local function updateRows() local row = rows[i] if idx <= NUM_SUBEVENTS then row:Show() - local displayName = SUBEVENTS[idx][1] + local displayN = SUBEVENTS[idx][1] if seen[idx] then row.check:SetText("|cff00ff00x|r") local argStr = seenArgs[idx] if argStr and argStr ~= "" then - row.nameStr:SetText("|cffffffff" .. displayName .. "|r |cffaaaaaa" .. argStr .. "|r") + row.nameStr:SetText("|cffffffff" .. displayN .. "|r |cffaaaaaa" .. argStr .. "|r") else - row.nameStr:SetText("|cffffffff" .. displayName .. "|r") + row.nameStr:SetText("|cffffffff" .. displayN .. "|r") end else row.check:SetText("|cff666666-|r") - row.nameStr:SetText("|cff666666" .. displayName .. "|r") + row.nameStr:SetText("|cff666666" .. displayN .. "|r") end else row:Hide() @@ -501,7 +569,6 @@ end) updateRows() --- Reset button local resetBtn = CreateFrame("Button", nil, main, "UIPanelButtonTemplate") resetBtn:SetWidth(60) resetBtn:SetHeight(18) @@ -522,27 +589,17 @@ end) -- Event dispatcher -- ============================================================================ -local function print(msg) - DEFAULT_CHAT_FRAME:AddMessage(msg) -end - +-- Expand args table to handle up to 17 args (spell damage has the most: 5 base + 3 prefix + 9 suffix) 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 - -- Collect args into a table for variant field lookup - local args = { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10 } + local args = { arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, + arg11, arg12, arg13, arg14, arg15, arg16, arg17 } - -- Check all variant entries for this subevent local indices = subeventIndices[subevent] if not indices then return end @@ -555,18 +612,7 @@ eventFrame:SetScript("OnEvent", function() local matches = true if varField then - -- Check specific arg matches variant value - -- For swing flags (bitfield), check if the bit is set - if subevent == "SWING_DAMAGE" and varField == 10 then - -- flags is arg10, check bitfield - local flags = args[varField] or 0 - if varValue == 1 then matches = (math.mod(flags, 2) == 1) -- glancing - elseif varValue == 2 then matches = (math.mod(math.floor(flags / 2), 2) == 1) -- crushing - elseif varValue == 4 then matches = (math.mod(math.floor(flags / 4), 2) == 1) -- offhand - else matches = false end - else - matches = (args[varField] == varValue) - end + matches = (args[varField] == varValue) end if matches then @@ -574,16 +620,18 @@ eventFrame:SetScript("OnEvent", function() seenCount = seenCount + 1 local formatter = argFormatters[subevent] if formatter then - seenArgs[idx] = formatter(arg4, arg5, arg6, arg7, arg8, arg9, arg10) + -- Pass args starting from arg6 (after base: sub, srcGUID, srcName, dstGUID, dstName) + seenArgs[idx] = formatter(arg6, arg7, arg8, arg9, arg10, arg11, arg12, + arg13, arg14, arg15, arg16, arg17) else seenArgs[idx] = "" end any_new = true - -- Chat output (first occurrence of this variant) local chatHandler = chatHandlers[subevent] if chatHandler then - chatHandler(arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10) + chatHandler(arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, + arg11, arg12, arg13, arg14, arg15, arg16, arg17) end end end @@ -608,4 +656,4 @@ SlashCmdList["DPSLOG"] = function(msg) end end -DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00DPSLog Tracker|r loaded — /dpslog to toggle window") +DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00DPSLog Tracker|r loaded -- /dpslog to toggle window") diff --git a/src/dpslog/dpslog.zig b/src/dpslog/dpslog.zig index 3f031f1..47c79c8 100644 --- a/src/dpslog/dpslog.zig +++ b/src/dpslog/dpslog.zig @@ -7,31 +7,28 @@ //! See reference/COMBAT_LOG_EVENT_WOTLK.md for full WotLK spec. //! See RESEARCH.md "Implementation Status" for what's implemented vs TODO. //! -//! Arg layouts per suffix type: +//! WotLK CLEU parity — see WOTLK_CLEU_SPEC.md for full spec. +//! Base: arg1=subevent, arg2=sourceGUID, arg3=sourceName, arg4=destGUID, arg5=destName +//! Spell prefix: spellId, spellName, spellSchool +//! Env prefix: envType(string) +//! Swing prefix: (none) //! -//! _DAMAGE (spell prefix): arg4=spellId, arg5=amount, arg6=school, -//! arg7=resisted, arg8=absorbed, arg9=blocked, arg10=critical -//! _DAMAGE (swing): arg4=amount, arg5=school, arg6=resisted, -//! arg7=absorbed, arg8=blocked, arg9=critical, arg10=0 -//! _DAMAGE (environmental): arg4=envType, arg5=amount, arg6=absorbed, -//! arg7-10=0 -//! _MISSED (spell prefix): arg4=spellId, arg5=spellSchool, arg6=missType(string) -//! _MISSED (swing): arg4=missType(string) -//! _HEAL (spell prefix): arg4=spellId, arg5=amount, arg6=0(overheal), -//! arg7=0(absorbed), arg8=critical -//! _ENERGIZE: arg4=spellId, arg5=amount, arg6=powerType -//! _DRAIN/_LEECH: arg4=spellId, arg5=amount, arg6=powerType, -//! arg7=extraAmount -//! _AURA_APPLIED/REMOVED: arg4=spellId, arg5=spellSchool, arg6=auraType(string) -//! _AURA_APPLIED_DOSE/REMOVED_DOSE: same + arg7=amount(stacks) -//! _CAST_START/SUCCESS: arg4=spellId, arg5=spellSchool -//! _CAST_FAILED: arg4=spellId, arg5=spellSchool, arg6=failedType(string) -//! _INTERRUPT: arg4=spellId, arg5=spellSchool, arg6=extraSpellId, -//! arg7=extraSpellSchool -//! _DISPEL/_STOLEN: arg4=spellId, arg5=spellSchool, arg6=extraSpellId, -//! arg7=extraSpellSchool, arg8=auraType(string) -//! _EXTRA_ATTACKS: arg4=spellId, arg5=spellSchool, arg6=amount -//! UNIT_DIED/PARTY_KILL: (base params only) +//! _DAMAGE: amount, overkill(-1), school, resisted, blocked, absorbed, +//! critical, glancing, crushing +//! _MISSED: missType(string), amountMissed +//! _HEAL: amount, overheal, absorbed, critical +//! _ENERGIZE: amount, powerType +//! _LEECH/_DRAIN: amount, powerType(-2=health), extraAmount +//! _EXTRA_ATTACKS: amount +//! _AURA_APPLIED/REMOVED: auraType(string) +//! _AURA_DOSE: auraType(string), amount(stacks) +//! _CAST_START/SUCCESS/INSTAKILL/SUMMON/RESURRECT: (prefix only) +//! _CAST_FAILED: failedType(string) +//! _INTERRUPT/_DISPEL_FAILED: extraSpellId, extraSpellName, extraSchool +//! _DISPEL/_STOLEN/_AURA_BROKEN_SPELL: extraSpellId, extraSpellName, +//! extraSchool, auraType(string) +//! _AURA_BROKEN: auraType(string) +//! UNIT_DIED/PARTY_KILL/UNIT_DESTROYED: (base only) //! //! Power types: 0=mana, 1=rage, 2=focus, 3=energy, 4=combo points //! Env types: 0=EXHAUSTED, 1=DROWNING, 2=FALLING, 3=LAVA, 4=SLIME, 5=FIRE @@ -42,6 +39,7 @@ const std = @import("std"); const hook = @import("zhook"); const logging = @import("../logging.zig"); +const lua = @import("../lua.zig"); const mod_mutex = @import("../mutex.zig"); const wow = @import("../wow.zig"); @@ -203,6 +201,10 @@ const HITINFO_GLANCING: u32 = 0x4000; // Unit power descriptor offsets (from m_data at obj+0x08) // ============================================================================= +/// UNIT_FIELD_HEALTH at absolute index 0x16. Offset = 0x58. +const DESC_HEALTH: u32 = 0x16 * 4; // 0x58 +/// UNIT_FIELD_MAXHEALTH at absolute index 0x1C. Offset = 0x70. +const DESC_MAXHEALTH: u32 = 0x1C * 4; // 0x70 /// UNIT_FIELD_POWER1 at absolute index 0x17. Power(N) = 0x5C + N*4. const DESC_POWER_BASE: u32 = 0x17 * 4; // 0x5C /// UNIT_FIELD_MAXPOWER1 at absolute index 0x1D. MaxPower(N) = 0x74 + N*4. @@ -304,55 +306,104 @@ fn guidToString(guid: u64) [*:0]const u8 { const SIGNAL: u32 = 0x703F50; -/// Pattern A: 3 strings + 7 numbers — _DAMAGE, _HEAL, _ENERGIZE, etc. -/// subevent, srcGUID, dstGUID, n1..n7 -fn fireCombatLog(sub: [*:0]const u8, src: [*:0]const u8, dst: [*:0]const u8, n1: u32, n2: u32, n3: u32, n4: u32, n5: u32, n6: u32, n7: u32) void { - const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, u32, u32, u32, u32, u32, u32) callconv(hook.cc.cdecl) void; - @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%d%d%d%d%d%d%d", sub, src, dst, n1, n2, n3, n4, n5, n6, n7 }); +// ============================================================================= +// Fire functions — WotLK CLEU parity (see WOTLK_CLEU_SPEC.md) +// +// Base args: sub(s), srcGUID(s), srcName(s), dstGUID(s), dstName(s) +// Spell prefix: spellId(d), spellName(s), spellSchool(d) +// Env prefix: envType(s) +// Swing prefix: (none) +// ============================================================================= + +/// Spell prefix + _DAMAGE suffix (9 fields: amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing) +/// Used by: SPELL_DAMAGE, RANGE_DAMAGE, SPELL_PERIODIC_DAMAGE, DAMAGE_SHIELD, DAMAGE_SPLIT +fn fireSpellDamage(sub: [*:0]const u8, src: [*:0]const u8, src_name: [*:0]const u8, dst: [*:0]const u8, dst_name: [*:0]const u8, id: u32, name: [*:0]const u8, school: u32, amount: u32, overkill: u32, dmg_school: u32, resisted: u32, blocked: u32, absorbed: u32, critical: u32, glancing: u32, crushing: u32) void { + const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, [*:0]const u8, u32, u32, u32, u32, u32, u32, u32, u32, u32, u32) callconv(hook.cc.cdecl) void; + @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%s%d%s%d%d%d%d%d%d%d%d%d%d", sub, src, src_name, dst, dst_name, id, name, school, amount, overkill, dmg_school, resisted, blocked, absorbed, critical, glancing, crushing }); } -/// Pattern B: 4 strings — SWING_MISSED (subevent, src, dst, missType) -fn fireSwingMissed(src: [*:0]const u8, dst: [*:0]const u8, miss_type: [*:0]const u8) void { - const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8) callconv(hook.cc.cdecl) void; - @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s", SUB_SWING_MISSED, src, dst, miss_type }); +/// Swing _DAMAGE suffix (no prefix, 9 fields) +fn fireSwingDamage(src: [*:0]const u8, src_name: [*:0]const u8, dst: [*:0]const u8, dst_name: [*:0]const u8, amount: u32, overkill: u32, school: u32, resisted: u32, blocked: u32, absorbed: u32, critical: u32, glancing: u32, crushing: u32) void { + const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, u32, u32, u32, u32, u32, u32, u32, u32) callconv(hook.cc.cdecl) void; + @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%s%d%d%d%d%d%d%d%d%d", SUB_SWING_DAMAGE, src, src_name, dst, dst_name, amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing }); } -/// Pattern C: 3 strings + 2 numbers + 1 string — SPELL_MISSED, SPELL_AURA_*, SPELL_CAST_FAILED -/// subevent, srcGUID, dstGUID, spellId, spellSchool, stringArg -fn fireSpellStr(sub: [*:0]const u8, src: [*:0]const u8, dst: [*:0]const u8, spell_id: u32, spell_school: u32, str_arg: [*:0]const u8) void { - const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, u32, [*:0]const u8) callconv(hook.cc.cdecl) void; - @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%d%d%s", sub, src, dst, spell_id, spell_school, str_arg }); +/// Environmental prefix + _DAMAGE suffix (envType string + 9 fields) +fn fireEnvDamage(dst: [*:0]const u8, dst_name: [*:0]const u8, env_str: [*:0]const u8, amount: u32, overkill: u32, school: u32, resisted: u32, blocked: u32, absorbed: u32, critical: u32, glancing: u32, crushing: u32) void { + const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, u32, u32, u32, u32, u32, u32, u32, u32) callconv(hook.cc.cdecl) void; + @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%s%s%d%d%d%d%d%d%d%d%d", SUB_ENV_DAMAGE, GUID_ZERO, "", dst, dst_name, env_str, amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing }); } -/// Pattern D: 3 strings + 2 numbers — SPELL_CAST_START/SUCCESS, SPELL_SUMMON, etc. -/// subevent, srcGUID, dstGUID, spellId, spellSchool -fn fireSpell(sub: [*:0]const u8, src: [*:0]const u8, dst: [*:0]const u8, spell_id: u32, spell_school: u32) void { - const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, u32) callconv(hook.cc.cdecl) void; - @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%d%d", sub, src, dst, spell_id, spell_school }); +/// Swing _MISSED suffix (missType string + amountMissed) +fn fireSwingMissed(src: [*:0]const u8, src_name: [*:0]const u8, dst: [*:0]const u8, dst_name: [*:0]const u8, miss_type: [*:0]const u8, amount_missed: u32) void { + const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32) callconv(hook.cc.cdecl) void; + @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%s%s%d", SUB_SWING_MISSED, src, src_name, dst, dst_name, miss_type, amount_missed }); } -/// Pattern E: 3 strings only — UNIT_DIED, PARTY_KILL -fn fireBase(sub: [*:0]const u8, src: [*:0]const u8, dst: [*:0]const u8) void { - const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8) callconv(hook.cc.cdecl) void; - @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s", sub, src, dst }); +/// Spell prefix + _MISSED suffix (missType string + amountMissed) +fn fireSpellMissed(sub: [*:0]const u8, src: [*:0]const u8, src_name: [*:0]const u8, dst: [*:0]const u8, dst_name: [*:0]const u8, id: u32, name: [*:0]const u8, school: u32, miss_type: [*:0]const u8, amount_missed: u32) void { + const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, [*:0]const u8, u32, [*:0]const u8, u32) callconv(hook.cc.cdecl) void; + @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%s%d%s%d%s%d", sub, src, src_name, dst, dst_name, id, name, school, miss_type, amount_missed }); } -/// Pattern F: 3s + 2d + 1s + 1d — SPELL_AURA_APPLIED_DOSE/REMOVED_DOSE -fn fireSpellStrD(sub: [*:0]const u8, src: [*:0]const u8, dst: [*:0]const u8, spell_id: u32, spell_school: u32, str_arg: [*:0]const u8, amount: u32) void { - const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, u32, [*:0]const u8, u32) callconv(hook.cc.cdecl) void; - @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%d%d%s%d", sub, src, dst, spell_id, spell_school, str_arg, amount }); +/// Spell prefix + _HEAL suffix (amount, overheal, absorbed, critical) +fn fireSpellHeal(sub: [*:0]const u8, src: [*:0]const u8, src_name: [*:0]const u8, dst: [*:0]const u8, dst_name: [*:0]const u8, id: u32, name: [*:0]const u8, school: u32, amount: u32, overheal: u32, absorbed: u32, critical: u32) void { + const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, [*:0]const u8, u32, u32, u32, u32, u32) callconv(hook.cc.cdecl) void; + @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%s%d%s%d%d%d%d%d", sub, src, src_name, dst, dst_name, id, name, school, amount, overheal, absorbed, critical }); } -/// Pattern G: 3s + 4d + 1s — SPELL_DISPEL/STOLEN (spellId, spellSchool, extraSpellId, extraSchool, auraType) -fn fireSpellDispel(sub: [*:0]const u8, src: [*:0]const u8, dst: [*:0]const u8, spell_id: u32, spell_school: u32, extra_id: u32, extra_school: u32, aura_type: [*:0]const u8) void { - const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, u32, u32, u32, [*:0]const u8) callconv(hook.cc.cdecl) void; - @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%d%d%d%d%s", sub, src, dst, spell_id, spell_school, extra_id, extra_school, aura_type }); +/// Spell prefix + _ENERGIZE suffix (amount, powerType) +fn fireSpellEnergize(sub: [*:0]const u8, src: [*:0]const u8, src_name: [*:0]const u8, dst: [*:0]const u8, dst_name: [*:0]const u8, id: u32, name: [*:0]const u8, school: u32, amount: u32, power_type: u32) void { + const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, [*:0]const u8, u32, u32, u32) callconv(hook.cc.cdecl) void; + @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%s%d%s%d%d%d", sub, src, src_name, dst, dst_name, id, name, school, amount, power_type }); } -/// Pattern H: 4 strings + 6 numbers — ENVIRONMENTAL_DAMAGE (envType as string) -fn fireEnvDamage(dst: [*:0]const u8, env_str: [*:0]const u8, amount: u32, school: u32, absorb: u32) void { - const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, u32, u32, u32, u32) callconv(hook.cc.cdecl) void; - @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%d%d%d%d%d", SUB_ENV_DAMAGE, GUID_ZERO, dst, env_str, amount, school, absorb, 0, 0 }); +/// Spell prefix + _LEECH/_DRAIN suffix (amount, powerType, extraAmount) +fn fireSpellLeech(sub: [*:0]const u8, src: [*:0]const u8, src_name: [*:0]const u8, dst: [*:0]const u8, dst_name: [*:0]const u8, id: u32, name: [*:0]const u8, school: u32, amount: u32, power_type: u32, extra_amount: u32) void { + const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, [*:0]const u8, u32, u32, u32, u32) callconv(hook.cc.cdecl) void; + @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%s%d%s%d%d%d%d", sub, src, src_name, dst, dst_name, id, name, school, amount, power_type, extra_amount }); +} + +/// Spell prefix + _EXTRA_ATTACKS suffix (amount) +fn fireSpellExtraAttacks(src: [*:0]const u8, src_name: [*:0]const u8, dst: [*:0]const u8, dst_name: [*:0]const u8, id: u32, name: [*:0]const u8, school: u32, amount: u32) void { + const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, [*:0]const u8, u32, u32) callconv(hook.cc.cdecl) void; + @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%s%d%s%d%d", SUB_SPELL_EXTRA_ATTACKS, src, src_name, dst, dst_name, id, name, school, amount }); +} + +/// Spell prefix + string suffix (_AURA_APPLIED/REMOVED, _CAST_FAILED) +fn fireSpellStr(sub: [*:0]const u8, src: [*:0]const u8, src_name: [*:0]const u8, dst: [*:0]const u8, dst_name: [*:0]const u8, id: u32, name: [*:0]const u8, school: u32, str_arg: [*:0]const u8) void { + const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, [*:0]const u8, u32, [*:0]const u8) callconv(hook.cc.cdecl) void; + @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%s%d%s%d%s", sub, src, src_name, dst, dst_name, id, name, school, str_arg }); +} + +/// Spell prefix + string + number suffix (_AURA_APPLIED_DOSE/REMOVED_DOSE) +fn fireSpellStrD(sub: [*:0]const u8, src: [*:0]const u8, src_name: [*:0]const u8, dst: [*:0]const u8, dst_name: [*:0]const u8, id: u32, name: [*:0]const u8, school: u32, str_arg: [*:0]const u8, amount: u32) void { + const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, [*:0]const u8, u32, [*:0]const u8, u32) callconv(hook.cc.cdecl) void; + @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%s%d%s%d%s%d", sub, src, src_name, dst, dst_name, id, name, school, str_arg, amount }); +} + +/// Spell prefix only (_CAST_START/SUCCESS, _INSTAKILL, _SUMMON, _RESURRECT) +fn fireSpell(sub: [*:0]const u8, src: [*:0]const u8, src_name: [*:0]const u8, dst: [*:0]const u8, dst_name: [*:0]const u8, id: u32, name: [*:0]const u8, school: u32) void { + const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, [*:0]const u8, u32) callconv(hook.cc.cdecl) void; + @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%s%d%s%d", sub, src, src_name, dst, dst_name, id, name, school }); +} + +/// Spell prefix + _INTERRUPT/_DISPEL_FAILED suffix (extraSpellId, extraSpellName, extraSchool) +fn fireSpellInterrupt(sub: [*:0]const u8, src: [*:0]const u8, src_name: [*:0]const u8, dst: [*:0]const u8, dst_name: [*:0]const u8, id: u32, name: [*:0]const u8, school: u32, extra_id: u32, extra_name: [*:0]const u8, extra_school: u32) void { + const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, [*:0]const u8, u32, u32, [*:0]const u8, u32) callconv(hook.cc.cdecl) void; + @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%s%d%s%d%d%s%d", sub, src, src_name, dst, dst_name, id, name, school, extra_id, extra_name, extra_school }); +} + +/// Spell prefix + _DISPEL/_STOLEN/_AURA_BROKEN_SPELL suffix (extraSpellId, extraSpellName, extraSchool, auraType) +fn fireSpellDispel(sub: [*:0]const u8, src: [*:0]const u8, src_name: [*:0]const u8, dst: [*:0]const u8, dst_name: [*:0]const u8, id: u32, name: [*:0]const u8, school: u32, extra_id: u32, extra_name: [*:0]const u8, extra_school: u32, aura_type: [*:0]const u8) void { + const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, u32, [*:0]const u8, u32, u32, [*:0]const u8, u32, [*:0]const u8) callconv(hook.cc.cdecl) void; + @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%s%d%s%d%d%s%d%s", sub, src, src_name, dst, dst_name, id, name, school, extra_id, extra_name, extra_school, aura_type }); +} + +/// Base only — UNIT_DIED, PARTY_KILL, UNIT_DESTROYED +fn fireBase(sub: [*:0]const u8, src: [*:0]const u8, src_name: [*:0]const u8, dst: [*:0]const u8, dst_name: [*:0]const u8) void { + const F = fn (u32, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8, [*:0]const u8) callconv(hook.cc.cdecl) void; + @call(.auto, @as(*const F, @ptrFromInt(SIGNAL)), .{ g_event_combat_log, "%s%s%s%s%s", sub, src, src_name, dst, dst_name }); } // ============================================================================= @@ -463,20 +514,38 @@ fn spellNonMeleeDmgLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callcon { const src_str = guidToString(caster_guid.?); const dst_str = guidToString(target_guid.?); + const src_name = wow.getNameByGUID(caster_guid.?); + const dst_name = wow.getNameByGUID(target_guid.?); const critical: u32 = if (hit_info.? & 0x02 != 0) 1 else 0; // Classify the damage subevent - const sub = if (spell_id.? == 75 or spell_id.? == 5019) - SUB_RANGE_DAMAGE // Auto Shot / Wand Shoot - else if (isDamageSplitSpell(spell_id.?)) - SUB_DAMAGE_SPLIT // Soul Link, Blessing of Sacrifice, etc. - else - SUB_SPELL_DAMAGE; - log.fmt("{s}: spell={d} amt={d} school={d} crit={d}\n", .{ - std.mem.span(sub), spell_id.?, damage.?, @as(u32, school.?), critical, - }); - fireCombatLog(sub, src_str, dst_str, spell_id.?, damage.?, @as(u32, school.?), @bitCast(resist.?), absorb.?, blocked.?, critical); - recordDamage(target_guid.?, caster_guid.?, spell_id.?); + if (isPeriodicLeechSpell(spell_id.?)) { + // Vanilla routes periodic leech (Drain Life, Siphon Life) through the spell damage + // packet instead of PERIODICAURALOG. Reclassify as SPELL_PERIODIC_LEECH. + // WotLK _LEECH suffix: amount, powerType, extraAmount + // For health leech: powerType = -2 (0xFFFFFFFE, WotLK convention for health) + // Approximate gained: damage - absorb - resist (heal is suppressed in heal hook) + const effective: u32 = if (damage.? > absorb.?) damage.? - absorb.? else 0; + const resist_u: u32 = @bitCast(resist.?); + const gained: u32 = if (effective > resist_u) effective - resist_u else 0; + log.fmt("SPELL_PERIODIC_LEECH: [{d}]{s} amt={d} gained={d}\n", .{ spell_id.?, std.mem.span(getSpellName(spell_id.?)), damage.?, gained }); + // _LEECH: spellId, amount, powerType(-2=health), extraAmount(gained) + fireSpellLeech(SUB_SPELL_PERIODIC_LEECH, src_str, src_name, dst_str, dst_name, spell_id.?, getSpellName(spell_id.?), getSpellSchool(spell_id.?), damage.?, @bitCast(@as(i32, -2)), gained); + recordDamage(target_guid.?, caster_guid.?, spell_id.?); + } else { + const sub = if (spell_id.? == 75 or spell_id.? == 5019) + SUB_RANGE_DAMAGE // Auto Shot / Wand Shoot + else if (isDamageSplitSpell(spell_id.?)) + SUB_DAMAGE_SPLIT // Soul Link, Blessing of Sacrifice, etc. + else + SUB_SPELL_DAMAGE; + const overkill = computeOverkill(target_guid.?, damage.?); + log.fmt("{s}: [{d}]{s} amt={d} school={d} crit={d}\n", .{ + std.mem.span(sub), spell_id.?, std.mem.span(getSpellName(spell_id.?)), damage.?, @as(u32, school.?), critical, + }); + fireSpellDamage(sub, src_str, src_name, dst_str, dst_name, spell_id.?, getSpellName(spell_id.?), getSpellSchool(spell_id.?), damage.?, overkill, @as(u32, school.?), @bitCast(resist.?), blocked.?, absorb.?, critical, 0, 0); + recordDamage(target_guid.?, caster_guid.?, spell_id.?); + } } return spell_dmg_hook.callOriginal(.{ unk, opcode, unk2, cds }); @@ -504,6 +573,8 @@ fn periodicAuraLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(ho if (target_guid != null and caster_guid != null and spell_id != null and count != null) { const src_str = guidToString(caster_guid.?); const dst_str = guidToString(target_guid.?); + const src_name = wow.getNameByGUID(caster_guid.?); + const dst_name = wow.getNameByGUID(target_guid.?); const aura_type = cdsGet(u32, cds); if (aura_type) |at| { @@ -514,50 +585,64 @@ fn periodicAuraLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(ho const spell_school = cdsGet(u32, cds) orelse 0; const absorb = cdsGet(u32, cds) orelse 0; const resist = cdsGet(i32, cds) orelse 0; - fireCombatLog(SUB_SPELL_PERIODIC_DAMAGE, src_str, dst_str, spell_id.?, amount, spell_school, @bitCast(resist), absorb, 0, 0); + const overkill = computeOverkill(target_guid.?, amount); + fireSpellDamage(SUB_SPELL_PERIODIC_DAMAGE, src_str, src_name, dst_str, dst_name, spell_id.?, getSpellName(spell_id.?), spell_school, amount, overkill, spell_school, @bitCast(resist), 0, absorb, 0, 0, 0); recordDamage(target_guid.?, caster_guid.?, spell_id.?); }, 8, 20 => { // SPELL_PERIODIC_HEAL const amount = cdsGet(u32, cds) orelse 0; - fireCombatLog(SUB_SPELL_PERIODIC_HEAL, src_str, dst_str, spell_id.?, amount, 0, 0, 0, 0, 0); + const overheal = computeOverheal(target_guid.?, amount); + fireSpellHeal(SUB_SPELL_PERIODIC_HEAL, src_str, src_name, dst_str, dst_name, spell_id.?, getSpellName(spell_id.?), getSpellSchool(spell_id.?), amount, overheal, 0, 0); }, 21, 24 => { // SPELL_PERIODIC_ENERGIZE const power_type = cdsGet(u32, cds) orelse 0; - const amount = cdsGet(u32, cds) orelse 0; - // Compute overEnergize from descriptors (before original processes the update) - var over_energize: u32 = 0; - if (target_guid != null) { - const cur = getUnitPower(target_guid.?, power_type) orelse 0; - const max = getUnitMaxPower(target_guid.?, power_type) orelse 0; - if (max > 0 and cur + amount > max) { - over_energize = cur + amount - max; - } - } - fireCombatLog(SUB_SPELL_PERIODIC_ENERGIZE, src_str, dst_str, spell_id.?, amount, over_energize, power_type, 0, 0, 0); + const raw_amount = cdsGet(u32, cds) orelse 0; + // Power display divisor table at 0x86F978 (verified from client binary): + // Mana(0)=1, Rage(1)=10, Focus(2)=1, Energy(3)=1, Happiness(4)=1000 + // SMSG_SPELLENERGIZELOG handler divides by this; PERIODICAURALOG does not. + const divisor: u32 = switch (power_type) { + 1 => 10, // rage + 4 => 1000, // happiness + else => 1, + }; + const amount = raw_amount / divisor; + fireSpellEnergize(SUB_SPELL_PERIODIC_ENERGIZE, src_str, src_name, dst_str, dst_name, spell_id.?, getSpellName(spell_id.?), getSpellSchool(spell_id.?), amount, power_type); }, 53 => { - // SPELL_PERIODIC_LEECH — health drain (Drain Life, Siphon Life) - // Packet format: damage, spellSchool, absorb, resist (same as periodic damage) + // SPELL_PERIODIC_LEECH — health leech (Drain Life, Siphon Life) + // NOTE: vanilla server routes health leech through SendSpellNonMeleeDamageLog, + // NOT PERIODICAURALOG. This case won't fire on vanilla servers — Drain Life + // ticks are reclassified in spellNonMeleeDmgLogDetour via isPeriodicLeechSpell(). + // Kept for protocol completeness. const amount = cdsGet(u32, cds) orelse 0; - const spell_school = cdsGet(u32, cds) orelse 0; + _ = cdsGet(u32, cds); // school const absorb = cdsGet(u32, cds) orelse 0; const resist = cdsGet(i32, cds) orelse 0; - // WotLK _LEECH suffix: amount, powerType(0=health), extraAmount(gained back) - // Vanilla doesn't send the gained amount separately — approximate as amount - absorb - resist const effective: u32 = if (amount > absorb) amount - absorb else 0; - const gained: u32 = if (effective > @as(u32, @bitCast(resist))) effective - @as(u32, @bitCast(resist)) else 0; - fireCombatLog(SUB_SPELL_PERIODIC_LEECH, src_str, dst_str, spell_id.?, amount, spell_school, gained, absorb, @bitCast(resist), 0); + const resist_u: u32 = @bitCast(resist); + const gained: u32 = if (effective > resist_u) effective - resist_u else 0; + // _LEECH: spellId, amount, powerType(-2=health), extraAmount(gained) + fireSpellLeech(SUB_SPELL_PERIODIC_LEECH, src_str, src_name, dst_str, dst_name, spell_id.?, getSpellName(spell_id.?), getSpellSchool(spell_id.?), amount, @bitCast(@as(i32, -2)), gained); recordDamage(target_guid.?, caster_guid.?, spell_id.?); }, 64 => { - // SPELL_PERIODIC_DRAIN — mana drain/leech (Mana Burn, Drain Mana) - // Packet format: powerType, amount, gainMultiplier + // SPELL_AURA_PERIODIC_MANA_LEECH — power leech/drain + // Packet format: powerType(u32), amount(u32), gainMultiplier(f32) + // multiplier > 0 = LEECH (caster gains amount*mult): Drain Mana, Viper Sting + // multiplier == 0 = DRAIN (caster gains nothing): rare boss mechanics const power_type = cdsGet(u32, cds) orelse 0; const amount = cdsGet(u32, cds) orelse 0; - const extra = cdsGet(u32, cds) orelse 0; // multiplied gain - fireCombatLog(SUB_SPELL_PERIODIC_DRAIN, src_str, dst_str, spell_id.?, amount, power_type, extra, 0, 0, 0); + const multiplier = cdsGet(f32, cds) orelse 0.0; + if (multiplier > 0.0) { + const gained: u32 = @intFromFloat(@as(f32, @floatFromInt(amount)) * multiplier); + log.fmt("SPELL_PERIODIC_LEECH: spell={d} amt={d} power={d} gained={d} mult={d}\n", .{ spell_id.?, amount, power_type, gained, @as(u32, @bitCast(multiplier)) }); + fireSpellLeech(SUB_SPELL_PERIODIC_LEECH, src_str, src_name, dst_str, dst_name, spell_id.?, getSpellName(spell_id.?), getSpellSchool(spell_id.?), amount, power_type, gained); + } else { + log.fmt("SPELL_PERIODIC_DRAIN: [{d}]{s} amt={d} power={d}\n", .{ spell_id.?, std.mem.span(getSpellName(spell_id.?)), amount, power_type }); + fireSpellLeech(SUB_SPELL_PERIODIC_DRAIN, src_str, src_name, dst_str, dst_name, spell_id.?, getSpellName(spell_id.?), getSpellSchool(spell_id.?), amount, power_type, 0); + } }, else => {}, } @@ -590,12 +675,16 @@ fn healDisplayDetour(caster_ptr: u32, target_ptr: u32, spell_id: u32, heal_amoun const caster_guid: u64 = @as(u64, caster_hi) << 32 | @as(u64, caster_lo); const target_guid: u64 = @as(u64, target_hi) << 32 | @as(u64, target_lo); - if (caster_guid != 0 and target_guid != 0) { + if (caster_guid != 0 and target_guid != 0 and !isPeriodicLeechSpell(spell_id)) { + // Suppress heal for periodic leech spells — already covered by SPELL_PERIODIC_LEECH const src_str = guidToString(caster_guid); const dst_str = guidToString(target_guid); + const src_name = wow.getNameByGUID(caster_guid); + const dst_name = wow.getNameByGUID(target_guid); const critical: u32 = if (is_crit != 0) 1 else 0; - // _HEAL: spellId, amount, overheal(0), absorbed(0), critical - fireCombatLog(SUB_SPELL_HEAL, src_str, dst_str, spell_id, heal_amount, 0, 0, critical, 0, 0); + const overheal = computeOverheal(target_guid, heal_amount); + // _HEAL: spellId, amount, overheal, absorbed(0), critical + fireSpellHeal(SUB_SPELL_HEAL, src_str, src_name, dst_str, dst_name, spell_id, getSpellName(spell_id), getSpellSchool(spell_id), heal_amount, overheal, 0, critical); } } @@ -666,6 +755,8 @@ fn parseMeleePacket(cds: u32) void { const src_str = guidToString(attacker_guid); const dst_str = guidToString(target_guid); + const src_name = wow.getNameByGUID(attacker_guid); + const dst_name = wow.getNameByGUID(target_guid); // Determine miss type from victimState const miss_type: ?[*:0]const u8 = switch (victim_state) { @@ -681,17 +772,15 @@ fn parseMeleePacket(cds: u32) void { if (miss_type) |mt| { log.fmt("SWING_MISSED: vs={d} type={s}\n", .{ victim_state, std.mem.span(mt) }); - fireSwingMissed(src_str, dst_str, mt); + fireSwingMissed(src_str, src_name, dst_str, dst_name, mt, 0); } else { const critical: u32 = if (hit_info & HITINFO_CRITICALHIT != 0) 1 else 0; - // Pack glancing/crushing/offhand into flags bitmask (arg10): - // bit0=glancing, bit1=crushing, bit2=offhand - const flags: u32 = (if (hit_info & HITINFO_GLANCING != 0) @as(u32, 1) else 0) | - (if (hit_info & HITINFO_CRUSHING != 0) @as(u32, 2) else 0) | - (if (hit_info & HITINFO_LEFTSWING != 0) @as(u32, 4) else 0); - log.fmt("SWING_DAMAGE: dmg={d} crit={d} flags={d} hitInfo=0x{x}\n", .{ total_damage, critical, flags, hit_info }); - // _DAMAGE: amount, school, resisted, absorbed, blocked, critical, flags(glancing|crushing|offhand) - fireCombatLog(SUB_SWING_DAMAGE, src_str, dst_str, total_damage, school, resist, absorb, blocked, critical, flags); + const glancing: u32 = if (hit_info & HITINFO_GLANCING != 0) 1 else 0; + const crushing: u32 = if (hit_info & HITINFO_CRUSHING != 0) 1 else 0; + const overkill = computeOverkill(target_guid, total_damage); + log.fmt("SWING_DAMAGE: dmg={d} crit={d} glance={d} crush={d} hitInfo=0x{x}\n", .{ total_damage, critical, glancing, crushing, hit_info }); + // _DAMAGE: amount, overkill, school, resisted, blocked, absorbed, critical, glancing, crushing + fireSwingDamage(src_str, src_name, dst_str, dst_name, total_damage, overkill, school, resist, blocked, absorb, critical, glancing, crushing); recordDamage(target_guid, attacker_guid, 0); } } @@ -715,6 +804,7 @@ fn envDamageDetour(victim_guid_ptr: u32, damage_type: u32, damage: u32, absorb: if (victim_guid != 0) { const dst_str = guidToString(victim_guid); + const dst_name = wow.getNameByGUID(victim_guid); const env_str = envTypeToString(damage_type); // Env school: falling=1(physical), fire/lava=4(fire), drowning/slime=8(nature), exhausted=1 const school: u32 = switch (damage_type) { @@ -722,8 +812,9 @@ fn envDamageDetour(victim_guid_ptr: u32, damage_type: u32, damage: u32, absorb: 3, 5 => 4, // lava, fire → fire else => 1, // exhausted, falling → physical }; + const overkill = computeOverkill(victim_guid, damage); log.fmt("ENVIRONMENTAL_DAMAGE: type={s} dmg={d} absorb={d}\n", .{ std.mem.span(env_str), damage, absorb }); - fireEnvDamage(dst_str, env_str, damage, school, absorb); + fireEnvDamage(dst_str, dst_name, env_str, damage, overkill, school, 0, 0, absorb, 0, 0, 0); recordDamage(victim_guid, 0, 0); } } @@ -780,6 +871,8 @@ fn spellMissedDetour(miss_type: u32, spell_id: u32, caster_lo: u32, caster_hi: u if (caster_guid != 0 and target_guid != 0 and spell_id != 0) { const src_str = guidToString(caster_guid); const dst_str = guidToString(target_guid); + const src_name = wow.getNameByGUID(caster_guid); + const dst_name = wow.getNameByGUID(target_guid); const miss_str = missInfoToString(@truncate(miss_type)); const school = getSpellSchool(spell_id); // Classify miss subevent by spell type @@ -791,8 +884,8 @@ fn spellMissedDetour(miss_type: u32, spell_id: u32, caster_lo: u32, caster_hi: u SUB_SPELL_PERIODIC_MISSED else SUB_SPELL_MISSED; - log.fmt("{s}: spell={d} miss={s} logmiss={d}\n", .{ std.mem.span(sub), spell_id, std.mem.span(miss_str), is_spell_log_miss }); - fireSpellStr(sub, src_str, dst_str, spell_id, school, miss_str); + log.fmt("{s}: [{d}]{s} miss={s} logmiss={d}\n", .{ std.mem.span(sub), spell_id, std.mem.span(getSpellName(spell_id)), std.mem.span(miss_str), is_spell_log_miss }); + fireSpellMissed(sub, src_str, src_name, dst_str, dst_name, spell_id, getSpellName(spell_id), school, miss_str, 0); } spell_missed_hook.callOriginal(.{ miss_type, spell_id, caster_lo, caster_hi, target_lo, target_hi, is_spell_log_miss }); @@ -823,15 +916,53 @@ fn damageShieldDetour(victim_ptr: u32, caster_ptr: u32, damage: u32, school: u32 if (victim_guid != 0 and caster_guid != 0) { const src_str = guidToString(caster_guid); const dst_str = guidToString(victim_guid); + const src_name = wow.getNameByGUID(caster_guid); + const dst_name = wow.getNameByGUID(victim_guid); + const overkill = computeOverkill(victim_guid, damage); log.fmt("DAMAGE_SHIELD: dmg={d} school={d}\n", .{ damage, school }); // No spellId available in SMSG_SPELLDAMAGESHIELD — pass 0 - fireCombatLog(SUB_DAMAGE_SHIELD, src_str, dst_str, 0, damage, school, 0, 0, 0, 0); + fireSpellDamage(SUB_DAMAGE_SHIELD, src_str, src_name, dst_str, dst_name, 0, getSpellName(0), school, damage, overkill, school, 0, 0, 0, 0, 0, 0); } } damage_shield_hook.callOriginal(.{ victim_ptr, caster_ptr, damage, school }); } +// ============================================================================= +// Hook: ProcessStandardPowerGainMessage (0x62CA00) — downstream of SMSG_SPELLENERGIZELOG +// __fastcall(ECX=casterGuid_ptr, EDX=targetGuid_ptr, stack: spellId, powerType, amount) +// RET 0xC (3 stack params) +// Fires: SPELL_ENERGIZE +// ============================================================================= + +const EnergizeFn = fn (u32, u32, u32, u32, u32) callconv(hook.cc.fastcall) void; + +var energize_hook: hook.Detour(EnergizeFn) = .{}; + +fn energizeDetour(caster_ptr: u32, target_ptr: u32, spell_id: u32, power_type: u32, amount: u32) callconv(hook.cc.fastcall) void { + asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true }); + + if (caster_ptr != 0 and target_ptr != 0 and spell_id != 0) { + const caster_lo = hook.readMem(u32, caster_ptr); + const caster_hi = hook.readMem(u32, caster_ptr + 4); + const target_lo = hook.readMem(u32, target_ptr); + const target_hi = hook.readMem(u32, target_ptr + 4); + const caster_guid: u64 = @as(u64, caster_hi) << 32 | @as(u64, caster_lo); + const target_guid: u64 = @as(u64, target_hi) << 32 | @as(u64, target_lo); + + if (caster_guid != 0 and target_guid != 0) { + const src_str = guidToString(caster_guid); + const dst_str = guidToString(target_guid); + const src_name = wow.getNameByGUID(caster_guid); + const dst_name = wow.getNameByGUID(target_guid); + log.fmt("SPELL_ENERGIZE: [{d}]{s} amt={d} power={d}\n", .{ spell_id, std.mem.span(getSpellName(spell_id)), amount, power_type }); + fireSpellEnergize(SUB_SPELL_ENERGIZE, src_str, src_name, dst_str, dst_name, spell_id, getSpellName(spell_id), getSpellSchool(spell_id), amount, power_type); + } + } + + energize_hook.callOriginal(.{ caster_ptr, target_ptr, spell_id, power_type, amount }); +} + // ============================================================================= // Hook: DisplaySpellInterruptMessage (0x626A10) — downstream of SMSG_SPELLLOGEXECUTE // __fastcall(ECX=casterGuid_ptr, EDX=targetGuid_ptr, stack: interruptedSpellId) @@ -857,10 +988,12 @@ fn spellInterruptDetour(caster_ptr: u32, target_ptr: u32, interrupted_spell_id: if (caster_guid != 0 and target_guid != 0 and interrupted_spell_id != 0) { const src_str = guidToString(caster_guid); const dst_str = guidToString(target_guid); + const src_name = wow.getNameByGUID(caster_guid); + const dst_name = wow.getNameByGUID(target_guid); const extra_school = getSpellSchool(interrupted_spell_id); log.fmt("SPELL_INTERRUPT: interrupted={d}\n", .{interrupted_spell_id}); // _INTERRUPT: spellId(interrupt ability — unknown, pass 0), spellSchool, extraSpellId(interrupted), extraSchool - fireSpellDispel(SUB_SPELL_INTERRUPT, src_str, dst_str, 0, 0, interrupted_spell_id, extra_school, ""); + fireSpellInterrupt(SUB_SPELL_INTERRUPT, src_str, src_name, dst_str, dst_name, 0, "", 0, interrupted_spell_id, getSpellName(interrupted_spell_id), extra_school); } } @@ -888,10 +1021,11 @@ fn instaKillDetour(caster_ptr: u32, spell_id: u32) callconv(hook.cc.fastcall) vo if (caster_guid != 0) { const src_str = guidToString(caster_guid); + const src_name = wow.getNameByGUID(caster_guid); const school = getSpellSchool(spell_id); - log.fmt("SPELL_INSTAKILL: spell={d} caster=0x{x}\n", .{ spell_id, caster_guid }); + log.fmt("SPELL_INSTAKILL: [{d}]{s} caster=0x{x}\n", .{ spell_id, std.mem.span(getSpellName(spell_id)), caster_guid }); // No victim GUID in SMSG_SPELLINSTAKILLLOG — pass zero GUID - fireSpell(SUB_SPELL_INSTAKILL, src_str, GUID_ZERO, spell_id, school); + fireSpell(SUB_SPELL_INSTAKILL, src_str, src_name, GUID_ZERO, "", spell_id, getSpellName(spell_id), school); } } @@ -919,8 +1053,10 @@ fn partyKillLogDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook. if (player_guid != null and victim_guid != null) { const src_str = guidToString(player_guid.?); const dst_str = guidToString(victim_guid.?); + const src_name = wow.getNameByGUID(player_guid.?); + const dst_name = wow.getNameByGUID(victim_guid.?); log.fmt("PARTY_KILL: src=0x{x} dst=0x{x}\n", .{ player_guid.?, victim_guid.? }); - fireBase(SUB_PARTY_KILL, src_str, dst_str); + fireBase(SUB_PARTY_KILL, src_str, src_name, dst_str, dst_name); } return party_kill_hook.callOriginal(.{ unk, opcode, unk2, cds }); @@ -950,13 +1086,14 @@ fn spellStartDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc if (caster_guid != null and spell_id != null) { const src_str = guidToString(caster_guid.?); + const src_name = wow.getNameByGUID(caster_guid.?); const school = getSpellSchool(spell_id.?); if (opcode == OPCODE_SPELL_START) { - log.fmt("SPELL_CAST_START: spell={d} caster=0x{x}\n", .{ spell_id.?, caster_guid.? }); - fireSpell(SUB_SPELL_CAST_START, src_str, GUID_ZERO, spell_id.?, school); + log.fmt("SPELL_CAST_START: [{d}]{s} caster=0x{x}\n", .{ spell_id.?, std.mem.span(getSpellName(spell_id.?)), caster_guid.? }); + fireSpell(SUB_SPELL_CAST_START, src_str, src_name, GUID_ZERO, "", spell_id.?, getSpellName(spell_id.?), school); } else { - log.fmt("SPELL_CAST_SUCCESS: spell={d} caster=0x{x}\n", .{ spell_id.?, caster_guid.? }); - fireSpell(SUB_SPELL_CAST_SUCCESS, src_str, GUID_ZERO, spell_id.?, school); + log.fmt("SPELL_CAST_SUCCESS: [{d}]{s} caster=0x{x}\n", .{ spell_id.?, std.mem.span(getSpellName(spell_id.?)), caster_guid.? }); + fireSpell(SUB_SPELL_CAST_SUCCESS, src_str, src_name, GUID_ZERO, "", spell_id.?, getSpellName(spell_id.?), school); // Heuristic SPELL_AURA_REFRESH for all units: // Parse SPELL_GO hit targets. If a hit target already has this aura @@ -974,9 +1111,10 @@ fn spellStartDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc if (target_guid != 0 and target_guid != local_guid) { if (unitHasAura(target_guid, spell_id.?)) |slot| { const dst_str = guidToString(target_guid); + const dst_name = wow.getNameByGUID(target_guid); const aura_type = getAuraType(spell_id.?, slot); - log.fmt("SPELL_AURA_REFRESH (heuristic): spell={d} target=0x{x}\n", .{ spell_id.?, target_guid }); - fireSpellStr(SUB_SPELL_AURA_REFRESH, src_str, dst_str, spell_id.?, school, aura_type); + log.fmt("SPELL_AURA_REFRESH (heuristic): [{d}]{s} target=0x{x}\n", .{ spell_id.?, std.mem.span(getSpellName(spell_id.?)), target_guid }); + fireSpellStr(SUB_SPELL_AURA_REFRESH, src_str, src_name, dst_str, dst_name, spell_id.?, getSpellName(spell_id.?), school, aura_type); } } } @@ -1013,9 +1151,10 @@ fn castResultDetour(unk: u32, opcode: u32, unk2: u32, cds: u32) callconv(hook.cc const player_guid = getActivePlayerGuid(); if (player_guid != 0) { const src_str = guidToString(player_guid); + const src_name = wow.getNameByGUID(player_guid); const school = getSpellSchool(spell_id.?); - log.fmt("SPELL_CAST_FAILED: spell={d} status={d} (self)\n", .{ spell_id.?, status.? }); - fireSpellStr(SUB_SPELL_CAST_FAILED, src_str, GUID_ZERO, spell_id.?, school, "FAILED"); + log.fmt("SPELL_CAST_FAILED: [{d}]{s} status={d} (self)\n", .{ spell_id.?, std.mem.span(getSpellName(spell_id.?)), status.? }); + fireSpellStr(SUB_SPELL_CAST_FAILED, src_str, src_name, GUID_ZERO, "", spell_id.?, getSpellName(spell_id.?), school, "FAILED"); } } @@ -1048,9 +1187,10 @@ fn spellFailedOtherDetour(msg_type: u32, cds: u32) callconv(hook.cc.stdcall) ?*a const player_guid = getActivePlayerGuid(); if (caster_guid.? != player_guid) { const src_str = guidToString(caster_guid.?); + const src_name = wow.getNameByGUID(caster_guid.?); const school = getSpellSchool(spell_id.?); - log.fmt("SPELL_CAST_FAILED: spell={d} caster=0x{x} (other)\n", .{ spell_id.?, caster_guid.? }); - fireSpellStr(SUB_SPELL_CAST_FAILED, src_str, GUID_ZERO, spell_id.?, school, "FAILED"); + log.fmt("SPELL_CAST_FAILED: [{d}]{s} caster=0x{x} (other)\n", .{ spell_id.?, std.mem.span(getSpellName(spell_id.?)), caster_guid.? }); + fireSpellStr(SUB_SPELL_CAST_FAILED, src_str, src_name, GUID_ZERO, "", spell_id.?, getSpellName(spell_id.?), school, "FAILED"); } } @@ -1081,6 +1221,7 @@ fn unitDeathDetour(unit_obj: u32) callconv(hook.cc.fastcall) void { if (guid != 0) { const dst_str = guidToString(guid); + const dst_name = wow.getNameByGUID(guid); // Check creature type: totems (type 11) fire UNIT_DESTROYED // CGUnit_GetCreatureType fallback path: obj+0xB30 -> +0x18 const is_totem = blk: { @@ -1092,10 +1233,10 @@ fn unitDeathDetour(unit_obj: u32) callconv(hook.cc.fastcall) void { }; if (is_totem) { log.fmt("UNIT_DESTROYED: unit=0x{x}\n", .{guid}); - fireBase(SUB_UNIT_DESTROYED, GUID_ZERO, dst_str); + fireBase(SUB_UNIT_DESTROYED, GUID_ZERO, "", dst_str, dst_name); } else { log.fmt("UNIT_DIED: unit=0x{x}\n", .{guid}); - fireBase(SUB_UNIT_DIED, GUID_ZERO, dst_str); + fireBase(SUB_UNIT_DIED, GUID_ZERO, "", dst_str, dst_name); } } } @@ -1128,6 +1269,7 @@ fn auraRemovedDetour(unit_obj: u32, _edx: u32, slot_index: u32, spell_id: u32) c if (guid != 0) { const dst_str = guidToString(guid); + const dst_name = wow.getNameByGUID(guid); const aura_type: [*:0]const u8 = getAuraType(spell_id, slot_index); const school = getSpellSchool(spell_id); @@ -1139,23 +1281,24 @@ fn auraRemovedDetour(unit_obj: u32, _edx: u32, slot_index: u32, spell_id: u32) c if (aura_int_flags & (AURA_INTERRUPT_FLAG_DAMAGE | AURA_INTERRUPT_FLAG_DIRECT_DAMAGE) != 0) { if (findRecentDamage(guid)) |dmg| { const src_str = if (dmg.source_guid != 0) guidToString(dmg.source_guid) else GUID_ZERO; + const breaker_name = if (dmg.source_guid != 0) wow.getNameByGUID(dmg.source_guid) else @as([*:0]const u8, ""); if (dmg.spell_id != 0) { // Spell broke the aura — WotLK arg order: broken aura as primary, breaker as extra const dmg_school = getSpellSchool(dmg.spell_id); - log.fmt("SPELL_AURA_BROKEN_SPELL: aura={d} by spell={d} src=0x{x}\n", .{ spell_id, dmg.spell_id, dmg.source_guid }); - fireSpellDispel(SUB_SPELL_AURA_BROKEN_SPELL, src_str, dst_str, spell_id, school, dmg.spell_id, dmg_school, aura_type); + log.fmt("SPELL_AURA_BROKEN_SPELL: aura=[{d}]{s} by [{d}]{s} src=0x{x}\n", .{ spell_id, std.mem.span(getSpellName(spell_id)), dmg.spell_id, std.mem.span(getSpellName(dmg.spell_id)), dmg.source_guid }); + fireSpellDispel(SUB_SPELL_AURA_BROKEN_SPELL, src_str, breaker_name, dst_str, dst_name, spell_id, getSpellName(spell_id), school, dmg.spell_id, getSpellName(dmg.spell_id), dmg_school, aura_type); } else { // Melee (swing) broke the aura — no breaking spell ID - log.fmt("SPELL_AURA_BROKEN: aura={d} by melee src=0x{x}\n", .{ spell_id, dmg.source_guid }); - fireSpellStr(SUB_SPELL_AURA_BROKEN, src_str, dst_str, spell_id, school, aura_type); + log.fmt("SPELL_AURA_BROKEN: aura=[{d}]{s} by melee src=0x{x}\n", .{ spell_id, std.mem.span(getSpellName(spell_id)), dmg.source_guid }); + fireSpellStr(SUB_SPELL_AURA_BROKEN, src_str, breaker_name, dst_str, dst_name, spell_id, getSpellName(spell_id), school, aura_type); } } } } // Always fire SPELL_AURA_REMOVED (even if broken — WotLK fires both) - log.fmt("SPELL_AURA_REMOVED: spell={d} slot={d} unit=0x{x}\n", .{ spell_id, slot_index, guid }); - fireSpellStr(SUB_SPELL_AURA_REMOVED, GUID_ZERO, dst_str, spell_id, school, aura_type); + log.fmt("SPELL_AURA_REMOVED: [{d}]{s} slot={d} unit=0x{x}\n", .{ spell_id, std.mem.span(getSpellName(spell_id)), slot_index, guid }); + fireSpellStr(SUB_SPELL_AURA_REMOVED, GUID_ZERO, "", dst_str, dst_name, spell_id, getSpellName(spell_id), school, aura_type); } } } @@ -1185,10 +1328,11 @@ fn auraAppliedDetour(unit_obj: u32, _edx: u32, slot_index: u32, spell_id: u32) c if (guid != 0) { const dst_str = guidToString(guid); + const dst_name = wow.getNameByGUID(guid); const aura_type: [*:0]const u8 = getAuraType(spell_id, slot_index); const school = getSpellSchool(spell_id); - log.fmt("SPELL_AURA_APPLIED: spell={d} slot={d} unit=0x{x}\n", .{ spell_id, slot_index, guid }); - fireSpellStr(SUB_SPELL_AURA_APPLIED, GUID_ZERO, dst_str, spell_id, school, aura_type); + log.fmt("SPELL_AURA_APPLIED: [{d}]{s} slot={d} unit=0x{x}\n", .{ spell_id, std.mem.span(getSpellName(spell_id)), slot_index, guid }); + fireSpellStr(SUB_SPELL_AURA_APPLIED, GUID_ZERO, "", dst_str, dst_name, spell_id, getSpellName(spell_id), school, aura_type); } } } @@ -1227,15 +1371,16 @@ fn auraDoseDetour(unit_obj: u32, _edx: u32, slot_index: u32, old_count_raw: u32) if (guid != 0) { const dst_str = guidToString(guid); + const dst_name = wow.getNameByGUID(guid); const aura_type: [*:0]const u8 = getAuraType(spell_id, slot_index); const school = getSpellSchool(spell_id); if (new_count > old_count) { - log.fmt("SPELL_AURA_APPLIED_DOSE: spell={d} slot={d} count={d}\n", .{ spell_id, slot_index, new_count }); - fireSpellStrD(SUB_SPELL_AURA_APPLIED_DOSE, GUID_ZERO, dst_str, spell_id, school, aura_type, new_count); + log.fmt("SPELL_AURA_APPLIED_DOSE: [{d}]{s} slot={d} count={d}\n", .{ spell_id, std.mem.span(getSpellName(spell_id)), slot_index, new_count }); + fireSpellStrD(SUB_SPELL_AURA_APPLIED_DOSE, GUID_ZERO, "", dst_str, dst_name, spell_id, getSpellName(spell_id), school, aura_type, new_count); } else { - log.fmt("SPELL_AURA_REMOVED_DOSE: spell={d} slot={d} count={d}\n", .{ spell_id, slot_index, new_count }); - fireSpellStrD(SUB_SPELL_AURA_REMOVED_DOSE, GUID_ZERO, dst_str, spell_id, school, aura_type, new_count); + log.fmt("SPELL_AURA_REMOVED_DOSE: [{d}]{s} slot={d} count={d}\n", .{ spell_id, std.mem.span(getSpellName(spell_id)), slot_index, new_count }); + fireSpellStrD(SUB_SPELL_AURA_REMOVED_DOSE, GUID_ZERO, "", dst_str, dst_name, spell_id, getSpellName(spell_id), school, aura_type, new_count); } } } @@ -1268,10 +1413,11 @@ fn extraAttacksDetour(caster_ptr: u32, count: u32, spell_id: u32) callconv(hook. if (caster_guid != 0) { const src_str = guidToString(caster_guid); + const src_name = wow.getNameByGUID(caster_guid); const school = getSpellSchool(spell_id); - log.fmt("SPELL_EXTRA_ATTACKS: spell={d} count={d}\n", .{ spell_id, count }); + log.fmt("SPELL_EXTRA_ATTACKS: [{d}]{s} count={d}\n", .{ spell_id, std.mem.span(getSpellName(spell_id)), count }); // _EXTRA_ATTACKS: spellId, spellSchool, amount - fireCombatLog(SUB_SPELL_EXTRA_ATTACKS, src_str, GUID_ZERO, spell_id, school, count, 0, 0, 0, 0); + fireSpellExtraAttacks(src_str, src_name, GUID_ZERO, "", spell_id, getSpellName(spell_id), school, count); } } @@ -1301,6 +1447,8 @@ fn dispelDetour(caster_ptr: u32, target_ptr: u32, spell_id: u32) callconv(hook.c if (caster_guid != 0 and target_guid != 0) { const src_str = guidToString(caster_guid); const dst_str = guidToString(target_guid); + const src_name = wow.getNameByGUID(caster_guid); + const dst_name = wow.getNameByGUID(target_guid); const extra_school = getSpellSchool(spell_id); // Determine aura type from spell DB for the dispelled aura const aura_type_str: [*:0]const u8 = blk: { @@ -1312,7 +1460,7 @@ fn dispelDetour(caster_ptr: u32, target_ptr: u32, spell_id: u32) callconv(hook.c }; log.fmt("SPELL_DISPEL: dispelled={d}\n", .{spell_id}); // _DISPEL: dispelSpellId(0 — unknown), school, extraSpellId(dispelled aura), extraSchool, auraType - fireSpellDispel(SUB_SPELL_DISPEL, src_str, dst_str, 0, 0, spell_id, extra_school, aura_type_str); + fireSpellDispel(SUB_SPELL_DISPEL, src_str, src_name, dst_str, dst_name, 0, "", 0, spell_id, getSpellName(spell_id), extra_school, aura_type_str); } } @@ -1330,6 +1478,32 @@ fn dispelDetour(caster_ptr: u32, target_ptr: u32, spell_id: u32) callconv(hook.c const SPELL_DB_RECORDS: u32 = 0xC0D788; // m_recordsById pointer const SPELL_DB_MAX_ID: u32 = 0xC0D78C; // m_maxId +/// GetSpellNameById (0x6264B0): __fastcall(ECX=spellId) -> EAX=char* +/// Returns localized spell name string, or null if invalid. +const GET_SPELL_NAME: u32 = 0x6264B0; + +/// Locale index global at 0xC0E080 — 0 for English. +const LOCALE_INDEX: u32 = 0xC0E080; + +var spell_name_debug_count: u32 = 0; + +fn getSpellName(spell_id: u32) [*:0]const u8 { + if (spell_id == 0) return ""; + // SpellRec name at offset 0x1E0 + locale*4 (verified from SuperWoW: puVar3[locale + 0x78]) + // 0x78 * 4 = 0x1E0. Locale index global at 0xC0E080. + const rec = getSpellRecord(spell_id) orelse return ""; + const locale = hook.readMem(u32, LOCALE_INDEX); + const name_ptr = hook.readMem(u32, rec + 0x1E0 + locale * 4); + if (name_ptr == 0) return ""; + // Debug: log first 5 lookups + if (spell_name_debug_count < 5) { + spell_name_debug_count += 1; + const name: [*:0]const u8 = @ptrFromInt(name_ptr); + log.fmt("[SPELLNAME] id={d} locale={d} -> 0x{x} \"{s}\"\n", .{ spell_id, locale, name_ptr, std.mem.span(name) }); + } + return @ptrFromInt(name_ptr); +} + fn getSpellRecord(spell_id: u32) ?u32 { if (spell_id == 0) return null; const max_id = hook.readMem(u32, SPELL_DB_MAX_ID); @@ -1370,6 +1544,41 @@ fn getUnitMaxPower(guid: u64, power_type: u32) ?u32 { return hook.readMem(u32, m_data + DESC_MAXPOWER_BASE + power_type * 4); } +/// Read a unit's current health from descriptors. +fn getUnitHealth(guid: u64) ?u32 { + const obj = wow.getObjectByGUID(guid); + if (obj == 0) return null; + const m_data = hook.readMem(u32, obj + 0x08); + if (m_data == 0) return null; + return hook.readMem(u32, m_data + DESC_HEALTH); +} + +/// Read a unit's max health from descriptors. +fn getUnitMaxHealth(guid: u64) ?u32 { + const obj = wow.getObjectByGUID(guid); + if (obj == 0) return null; + const m_data = hook.readMem(u32, obj + 0x08); + if (m_data == 0) return null; + return hook.readMem(u32, m_data + DESC_MAXHEALTH); +} + +/// Compute overkill: how much damage exceeded target's remaining HP. +/// Returns -1 (as u32 bitcast) if the target survives — WotLK convention. +fn computeOverkill(target_guid: u64, damage: u32) u32 { + const hp = getUnitHealth(target_guid) orelse return @bitCast(@as(i32, -1)); + if (damage >= hp) return damage - hp; + return @bitCast(@as(i32, -1)); +} + +/// Compute overheal: how much healing exceeded the target's health deficit. +fn computeOverheal(target_guid: u64, heal_amount: u32) u32 { + const cur = getUnitHealth(target_guid) orelse return 0; + const max = getUnitMaxHealth(target_guid) orelse return 0; + const deficit = if (max > cur) max - cur else 0; + if (heal_amount > deficit) return heal_amount - deficit; + return 0; +} + /// Vanilla spell effect types for summon abilities. fn isSummonEffect(e: u32) bool { return e == 28 or e == 42 or e == 56 or @@ -1399,6 +1608,21 @@ fn isDamageSplitSpell(spell_id: u32) bool { return false; } +/// Check if spell has SPELL_AURA_PERIODIC_LEECH (53) — health drain (Drain Life, Siphon Life). +/// Vanilla server routes these through SendSpellNonMeleeDamageLog, not PERIODICAURALOG, +/// so we reclassify them from SPELL_DAMAGE to SPELL_PERIODIC_LEECH in the damage hook. +fn isPeriodicLeechSpell(spell_id: u32) bool { + const rec = getSpellRecord(spell_id) orelse return false; + inline for (0..3) |i| { + const effect = hook.readMem(u32, rec + 0xF4 + i * 4); + if (effect == 6 or effect == 27) { // APPLY_AURA or APPLY_AREA_AURA + const aura = hook.readMem(u32, rec + 0x16C + i * 4); + if (aura == 53) return true; + } + } + return false; +} + /// Determine aura type from spell DB Attributes flag. /// Spell attribute 0x4000000 = SPELL_ATTR_NEGATIVE (harmful/debuff). /// Falls back to slot-based heuristic if spell not found in DB. @@ -1494,41 +1718,39 @@ fn spellEffectDetour(caster_ptr: u32, target_ptr: u32, spell_id: u32) callconv(h const is_summon = isSummonEffect(e0) or isSummonEffect(e1) or isSummonEffect(e2); const is_resurrect = isResurrectEffect(e0) or isResurrectEffect(e1) or isResurrectEffect(e2); - const is_energize = isEnergizeEffect(e0) or isEnergizeEffect(e1) or isEnergizeEffect(e2); - if (is_summon or is_resurrect or is_energize) { + if (is_summon or is_resurrect) { const caster_lo = hook.readMem(u32, caster_ptr); const caster_hi = hook.readMem(u32, caster_ptr + 4); const caster_guid: u64 = @as(u64, caster_hi) << 32 | @as(u64, caster_lo); if (caster_guid != 0) { const src_str = guidToString(caster_guid); + const src_name = wow.getNameByGUID(caster_guid); const school = hook.readMem(u32, rec + 0x04); // Target may be NULL for summons (SPELLLOGEXECUTE case 10 passes NULL) var dst_str: [*:0]const u8 = GUID_ZERO; + var dst_name: [*:0]const u8 = ""; if (target_ptr != 0) { const target_lo = hook.readMem(u32, target_ptr); const target_hi = hook.readMem(u32, target_ptr + 4); const target_guid: u64 = @as(u64, target_hi) << 32 | @as(u64, target_lo); if (target_guid != 0) { dst_str = guidToString(target_guid); + dst_name = wow.getNameByGUID(target_guid); } } if (is_summon) { - log.fmt("SPELL_SUMMON: spell={d}\n", .{spell_id}); - fireSpell(SUB_SPELL_SUMMON, src_str, dst_str, spell_id, school); + log.fmt("SPELL_SUMMON: [{d}]{s}\n", .{ spell_id, std.mem.span(getSpellName(spell_id)) }); + fireSpell(SUB_SPELL_SUMMON, src_str, src_name, dst_str, dst_name, spell_id, getSpellName(spell_id), school); } if (is_resurrect) { - log.fmt("SPELL_RESURRECT: spell={d}\n", .{spell_id}); - fireSpell(SUB_SPELL_RESURRECT, src_str, dst_str, spell_id, school); - } - if (is_energize) { - log.fmt("SPELL_ENERGIZE: spell={d}\n", .{spell_id}); - // _ENERGIZE: spellId, amount(0 — not available from SPELLLOGEXECUTE), powerType(0 — unknown) - fireCombatLog(SUB_SPELL_ENERGIZE, src_str, dst_str, spell_id, 0, 0, 0, 0, 0, 0); + log.fmt("SPELL_RESURRECT: [{d}]{s}\n", .{ spell_id, std.mem.span(getSpellName(spell_id)) }); + fireSpell(SUB_SPELL_RESURRECT, src_str, src_name, dst_str, dst_name, spell_id, getSpellName(spell_id), school); } + // SPELL_ENERGIZE now handled by dedicated ProcessStandardPowerGainMessage hook } } } @@ -1589,10 +1811,11 @@ fn auraDurationDetour(slot: u32, duration: u32) callconv(hook.cc.fastcall) void if (is_active) { // Slot already has this aura — this is a refresh const dst_str = guidToString(player_guid); + const dst_name = wow.getNameByGUID(player_guid); const aura_type = getAuraType(spell_id, slot); const school = getSpellSchool(spell_id); - log.fmt("SPELL_AURA_REFRESH: spell={d} slot={d}\n", .{ spell_id, slot }); - fireSpellStr(SUB_SPELL_AURA_REFRESH, GUID_ZERO, dst_str, spell_id, school, aura_type); + log.fmt("SPELL_AURA_REFRESH: [{d}]{s} slot={d}\n", .{ spell_id, std.mem.span(getSpellName(spell_id)), slot }); + fireSpellStr(SUB_SPELL_AURA_REFRESH, GUID_ZERO, "", dst_str, dst_name, spell_id, getSpellName(spell_id), school, aura_type); } } } @@ -1632,6 +1855,8 @@ fn dispelFailedDetour(msg_type: u32, cds: u32) callconv(hook.cc.stdcall) ?*anyop if (caster_guid != 0 and target_guid != 0) { const src_str = guidToString(caster_guid); const dst_str = guidToString(target_guid); + const src_name = wow.getNameByGUID(caster_guid); + const dst_name = wow.getNameByGUID(target_guid); // Loop: read spell IDs until buffer exhausted (ReadPointerFromStream reads u32) var count: u32 = 0; @@ -1641,10 +1866,10 @@ fn dispelFailedDetour(msg_type: u32, cds: u32) callconv(hook.cc.stdcall) ?*anyop if (spell_id.? == 0) break; const school = getSpellSchool(spell_id.?); - log.fmt("SPELL_DISPEL_FAILED: spell={d} caster=0x{x} target=0x{x}\n", .{ - spell_id.?, caster_guid, target_guid, + log.fmt("SPELL_DISPEL_FAILED: [{d}]{s} caster=0x{x} target=0x{x}\n", .{ + spell_id.?, std.mem.span(getSpellName(spell_id.?)), caster_guid, target_guid, }); - fireSpell(SUB_SPELL_DISPEL_FAILED, src_str, dst_str, spell_id.?, school); + fireSpellInterrupt(SUB_SPELL_DISPEL_FAILED, src_str, src_name, dst_str, dst_name, 0, "", 0, spell_id.?, getSpellName(spell_id.?), school); } } } @@ -1747,6 +1972,12 @@ pub fn installHooks() void { log.print("Hooked ProcessSpellDrainEffectMessage (DAMAGE_SHIELD)\n"); } + if (energize_hook.attach(0x62CA00, &energizeDetour) != .ok) { + log.print("FAILED to hook ProcessStandardPowerGainMessage\n"); + } else { + log.print("Hooked ProcessStandardPowerGainMessage (SPELL_ENERGIZE)\n"); + } + if (spell_interrupt_hook.attach(0x626A10, &spellInterruptDetour) != .ok) { log.print("FAILED to hook DisplaySpellInterruptMessage\n"); } else { @@ -1845,3 +2076,119 @@ pub fn removeHooks() void { } g_is_hook_owner = false; } + +// ============================================================================= +// Lua function: GetSpellInfo(spellId) — WotLK-compatible +// Returns: name, rank, icon, castTime, minRange, maxRange, spellId +// +// SpellRec field offsets (dword indices from SuperWoW, byte offset = index * 4): +// Name: [0x78 + locale] = byte 0x1E0 + locale*4 +// Rank: [0x81 + locale] = byte 0x204 + locale*4 (SpellNameSubtext) +// SpellIconID: [0x8A] = byte 0x228 +// CastingTimeIdx: [0x55] = byte 0x154 +// RangeIdx: [0x54] = byte 0x150 +// ============================================================================= + +/// SpellRec field offsets (byte offsets from record base) +/// Verified from nampower game.hpp SpellRec struct + SuperWoW Ghidra analysis. +const SPELL_CAST_TIME_IDX: u32 = 0x48; // CastingTimeIndex — indexes SpellCastTimes.dbc +const SPELL_RANGE_IDX: u32 = 0x90; // rangeIndex — indexes SpellRange.dbc +const SPELL_ICON_ID: u32 = 0x1D4; // SpellIconID — indexes SpellIcon.dbc +const SPELL_NAME_BASE: u32 = 0x1E0; // SpellName[0] — + locale*4 for localized name +const SPELL_RANK_BASE: u32 = 0x204; // SpellNameSubtext[0] — + locale*4 for rank string + +/// SpellIcon.dbc (verified: nampower + SuperWoW) +/// Record: [0]=ID, [0x04]=texture_path_string_ptr +const SPELL_ICON_RECORDS: u32 = 0xC0D7EC; // m_recordsById +const SPELL_ICON_MAX_ID: u32 = 0xC0D7F0; + +/// SpellRange.dbc (verified: nampower offsets.hpp) +/// Record: [0]=ID, [0x04]=minRange(f32), [0x08]=maxRange(f32), [0x0C]=flags, [0x10]=name[8 locales] +const SPELL_RANGE_RECORDS: u32 = 0xC0D79C; // m_recordsById +const SPELL_RANGE_MAX_ID: u32 = 0xC0D7A0; + +/// SpellCastTimes.dbc (verified: Ghidra analysis of Spell_C_GetCastTime 0x6E3340) +/// Record: [0]=ID, [0x04]=baseCastTime(ms), [0x08]=castTimePerLevel, [0x0C]=minCastTime +const CAST_TIMES_RECORDS: u32 = 0xC0D878; // m_recordsById +const CAST_TIMES_MAX_ID: u32 = 0xC0D87C; + +fn readDbRecord(records_ptr_addr: u32, max_id_addr: u32, id: u32) ?u32 { + if (id == 0) return null; + const max_id = hook.readMem(u32, max_id_addr); + if (id > max_id) return null; + const records_ptr = hook.readMem(u32, records_ptr_addr); + if (records_ptr == 0) return null; + const rec = hook.readMem(u32, records_ptr + id * 4); + if (rec == 0) return null; + return rec; +} + +fn readLocString(rec: u32, base_offset: u32) [*:0]const u8 { + const locale = hook.readMem(u32, LOCALE_INDEX); + const ptr = hook.readMem(u32, rec + base_offset + locale * 4); + if (ptr == 0) return ""; + return @ptrFromInt(ptr); +} + +/// GetSpellInfo(spellId) -> name, rank, icon, castTime, minRange, maxRange, spellId +/// Matches WotLK API. Returns nil if spell doesn't exist. +pub fn luaGetSpellInfo(L: usize) callconv(hook.cc.fastcall) u32 { + const state: lua.State = @ptrFromInt(L); + const nargs = lua.gettop(state); + if (nargs < 1) return 0; + + const spell_id: u32 = @intFromFloat(lua.tonumber(state, 1)); + const rec = getSpellRecord(spell_id) orelse { + // Invalid spell — return nil (WotLK behavior) + lua.pushnil(state); + return 1; + }; + + // 1. name + lua.pushstring(state, readLocString(rec, SPELL_NAME_BASE)); + + // 2. rank (SpellNameSubtext) + lua.pushstring(state, readLocString(rec, SPELL_RANK_BASE)); + + // 3. icon texture path — resolve SpellIconID through SpellIcon.dbc + const icon_id = hook.readMem(u32, rec + SPELL_ICON_ID); + if (readDbRecord(SPELL_ICON_RECORDS, SPELL_ICON_MAX_ID, icon_id)) |icon_rec| { + // SpellIcon.dbc record: [0]=ID, [1]=texture path string + const tex_ptr = hook.readMem(u32, icon_rec + 4); + if (tex_ptr != 0) { + lua.pushstring(state, @ptrFromInt(tex_ptr)); + } else { + lua.pushnil(state); + } + } else { + lua.pushnil(state); + } + + // 4. castTime (ms) — resolve CastingTimeIndex through SpellCastTimes.dbc + const cast_idx = hook.readMem(u32, rec + SPELL_CAST_TIME_IDX); + if (readDbRecord(CAST_TIMES_RECORDS, CAST_TIMES_MAX_ID, cast_idx)) |ct_rec| { + // SpellCastTimes.dbc record: [0]=ID, [1]=castTime (ms) + const cast_time = hook.readMem(u32, ct_rec + 4); + lua.pushnumber(state, @floatFromInt(cast_time)); + } else { + lua.pushnumber(state, 0); + } + + // 5. minRange, 6. maxRange — resolve RangeIndex through SpellRange.dbc + const range_idx = hook.readMem(u32, rec + SPELL_RANGE_IDX); + if (readDbRecord(SPELL_RANGE_RECORDS, SPELL_RANGE_MAX_ID, range_idx)) |range_rec| { + // SpellRange.dbc record: [0]=ID, [1]=minRange(float), [2]=maxRange(float) + const min_range: f32 = @bitCast(hook.readMem(u32, range_rec + 4)); + const max_range: f32 = @bitCast(hook.readMem(u32, range_rec + 8)); + lua.pushnumber(state, @floatCast(min_range)); + lua.pushnumber(state, @floatCast(max_range)); + } else { + lua.pushnumber(state, 0); + lua.pushnumber(state, 0); + } + + // 7. spellId (echo back — matches WotLK) + lua.pushnumber(state, @floatFromInt(spell_id)); + + return 7; // 7 return values +} diff --git a/src/main.zig b/src/main.zig index aa495d3..6c3930c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -109,6 +109,9 @@ fn registerLuaFunctions() void { if (build_opts.transform44) { registerFunction("SetWeatherOverride", @intFromPtr(&transform44.luaSetWeatherOverride)); } + if (build_opts.dpslog) { + registerFunction("GetSpellInfo", @intFromPtr(&dpslog.luaGetSpellInfo)); + } if (build_opts.addonperf) { registerFunction("GetAddOnMemoryUsage", @intFromPtr(&addonperf.luaGetAddOnMemoryUsage)); registerFunction("UpdateAddOnMemoryUsage", @intFromPtr(&addonperf.luaUpdateAddOnMemoryUsage)); diff --git a/src/offsets.zig b/src/offsets.zig index b7bf94d..a703563 100644 --- a/src/offsets.zig +++ b/src/offsets.zig @@ -162,6 +162,12 @@ pub const FN_CVAR_LOOKUP: usize = 0x0063DEC0; /// SceneEnd: __thiscall(device). Per-frame hook point. pub const FN_SCENE_END: usize = 0x5A17A0; +/// RetrieveNPCDataFromCache — __thiscall(ECX=cache_obj), 6 stack params, RET 0x18. +/// Returns char* name or 0 on miss. +pub const FN_NAME_CACHE_LOOKUP: usize = 0x0055F080; +/// Name cache object pointer (global at 0xC0E228). +pub const NAME_CACHE_OBJ: usize = 0x00C0E228; + // ============================================================================= // D3D9 / GxDevice // ============================================================================= diff --git a/src/wow.zig b/src/wow.zig index f19bd5d..7cf5652 100644 --- a/src/wow.zig +++ b/src/wow.zig @@ -224,6 +224,22 @@ pub fn getLocalPlayer() u32 { return getObjectByGUID(guid); } +/// Resolve a GUID to a unit/NPC name via the name cache. +/// Returns the name string or "" if not cached. +pub fn getNameByGUID(guid: u64) [*:0]const u8 { + if (guid == 0) return ""; + const lo: u32 = @truncate(guid); + const hi: u32 = @truncate(guid >> 32); + var name_buf: [2]u32 = .{ 0, 0 }; + const result = hook.call(fn (u32, u32, u32, u32, u32, u32, u32) callconv(hook.cc.thiscall) u32, o.FN_NAME_CACHE_LOOKUP, .{ + o.NAME_CACHE_OBJ, lo, hi, @intFromPtr(&name_buf), 0, 0, 0, + }); + if (result != 0) { + return @ptrFromInt(result); + } + return ""; +} + /// Get the current target's GUID. pub fn getTargetGUID() u64 { return unitGUID("target");