Publish source: Unlicense, public README, repo hygiene
The remote was previously a distribution-only point for pre-built DLLs. This opens the source. - LICENSE: Unlicense, with a GPL-3.0 carve-out for src/dpslog/WeirdDPSMate (a DPSMate fork that keeps its own license) - README.md replaces the stale internal one with the user-facing docs from DLL_README.md, swapping the 'Why No Source Code?' section for build and layout notes. DLL_README.md is dropped; one README now serves both. - RELEASING.md: drop the trim-the-README-per-release dance and the remote/WeirdUtils/ distribution clone, both obsolete now - gitignore agent/editor scratch, build caches, the vendored WSBT addon, and the WeirdThreat/uwu-logs checkouts (separate upstream repos) - Commit outstanding module work: superweirdo, clickthrough portal visuals, transform44 decompiles, worldmarkers demo presets, tools/
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
# Handoff: COMBAT_LOG_EVENT Filtering (CLEU vs CLEUF)
|
||||
|
||||
## Goal
|
||||
|
||||
Implement Blizzard's TBC/WotLK event filtering system so we provide both:
|
||||
- `COMBAT_LOG_EVENT` -- filtered version (only events relevant to the player's group)
|
||||
- `COMBAT_LOG_EVENT_UNFILTERED` -- all events in range (everything the client sees)
|
||||
|
||||
This matches how TBC and WotLK work: addons register for whichever they need. DPS meters
|
||||
use UNFILTERED for complete data, while UI elements use the filtered version to reduce noise.
|
||||
|
||||
## Background
|
||||
|
||||
### How Blizzard Did It
|
||||
|
||||
In TBC (2.4.0), Blizzard introduced `COMBAT_LOG_EVENT_UNFILTERED` which fires for ALL
|
||||
combat events the client receives. They also provided `COMBAT_LOG_EVENT` which fires only
|
||||
for events passing a set of filters.
|
||||
|
||||
The filtering is controlled by:
|
||||
- `CombatLogClearEntries()` -- clears the combat log
|
||||
- `CombatLogAddFilter(...)` -- adds a filter rule
|
||||
- `CombatLogResetFilter()` -- resets to default filters
|
||||
- `CombatLogGetCurrentEntry(...)` -- reads the current event (deprecated by WotLK)
|
||||
- `CombatLogGetNumEntries()` -- count of buffered events
|
||||
|
||||
Default filters include:
|
||||
- Source or dest is the player
|
||||
- Source or dest is in the player's party/raid
|
||||
- Source or dest is the player's pet/guardian
|
||||
- Events within a certain range
|
||||
|
||||
### What We Currently Have
|
||||
|
||||
We fire a single `COMBAT_LOG_EVENT` at slot 549 via SignalEventParam. Every hook fires
|
||||
every event it sees -- no filtering. This is effectively UNFILTERED behavior under the
|
||||
FILTERED event name.
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Phase 1: Register Both Events
|
||||
|
||||
Add a second event slot for `COMBAT_LOG_EVENT_UNFILTERED`:
|
||||
- `COMBAT_LOG_EVENT` at slot 549 (existing) -- will become the filtered version
|
||||
- `COMBAT_LOG_EVENT_UNFILTERED` at slot 550 -- fires everything (current behavior)
|
||||
|
||||
In `createEventsDetour`, bump the event count by 2 instead of 1 and register both names.
|
||||
Update all fire functions to fire both events (or fire UNFILTERED always and FILTERED
|
||||
conditionally).
|
||||
|
||||
**IMPORTANT**: Slot 550 was previously identified as a float global (0x3E800000 = 0.25).
|
||||
Need to verify this is still the case or find a different slot. May need to search for
|
||||
two consecutive free slots.
|
||||
|
||||
### Phase 2: Implement Filtering Logic
|
||||
|
||||
The filter checks whether source or dest is "interesting" to the local player:
|
||||
|
||||
```zig
|
||||
fn shouldFilter(src_guid: u64, dst_guid: u64) bool {
|
||||
// Always pass if source or dest is the local player
|
||||
if (src_guid == getPlayerGUID() or dst_guid == getPlayerGUID()) return false;
|
||||
// Always pass if source or dest is in the player's group
|
||||
if (isGroupMember(src_guid) or isGroupMember(dst_guid)) return false;
|
||||
// Always pass if source or dest is a pet owned by a group member
|
||||
if (isPetOfGroupMember(src_guid) or isPetOfGroupMember(dst_guid)) return false;
|
||||
// Filter out (don't fire COMBAT_LOG_EVENT)
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
Requires:
|
||||
- `getPlayerGUID()` -- already have this (0x468550)
|
||||
- `isGroupMember(guid)` -- check party (0xBC6F48) and raid (0xB712A8) GUID arrays
|
||||
- `isPetOfGroupMember(guid)` -- check UNIT_FIELD_SUMMONEDBY descriptor, resolve owner
|
||||
|
||||
### Phase 3: Fire Functions
|
||||
|
||||
Two approaches:
|
||||
|
||||
**Option A: Double-fire**
|
||||
Every fire function fires the event twice -- once for UNFILTERED (always), once for
|
||||
FILTERED (if passes filter). Simple but doubles the SignalEventParam calls.
|
||||
|
||||
**Option B: Conditional fire with shared push**
|
||||
Push args to Lua stack once, then call SignalEvent for each registered event that should
|
||||
receive it. Requires understanding SignalEventParam internals more deeply.
|
||||
|
||||
Option A is simpler and the performance cost of an extra SignalEventParam call per event
|
||||
is negligible compared to the Lua handler execution.
|
||||
|
||||
### Phase 4: sourceFlags / destFlags
|
||||
|
||||
With filtering in place, we can also construct the COMBATLOG_OBJECT_* bitfield that
|
||||
WotLK addons use for their own filtering:
|
||||
|
||||
```
|
||||
COMBATLOG_OBJECT_AFFILIATION_MINE = 0x0001
|
||||
COMBATLOG_OBJECT_AFFILIATION_PARTY = 0x0002
|
||||
COMBATLOG_OBJECT_AFFILIATION_RAID = 0x0004
|
||||
COMBATLOG_OBJECT_AFFILIATION_OUTSIDER = 0x0008
|
||||
COMBATLOG_OBJECT_REACTION_FRIENDLY = 0x0010
|
||||
COMBATLOG_OBJECT_REACTION_NEUTRAL = 0x0020
|
||||
COMBATLOG_OBJECT_REACTION_HOSTILE = 0x0040
|
||||
COMBATLOG_OBJECT_CONTROL_PLAYER = 0x0100
|
||||
COMBATLOG_OBJECT_CONTROL_NPC = 0x0200
|
||||
COMBATLOG_OBJECT_TYPE_PLAYER = 0x0400
|
||||
COMBATLOG_OBJECT_TYPE_NPC = 0x0800
|
||||
COMBATLOG_OBJECT_TYPE_PET = 0x1000
|
||||
COMBATLOG_OBJECT_TYPE_GUARDIAN = 0x2000
|
||||
COMBATLOG_OBJECT_TYPE_OBJECT = 0x4000
|
||||
```
|
||||
|
||||
These can be constructed from:
|
||||
- GUID type bits (high nibble of GUID encodes player/creature/pet/gameobject)
|
||||
- Group membership (party/raid roster arrays)
|
||||
- Reaction (UnitReaction at 0x6061E0)
|
||||
- Ownership (UNIT_FIELD_SUMMONEDBY descriptor)
|
||||
|
||||
This is the same data needed for filtering, so it comes naturally after Phase 2.
|
||||
|
||||
### Phase 5: Lua API (Optional)
|
||||
|
||||
Provide the filter configuration API for full parity:
|
||||
- `CombatLogAddFilter(srcFlags, dstFlags, eventType)`
|
||||
- `CombatLogResetFilter()`
|
||||
- `CombatLogGetNumEntries()`
|
||||
|
||||
Most addons don't use these -- they just register for UNFILTERED and do their own
|
||||
filtering. Low priority.
|
||||
|
||||
## Key Addresses
|
||||
|
||||
- Event slot 549: COMBAT_LOG_EVENT (existing)
|
||||
- Event slot 550: needs verification (was float 0.25 in earlier research)
|
||||
- Player GUID: 0x468550 (ClntObjMgrGetActivePlayer)
|
||||
- Party GUIDs: 0xBC6F48 (array of 4 u64 GUIDs)
|
||||
- Raid roster: 0xB712A8 (array of ptrs), count at 0xB713E0
|
||||
- UnitReaction: 0x6061E0
|
||||
- UNIT_FIELD_SUMMONEDBY: descriptor offset 0x30 (index 0x0C)
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Phase 1 can start immediately
|
||||
- Phase 2 needs isGroupMember which uses existing known addresses
|
||||
- Phase 3 trivial once Phase 1+2 done
|
||||
- Phase 4 is independent enhancement, can parallelize with Phase 2
|
||||
|
||||
## Risk
|
||||
|
||||
- Slot 550 conflict with float global -- may need to find a safe slot
|
||||
- Double-firing events may interact poorly with addons that register for both
|
||||
(they'd see every event twice) -- need to ensure the events have distinct IDs
|
||||
- Performance of filter check per event should be negligible (few memory reads)
|
||||
@@ -599,6 +599,20 @@ All in the 0x51xxxx addon/UI system. Zeroing/overwriting these corrupts game sta
|
||||
SMSG_RESURRECT_FAILED (0x252).
|
||||
- [ ] **UNIT_POSITION**: Periodic or event-driven unit position data in combat log events.
|
||||
Useful for replay/analysis tools.
|
||||
- [ ] **SPELL_CAST dest coordinates**: SMSG_SPELL_START/GO include destination XYZ for
|
||||
area-targeted spells via TARGET_FLAG_DEST_LOCATION (0x40) in SpellCastTargets.
|
||||
- Target data format in packet: uint16 targetMask, then if 0x02: packed GUID (unit),
|
||||
if 0x20: 3x float (source XYZ), if 0x40: 3x float (dest XYZ)
|
||||
- Player ground AoE (Blizzard, Flamestrike, Rain of Fire, Volley): client sends clicked
|
||||
position, server echoes in both SPELL_START and SPELL_GO
|
||||
- Mob ground AoE (Ignite Earth, mob Flamestrike): AI script calls CastSpell(x,y,z,...),
|
||||
server sets 0x40 via setDestination(), coords appear in SPELL_GO
|
||||
- Self-centered AoE (Arcane Explosion, Hellfire): no 0x40, no dest coords
|
||||
- Triggered spells: dest overridden to caster position regardless of original flags
|
||||
- Already hooked at 0x6E7640 (spellStartDetour) -- just need to parse target mask and
|
||||
extract floats when 0x40 is set, emit as extra CLEU params on SPELL_CAST_START/SUCCESS
|
||||
- Server ref: SpellCastTargets::write() in tortoise-wow Spell.cpp lines 223-268
|
||||
- Enables boss mod positional warnings (e.g. Karazhan 40-man Ignite Earth ground zones)
|
||||
|
||||
### Completed reference
|
||||
- [x] RANGE_MISSED: Detected via spell ID check (75/5019) in ProcessSpellCombatResult hook.
|
||||
@@ -1010,3 +1024,91 @@ ProcessSpellDamageWithLocalization (0x629d30)
|
||||
| FrameScript_CreateEvents | FrameScript_CreateEvents | 0x703D90 |
|
||||
| FrameScript_RegisterFunction | FrameScript_RegisterFunction | 0x704120 |
|
||||
| GetSpellNameById | (SpellDb at 0xC0D780) | 0x6264b0 |
|
||||
|
||||
---
|
||||
|
||||
## Missing WotLK CLEU Subevents
|
||||
|
||||
Events present in WotLK 3.3.5 COMBAT_LOG_EVENT_UNFILTERED but not yet implemented.
|
||||
|
||||
### Loot Events (novel -- never existed in any WoW combat log)
|
||||
|
||||
LOOT was never a COMBAT_LOG_EVENT subevent in any WoW expansion. Loot tracking addons
|
||||
use separate Lua events (LOOT_OPENED, CHAT_MSG_LOOT, etc.), not the combat log.
|
||||
Adding loot to our combat log would be a novel extension, not WotLK parity.
|
||||
|
||||
Relevant vanilla packets if we ever want to add this:
|
||||
|
||||
| Packet | Opcode | Format |
|
||||
|--------|--------|--------|
|
||||
| SMSG_ITEM_PUSH_RESULT | 0x166 | playerGUID(8), received(4), created(4), showInChat(4), bagSlot(1), itemSlot(4), itemID(4), suffix(4), randomProp(4), count(4) |
|
||||
| SMSG_LOOT_ROLL | 0x2A2 | targetGUID(8), slot(4), rollerGUID(8), itemID(4), suffix(4), randomProp(4), rollNum(1), rollType(1) |
|
||||
| SMSG_LOOT_ROLL_WON | 0x29F | targetGUID(8), slot(4), itemID(4), suffix(4), randomProp(4), winnerGUID(8), rollNum(1), rollType(1) |
|
||||
| SMSG_LOOT_ALL_PASSED | 0x29E | targetGUID(8), slot(4), itemID(4), randomProp(4), suffix(4) |
|
||||
| SMSG_LOOT_MONEY_NOTIFY | 0x163 | amount(4) |
|
||||
|
||||
### Enchant Events (verified in WotLK/Cata logs)
|
||||
|
||||
Confirmed present in real WotLK combat logs. No vanilla packet equivalent -- would need
|
||||
to hook the client enchant application/removal functions.
|
||||
|
||||
| Subevent | Description |
|
||||
|----------|-------------|
|
||||
| ENCHANT_APPLIED | Item enchanted (temp or permanent) |
|
||||
| ENCHANT_REMOVED | Enchant removed/expired |
|
||||
|
||||
Verified WotLK format:
|
||||
```
|
||||
ENCHANT_APPLIED,srcGUID,srcName,srcFlags,srcRaidFlags,dstGUID,dstName,dstFlags,dstRaidFlags,"enchantName",itemID,"itemName"
|
||||
ENCHANT_REMOVED,0x0000000000000000,nil,0x80000000,0x80000000,dstGUID,dstName,dstFlags,dstRaidFlags,"enchantName",itemID,"itemName"
|
||||
```
|
||||
|
||||
Note: ENCHANT_REMOVED source is always null GUID (no "remover").
|
||||
|
||||
### SPELL_CREATE (verified in WotLK/Cata logs)
|
||||
|
||||
Fires when a player creates a game object (trap, totem, feast, cauldron).
|
||||
Uses standard spell prefix. Dest is the created GO's GUID.
|
||||
|
||||
```
|
||||
SPELL_CREATE,srcGUID,srcName,srcFlags,srcRaidFlags,goGUID,"goName",goFlags,goRaidFlags,spellID,"spellName",spellSchool
|
||||
```
|
||||
|
||||
### Combat Events (low priority)
|
||||
|
||||
| Subevent | Description | Notes |
|
||||
|----------|-------------|-------|
|
||||
| UNIT_DISSIPATES | Pet/totem/guardian despawn | Variant of UNIT_DIED, not in WotLK sample but in retail |
|
||||
| SPELL_ABSORBED | Damage absorbed by shield | Present in Shadowlands+, not in WotLK sample |
|
||||
| SPELL_HEAL_ABSORBED | Healing absorbed by anti-heal | Shadowlands+, no vanilla mechanic |
|
||||
| SWING_DAMAGE_LANDED | Melee hit after absorb | Shadowlands+, not in WotLK |
|
||||
| SPELL_BUILDING_* | Structure damage prefix | WotLK siege content, N/A in vanilla |
|
||||
| *_DURABILITY_DAMAGE | Equipment durability loss | Rarely useful |
|
||||
|
||||
### Metadata Events (Shadowlands+ only, NOT in WotLK)
|
||||
|
||||
These do NOT appear in WotLK/Cata combat logs. They were added in later expansions
|
||||
(combat log version 16+). Listed for reference only.
|
||||
|
||||
| Subevent | Description | First seen |
|
||||
|----------|-------------|------------|
|
||||
| ENCOUNTER_START | Boss encounter begins | Shadowlands+ |
|
||||
| ENCOUNTER_END | Boss encounter ends | Shadowlands+ |
|
||||
| ZONE_CHANGE | Player changes zone | Shadowlands+ |
|
||||
| COMBATANT_INFO | Gear/talent snapshot | Shadowlands+ |
|
||||
| CHALLENGE_MODE_START/END | M+ key start/end | Shadowlands+ |
|
||||
| EMOTE | Boss emote text | Shadowlands+ |
|
||||
| MAP_CHANGE | Map transition | Shadowlands+ |
|
||||
| ARENA_MATCH_START/END | Arena match | Shadowlands+ |
|
||||
|
||||
### Current Implementation Status
|
||||
|
||||
37 subevents implemented across 23 hooks (Phases 1-5 complete):
|
||||
- Damage: SWING/RANGE/SPELL/PERIODIC/SHIELD/SPLIT/ENVIRONMENTAL (7)
|
||||
- Missed: SWING/RANGE/SPELL/PERIODIC/SHIELD (5)
|
||||
- Heal: SPELL/PERIODIC (2)
|
||||
- Power: ENERGIZE/DRAIN/PERIODIC_ENERGIZE/PERIODIC_DRAIN/PERIODIC_LEECH (5)
|
||||
- Aura: APPLIED/REMOVED/DOSE(x2)/REFRESH/BROKEN/BROKEN_SPELL (7)
|
||||
- Cast: START/SUCCESS/FAILED (3)
|
||||
- Misc: INTERRUPT/DISPEL/DISPEL_FAILED/STOLEN/EXTRA_ATTACKS/SUMMON/RESURRECT/INSTAKILL (8)
|
||||
- Death: UNIT_DIED/UNIT_DESTROYED/PARTY_KILL (3)
|
||||
|
||||
@@ -486,7 +486,7 @@ local function profReport(label, tbl)
|
||||
end
|
||||
end
|
||||
|
||||
local benchActive = true
|
||||
local benchActive = false
|
||||
local benchFrame = CreateFrame("Frame")
|
||||
|
||||
local function benchCombatStart()
|
||||
|
||||
@@ -145,6 +145,14 @@ end
|
||||
local profiling = false
|
||||
local profCLEU = { events = 0, totalMs = 0, gcStart = 0 }
|
||||
local profOrig = { events = 0, totalMs = 0, gcStart = 0 }
|
||||
|
||||
-- Fine-grained timing buckets (microseconds, accumulated)
|
||||
local profDetail = { getInfo = 0, dbCalls = 0, dispatch = 0, total = 0, count = 0 }
|
||||
-- Per-DB-function timing (microseconds, accumulated)
|
||||
local profDB = {
|
||||
DamageDone = 0, DamageTaken = 0, EnemyDamage = 0, DeathHistory = 0,
|
||||
Healing = 0, HealingTaken = 0, BuildBuffs = 0, Other = 0, count = 0
|
||||
}
|
||||
local profCurrent = nil
|
||||
|
||||
-- Hook the parser's OnEvent to measure original mode
|
||||
@@ -204,6 +212,32 @@ local function profReport(label, tbl)
|
||||
"|cff00ff00[%s]|r %d events, %.1fms total, %.1f us/event, %+.1f KB gc",
|
||||
label, tbl.events, totalMs, avgUs, gcDelta))
|
||||
|
||||
-- Detail breakdown for CLEU
|
||||
if label == "CLEU" and profDetail.count > 0 then
|
||||
local avgGetInfo = profDetail.getInfo / profDetail.count
|
||||
local avgDB = profDetail.dbCalls / profDetail.count
|
||||
local avgTotal = profDetail.total / profDetail.count
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
||||
"|cff00ff00[CLEU detail]|r getInfo=%.1f us, db+logic=%.1f us, total=%.1f us (%d events)",
|
||||
avgGetInfo, avgDB, avgTotal, profDetail.count))
|
||||
-- Per-DB-function breakdown
|
||||
if profDB.count > 0 then
|
||||
local n = profDB.count
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
||||
"|cff00ff00[DB funcs]|r DD=%.0f DT=%.0f ED=%.0f DH=%.0f H=%.0f HT=%.0f Buf=%.0f Oth=%.0f us/evt",
|
||||
profDB.DamageDone/n, profDB.DamageTaken/n, profDB.EnemyDamage/n,
|
||||
profDB.DeathHistory/n, profDB.Healing/n, profDB.HealingTaken/n,
|
||||
profDB.BuildBuffs/n, profDB.Other/n))
|
||||
end
|
||||
profDetail.getInfo = 0
|
||||
profDetail.dbCalls = 0
|
||||
profDetail.total = 0
|
||||
profDetail.count = 0
|
||||
profDB.DamageDone = 0; profDB.DamageTaken = 0; profDB.EnemyDamage = 0
|
||||
profDB.DeathHistory = 0; profDB.Healing = 0; profDB.HealingTaken = 0
|
||||
profDB.BuildBuffs = 0; profDB.Other = 0; profDB.count = 0
|
||||
end
|
||||
|
||||
-- If we have both measurements, show comparison
|
||||
if lastCLEUAvg and lastOrigAvg and lastOrigAvg > 0 then
|
||||
local pct = ((lastCLEUAvg - lastOrigAvg) / lastOrigAvg) * 100
|
||||
@@ -216,38 +250,42 @@ local function profReport(label, tbl)
|
||||
end
|
||||
end
|
||||
|
||||
-- /dpsbench -- enables per-combat A/B profiling. Each combat: measure, report, flip.
|
||||
local benchActive = true
|
||||
-- Always-on CLEU profiling: report stats at every combat end.
|
||||
-- /dpsbench toggles A/B mode (alternates CLEU/ORIGINAL each combat for comparison).
|
||||
local benchActive = false
|
||||
|
||||
local benchFrame = CreateFrame("Frame")
|
||||
|
||||
local function benchCombatStart()
|
||||
if not benchActive then return end
|
||||
local function combatStart()
|
||||
profCurrent = cleuActive and profCLEU or profOrig
|
||||
profReset(profCurrent)
|
||||
debugprofilestart() -- start the monotonic clock for this combat
|
||||
debugprofilestart()
|
||||
profiling = true
|
||||
local label = cleuActive and "CLEU" or "ORIGINAL"
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[DPS Bench]|r combat started, measuring %s", label))
|
||||
if benchActive then
|
||||
local label = cleuActive and "CLEU" or "ORIGINAL"
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[DPS Bench]|r measuring %s", label))
|
||||
end
|
||||
end
|
||||
|
||||
local function benchCombatEnd()
|
||||
if not benchActive or not profiling then return end
|
||||
local function combatEnd()
|
||||
if not profiling then return end
|
||||
profiling = false
|
||||
local label = cleuActive and "CLEU" or "ORIGINAL"
|
||||
profReport(label, profCurrent)
|
||||
toggle()
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
||||
"|cff00ff00[DPS Bench]|r next combat will use: %s", cleuActive and "CLEU" or "ORIGINAL"))
|
||||
if benchActive then
|
||||
toggle()
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
||||
"|cff00ff00[DPS Bench]|r next combat: %s", cleuActive and "CLEU" or "ORIGINAL"))
|
||||
end
|
||||
end
|
||||
|
||||
benchFrame:RegisterEvent("PLAYER_REGEN_DISABLED")
|
||||
benchFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
|
||||
benchFrame:SetScript("OnEvent", function()
|
||||
if event == "PLAYER_REGEN_DISABLED" then
|
||||
benchCombatStart()
|
||||
combatStart()
|
||||
elseif event == "PLAYER_REGEN_ENABLED" then
|
||||
benchCombatEnd()
|
||||
combatEnd()
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -256,11 +294,10 @@ SlashCmdList["DPSBENCH"] = function()
|
||||
benchActive = not benchActive
|
||||
if benchActive then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(string.format(
|
||||
"|cff00ff00[DPS Bench]|r enabled. Current mode: %s. Enter combat to begin.",
|
||||
"|cff00ff00[DPS Bench]|r A/B enabled. Current mode: %s. Enter combat to begin.",
|
||||
cleuActive and "CLEU" or "ORIGINAL"))
|
||||
else
|
||||
profiling = false
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[DPS Bench]|r disabled.")
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[DPS Bench]|r A/B disabled (stats still reported).")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -280,16 +317,17 @@ local FailDB = DPSMate.Parser.FailDB
|
||||
|
||||
cleuHandler = function()
|
||||
if not CombatLogGetCurrentEventInfo then return end
|
||||
-- Single call — positions differ by prefix type:
|
||||
-- Swing: p1=amount, p2=overkill, p3=school, ...
|
||||
-- Spell: p1=spellId, p2=spellName, p3=spellSchool, p4=amount, ...
|
||||
-- Env: p1=envType, p2=amount, ...
|
||||
|
||||
local t0 = profiling and debugprofilestop()
|
||||
|
||||
local sub, srcGUID, srcName, srcFlags, srcRaidFlags,
|
||||
dstGUID, dstName, dstFlags, dstRaidFlags,
|
||||
p1, p2, p3, p4, p5, p6, p7, p8, p9,
|
||||
p10, p11, p12 = CombatLogGetCurrentEventInfo()
|
||||
if not sub then return end
|
||||
|
||||
local t1 = profiling and debugprofilestop()
|
||||
|
||||
if not srcName or srcName == "" then srcName = "Unknown" end
|
||||
if not dstName or dstName == "" then dstName = "Unknown" end
|
||||
|
||||
@@ -306,11 +344,17 @@ cleuHandler = function()
|
||||
local crush = crushing and 1 or 0
|
||||
local hit = (crit == 0 and glanc == 0 and crush == 0) and 1 or 0
|
||||
|
||||
local ta, tb
|
||||
if profiling then ta = debugprofilestop() end
|
||||
DB:DamageDone(srcName, AAttack, hit, crit, 0, 0, 0, 0, amount, glanc, 0)
|
||||
if profiling then tb = debugprofilestop(); profDB.DamageDone = profDB.DamageDone + (tb - ta); ta = tb end
|
||||
DB:DamageTaken(dstName, AAttack, hit, crit, 0, 0, 0, 0, amount, srcName, crush, 0)
|
||||
if profiling then tb = debugprofilestop(); profDB.DamageTaken = profDB.DamageTaken + (tb - ta); ta = tb end
|
||||
DB:EnemyDamage(1, DPSMateEDT, dstName, AAttack, hit, crit, 0, 0, 0, 0, amount, srcName, 0, crush)
|
||||
DB:EnemyDamage(2, DPSMateEDD, srcName, AAttack, hit, crit, 0, 0, 0, 0, amount, dstName, 0, 0)
|
||||
if profiling then tb = debugprofilestop(); profDB.EnemyDamage = profDB.EnemyDamage + (tb - ta); ta = tb end
|
||||
DB:DeathHistory(dstName, srcName, AAttack, amount, hit, crit, "hit", crush)
|
||||
if profiling then tb = debugprofilestop(); profDB.DeathHistory = profDB.DeathHistory + (tb - ta); profDB.count = profDB.count + 1 end
|
||||
if absorbed > 0 then
|
||||
DB:SetUnregisterVariables(absorbed, AAttack, srcName)
|
||||
DB:Absorb(AAttack, dstName, srcName)
|
||||
@@ -346,11 +390,17 @@ cleuHandler = function()
|
||||
local hit = (crit == 0 and glanc == 0 and crush == 0) and 1 or 0
|
||||
local abilityName = (sub == "SPELL_PERIODIC_DAMAGE") and (spellName .. "(Periodic)") or spellName
|
||||
|
||||
local ta, tb
|
||||
if profiling then ta = debugprofilestop() end
|
||||
DB:DamageDone(srcName, abilityName, hit, crit, 0, 0, 0, 0, amount, glanc, 0)
|
||||
if profiling then tb = debugprofilestop(); profDB.DamageDone = profDB.DamageDone + (tb - ta); ta = tb end
|
||||
DB:DamageTaken(dstName, abilityName, hit, crit, 0, 0, 0, 0, amount, srcName, crush, 0)
|
||||
if profiling then tb = debugprofilestop(); profDB.DamageTaken = profDB.DamageTaken + (tb - ta); ta = tb end
|
||||
DB:EnemyDamage(1, DPSMateEDT, dstName, abilityName, hit, crit, 0, 0, 0, 0, amount, srcName, 0, crush)
|
||||
DB:EnemyDamage(2, DPSMateEDD, srcName, abilityName, hit, crit, 0, 0, 0, 0, amount, dstName, 0, 0)
|
||||
if profiling then tb = debugprofilestop(); profDB.EnemyDamage = profDB.EnemyDamage + (tb - ta); ta = tb end
|
||||
DB:DeathHistory(dstName, srcName, abilityName, amount, hit, crit, "hit", crush)
|
||||
if profiling then tb = debugprofilestop(); profDB.DeathHistory = profDB.DeathHistory + (tb - ta); profDB.count = profDB.count + 1 end
|
||||
if spellSchool then DB:AddSpellSchool(abilityName, spellSchool) end
|
||||
if absorbed > 0 then
|
||||
DB:SetUnregisterVariables(absorbed, abilityName, srcName)
|
||||
@@ -396,10 +446,15 @@ cleuHandler = function()
|
||||
local effective = amount - overheal
|
||||
if effective < 0 then effective = 0 end
|
||||
|
||||
local ta, tb
|
||||
if profiling then ta = debugprofilestop() end
|
||||
DB:Healing(1, DPSMateHealingTaken, srcName, spellName, hit, crit, effective)
|
||||
DB:Healing(2, DPSMateOverhealing, srcName, spellName, hit, crit, overheal)
|
||||
if profiling then tb = debugprofilestop(); profDB.Healing = profDB.Healing + (tb - ta); ta = tb end
|
||||
DB:HealingTaken(1, DPSMateHealingTaken, srcName, spellName, hit, crit, effective, dstName)
|
||||
if profiling then tb = debugprofilestop(); profDB.HealingTaken = profDB.HealingTaken + (tb - ta); ta = tb end
|
||||
DB:DeathHistory(dstName, srcName, spellName, effective, hit, crit, "heal", 0)
|
||||
if profiling then tb = debugprofilestop(); profDB.DeathHistory = profDB.DeathHistory + (tb - ta); profDB.count = profDB.count + 1 end
|
||||
|
||||
-- ========================================================================
|
||||
-- AURA events (+ absorb shield lifecycle)
|
||||
@@ -481,6 +536,14 @@ cleuHandler = function()
|
||||
if not Parser.petToOwnerMap[dstName] then Parser.petToOwnerMap[dstName] = {} end
|
||||
Parser.petToOwnerMap[dstName][srcName] = true
|
||||
end
|
||||
|
||||
if profiling and t0 then
|
||||
local t2 = debugprofilestop()
|
||||
profDetail.getInfo = profDetail.getInfo + (t1 - t0)
|
||||
profDetail.dbCalls = profDetail.dbCalls + (t2 - t1)
|
||||
profDetail.total = profDetail.total + (t2 - t0)
|
||||
profDetail.count = profDetail.count + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Set the measured wrapper as the actual handler
|
||||
|
||||
@@ -875,6 +875,7 @@ function DPSMate.DB:OnGroupUpdate()
|
||||
local num = GetNumRaidMembers()
|
||||
DPSMate.Parser.TargetParty = {}
|
||||
DPSMate.Parser.petToOwnerMap = {}
|
||||
petQualifiedCache = {}
|
||||
if num<=0 then
|
||||
type = "party"
|
||||
num = GetNumPartyMembers()
|
||||
@@ -993,18 +994,36 @@ function DPSMate.DB:OnGroupUpdate()
|
||||
DPSMate.Parser:AssociateShaman("None", "None", true)
|
||||
end
|
||||
|
||||
-- Cache: raw pet name -> qualified "PetName (OwnerName)" to avoid repeated pairs() iteration
|
||||
local petQualifiedCache = {}
|
||||
|
||||
function DPSMate.DB:BuildUser(Dname, Dclass)
|
||||
if not Dname then Dname = "?!NIL Name?!" end
|
||||
-- Auto-qualify raw pet/totem names via petToOwnerMap (single-owner only)
|
||||
local pom = DPSMate.Parser.petToOwnerMap
|
||||
if pom and pom[Dname] then
|
||||
local count, singleOwner = 0, nil
|
||||
for o, _ in pairs(pom[Dname]) do
|
||||
count = count + 1
|
||||
singleOwner = o
|
||||
end
|
||||
if count == 1 then
|
||||
Dname = Dname .. " (" .. singleOwner .. ")"
|
||||
-- Fast path: already-known non-pet name — skip regex + petToOwnerMap entirely
|
||||
local u = DPSUser[Dname]
|
||||
if u then return u[1] end
|
||||
-- Check pet name cache (avoids repeated pairs() iteration on petToOwnerMap)
|
||||
local cached = petQualifiedCache[Dname]
|
||||
if cached then
|
||||
u = DPSUser[cached]
|
||||
if u then return u[1] end
|
||||
Dname = cached
|
||||
else
|
||||
-- Auto-qualify raw pet/totem names via petToOwnerMap (single-owner only)
|
||||
local pom = DPSMate.Parser.petToOwnerMap
|
||||
if pom and pom[Dname] then
|
||||
local count, singleOwner = 0, nil
|
||||
for o, _ in pairs(pom[Dname]) do
|
||||
count = count + 1
|
||||
singleOwner = o
|
||||
end
|
||||
if count == 1 then
|
||||
local qualified = Dname .. " (" .. singleOwner .. ")"
|
||||
petQualifiedCache[Dname] = qualified
|
||||
u = DPSUser[qualified]
|
||||
if u then return u[1] end
|
||||
Dname = qualified
|
||||
end
|
||||
end
|
||||
end
|
||||
local _,_, pet,owner = strfind(Dname,"(.+)%s%((.+)%)")
|
||||
@@ -1102,7 +1121,8 @@ function DPSMate.DB:Threat(cause, spellname, target, value, amount)
|
||||
target = self:BuildUser(target)
|
||||
cause = self:BuildUser(cause)
|
||||
spellname = self:BuildAbility(spellname)
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do
|
||||
local val = tablemodes[cat]
|
||||
if not DPSThreat[cat] then DPSThreat[cat] = {} end
|
||||
if not DPSThreat[cat][cause] then
|
||||
DPSThreat[cat][cause] = {}
|
||||
@@ -1743,7 +1763,7 @@ function DPSMate.DB:RegisterAbsorb(owner, ability, abilityTarget)
|
||||
owner = self:BuildUser(owner)
|
||||
abilityTarget = self:BuildUser(abilityTarget)
|
||||
ability = self:BuildAbility(ability)
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do local val = tablemodes[cat]
|
||||
if not DPSAbsorb[cat] then DPSAbsorb[cat] = {} end
|
||||
if not DPSAbsorb[cat][abilityTarget] then
|
||||
DPSAbsorb[cat][abilityTarget] = {}
|
||||
@@ -1777,7 +1797,7 @@ function DPSMate.DB:UnregisterAbsorb(ability, abilityTarget)
|
||||
ability = self:BuildAbility(ability)
|
||||
abilityTarget = self:BuildUser(abilityTarget)
|
||||
local AbsorbingAbility
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do local val = tablemodes[cat]
|
||||
AbsorbingAbility = self:GetActiveAbsorbAbilityByPlayer(ability, abilityTarget, cat)
|
||||
if AbsorbingAbility[1] then
|
||||
path = DPSAbsorb[cat][abilityTarget][AbsorbingAbility[1]][AbsorbingAbility[2]][AbsorbingAbility[3]]["i"]
|
||||
@@ -1889,7 +1909,7 @@ function DPSMate.DB:Absorb(ability, abilityTarget, incTarget)
|
||||
abilityTarget = self:BuildUser(abilityTarget)
|
||||
ability = self:BuildAbility(ability)
|
||||
local AbsorbingAbility
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do local val = tablemodes[cat]
|
||||
local abilityName = DPSMate:GetAbilityById(ability)
|
||||
local abilityEntry = abilityName and DPSMateAbility[abilityName]
|
||||
if not abilityEntry then break end
|
||||
@@ -2094,7 +2114,7 @@ function DPSMate.DB:Dispels(cause, Dname, target, ability)
|
||||
target = self:BuildUser(target)
|
||||
Dname = self:BuildAbility(Dname)
|
||||
ability = self:BuildAbility(ability)
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do local val = tablemodes[cat]
|
||||
if not DPSDispel[cat] then DPSDispel[cat] = {} end
|
||||
if not DPSDispel[cat][cause] then
|
||||
DPSDispel[cat][cause] = {
|
||||
@@ -2130,13 +2150,13 @@ function DPSMate.DB:UnregisterDeath(target)
|
||||
if strfind(target, "%s") then return end
|
||||
target = self:BuildUser(target)
|
||||
local p
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do
|
||||
if DPSDeath[cat] and DPSDeath[cat][target] and DPSDeath[cat][target][1] then
|
||||
DPSDeath[cat][target][1]["i"][1]=1
|
||||
DPSDeath[cat][target][1]["i"][2]=GameTime_GT()
|
||||
if cat==1 and DPSMate.Parser.TargetParty[DPSMate:GetUserById(target)] and DPSDeath[cat][target][1][1] then
|
||||
p = DPSDeath[cat][target][1][1]
|
||||
DPSMate:Broadcast(4, DPSMate:GetUserById(target), DPSMate:GetUserById(p[1]), DPSMate:GetAbilityById(p[2]), p[3])
|
||||
DPSMate:Broadcast(4, DPSMate:GetUserById(target), DPSMate:GetUserById(p[1]), DPSMate:GetAbilityById(p[2]), p[3])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2148,7 +2168,8 @@ function DPSMate.DB:DeathHistory(target, cause, ability, amount, hit, crit, type
|
||||
cause = self:BuildUser(cause)
|
||||
ability = self:BuildAbility(ability)
|
||||
local hitCritCrush
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do
|
||||
local val = tablemodes[cat]
|
||||
if not DPSDeath[cat] then DPSDeath[cat] = {} end
|
||||
if not DPSDeath[cat][target] then
|
||||
DPSDeath[cat][target] = {}
|
||||
@@ -2245,7 +2266,7 @@ function DPSMate.DB:Kick(cause, target, causeAbility, targetAbility)
|
||||
cause = self:BuildUser(cause)
|
||||
causeAbility = self:BuildAbility(causeAbility)
|
||||
targetAbility = self:BuildAbility(targetAbility)
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do local val = tablemodes[cat]
|
||||
if not DPSInterrupt[cat] then DPSInterrupt[cat] = {} end
|
||||
if not DPSInterrupt[cat][cause] then
|
||||
DPSInterrupt[cat][cause] = {
|
||||
@@ -2310,7 +2331,7 @@ function DPSMate.DB:BuildBuffs(cause, target, ability, bool)
|
||||
target = self:BuildUser(target)
|
||||
cause = self:BuildUser(cause)
|
||||
ability = self:BuildAbility(ability)
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do local val = tablemodes[cat]
|
||||
if not DPSAurasGained[cat] then DPSAurasGained[cat] = {} end
|
||||
if not DPSAurasGained[cat][target] then
|
||||
DPSAurasGained[cat][target] = {}
|
||||
@@ -2347,7 +2368,7 @@ function DPSMate.DB:DestroyBuffs(target, ability)
|
||||
target = self:BuildUser(target)
|
||||
ability = self:BuildAbility(ability)
|
||||
local TL
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do local val = tablemodes[cat]
|
||||
if not DPSAurasGained[cat] then DPSAurasGained[cat] = {} end
|
||||
if not DPSAurasGained[cat][target] then
|
||||
DPSAurasGained[cat][target] = {}
|
||||
@@ -2557,7 +2578,7 @@ function DPSMate.DB:BuildFail(type, user, cause, ability, amount)
|
||||
cause = self:BuildUser(cause)
|
||||
ability = self:BuildAbility(ability)
|
||||
local time = GameTime_GT()
|
||||
for cat, val in pairs(tablemodes) do
|
||||
for cat = 1, 2 do local val = tablemodes[cat]
|
||||
if not DPSFail[cat] then DPSFail[cat] = {} end
|
||||
if not DPSFail[cat][cause] then
|
||||
DPSFail[cat][cause] = {}
|
||||
|
||||
Reference in New Issue
Block a user