Files
WeirdUtils/src/dpslog/WeirdDPSMate/DPSMate_CombatPatterns.lua
T
MarcelineVQ 86da3bcc03 dpslog: DPSMate CLEU adapter with per-combat A/B profiling
Adds DPSMate_CLEUAdapter.lua -- replaces DPSMate's string-parsing
CHAT_MSG_* system with structured COMBAT_LOG_EVENT data from our DLL.

Adapter maps CLEU subevents directly to DPSMate.DB API calls:
DamageDone, DamageTaken, EnemyDamage, Healing, HealingTaken,
DeathHistory, Kick, Dispels, BuildBuffs, CCBreaker, etc.
Eliminates all strfind pattern matching from the combat log path.

Toggle: /dpscleu (on/off/status)
Benchmark: /dpsbench -- always-on per-combat A/B profiling.
  Resets at combat start, reports at combat end, flips mode for next
  combat. Reports events, total ms, us/event, GC delta, and percentage
  comparison between CLEU and original modes.

Starts in CLEU mode by default.
2026-03-18 01:12:04 -07:00

368 lines
14 KiB
Lua

-- DPSMate_CombatPatterns.lua
-- Pattern-based combat log matching using WoW global format strings.
-- Automatically adapts to server-specific combat log format changes (e.g., TWoW).
-- Field mappings are auto-detected from format string context, not hardcoded.
DPSMate.CombatPatterns = {}
local CP = DPSMate.CombatPatterns
local strfind = string.find
local strsub = string.sub
local tonumber = tonumber
local tinsert = table.insert
-- Convert a WoW format string (e.g., "You hit %s for %d.") to a Lua pattern
-- e.g., "You hit %s for %d." -> "^You hit (.+) for (%d+)%.$"
function CP:Sanitize(str)
if not str then return nil end
-- Replace format specifiers with placeholders before escaping
str = string.gsub(str, "%%s", "\001")
str = string.gsub(str, "%%d", "\002")
-- Escape Lua pattern magic characters
str = string.gsub(str, "([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1")
-- Replace placeholders with capture groups
str = string.gsub(str, "\001", "(.+)")
str = string.gsub(str, "\002", "(%d+)")
return "^" .. str .. "$"
end
-- Strip combat log trailers: (X absorbed), (blocked), (glancing), (crushing), (X resisted)
-- Returns: cleaned_msg, absorbed_amount, blocked(0/1), glancing(bool), crushing(bool)
function CP:StripTrailers(msg)
local absorbed, blocked, glancing, crushing = 0, 0, false, false
local i, j, amt
i, j, amt = strfind(msg, " %((%d+) absorbed%)")
if i then absorbed = tonumber(amt); msg = strsub(msg, 1, i-1) .. strsub(msg, j+1) end
i, j, amt = strfind(msg, " %((%d+) blocked%)")
if i then blocked = 1; msg = strsub(msg, 1, i-1) .. strsub(msg, j+1) end
if blocked == 0 then
i, j = strfind(msg, " %(blocked%)")
if i then blocked = 1; msg = strsub(msg, 1, i-1) .. strsub(msg, j+1) end
end
i, j = strfind(msg, " %(glancing%)")
if i then glancing = true; msg = strsub(msg, 1, i-1) .. strsub(msg, j+1) end
i, j = strfind(msg, " %(crushing%)")
if i then crushing = true; msg = strsub(msg, 1, i-1) .. strsub(msg, j+1) end
-- Strip resisted trailer to prevent pattern mismatch
i, j = strfind(msg, " %((%d+) resisted%)")
if i then msg = strsub(msg, 1, i-1) .. strsub(msg, j+1) end
return msg, absorbed, blocked, glancing, crushing
end
-- Try matching a message against a list of pattern entries
-- Each entry: {pattern_string, hitType_string, {field=captureIndex, ...}}
-- Returns: hitType, result_table (with named fields) on match, or nil
function CP:TryMatch(msg, patterns)
if not patterns then return nil end
for _, entry in ipairs(patterns) do
local pattern = entry[1]
if pattern then
local c = {strfind(msg, pattern)}
if c[1] then
local result = {}
local fields = entry[3]
for field, idx in pairs(fields) do
result[field] = c[idx + 2] -- +2 to skip start/end positions
end
if result.amount then result.amount = tonumber(result.amount) end
return entry[2], result
end
end
end
return nil
end
-- Auto-detect field names for each format specifier based on surrounding text context.
-- Returns a table mapping field_name -> capture_index, or nil if format is empty/invalid.
-- overrides: optional table mapping detected_name -> desired_name (e.g., {target="source"})
function CP:DetectFields(fmt, overrides)
if not fmt then return nil end
-- Parse specifiers and the text segments between them
local specs = {}
local n = 0
local pos = 1
while true do
local i, j, specType = strfind(fmt, "%%([sd])", pos)
if not i then break end
n = n + 1
specs[n] = {
type = specType,
before = strsub(fmt, pos, i - 1),
}
pos = j + 1
end
if n == 0 then return nil end
-- Set "after" text for each spec (text between this spec and the next, or trailing)
for i = 1, n - 1 do
specs[i].after = specs[i + 1].before
end
specs[n].after = strsub(fmt, pos)
-- Detect field name for each specifier based on surrounding text
local fields = {}
for i = 1, n do
local s = specs[i]
local field
if s.type == "d" then
field = "amount"
else
local before = s.before
local after = s.after
-- Rule 1: After "Your/your " -> ability (e.g., "Your %s hits")
if strfind(before, "Your ?$") or strfind(before, "your ?$") then
field = "ability"
-- Rule 2: After "'s " -> ability (e.g., "Source's %s hits")
elseif strfind(before, "'s ?$") or strfind(before, "' s ?$") then
field = "ability"
-- Rule 3: Before "'s" -> source (e.g., "%s's Ability", "%s 's Ability")
elseif strfind(after, "^'s") or strfind(after, "^' s") or strfind(after, "^ 's") then
field = "source"
-- Rule 4: Before " damage" -> school (e.g., "%d %s damage")
elseif strfind(after, "^ damage") then
field = "school"
-- Rule 5: Before " suffers" -> target (e.g., "%s suffers %d")
elseif strfind(after, "^ suffers") then
field = "target"
-- Rule 6: Before action verbs -> source (e.g., "%s hits", "%s critically hits")
elseif strfind(after, "^ hits") or strfind(after, "^ crits") or
strfind(after, "^ misses") or strfind(after, "^ attacks") or
strfind(after, "^ reflects") or strfind(after, "^ critically") then
field = "source"
-- Rule 7: Before reaction verbs -> target (e.g., "%s parries", "%s is immune")
elseif strfind(after, "^ parries") or strfind(after, "^ dodges") or
strfind(after, "^ blocks") or strfind(after, "^ absorbs") or
strfind(after, "^ is immune") then
field = "target"
-- Rule 8: Before " for " -> target (e.g., "hits %s for %d")
elseif strfind(after, "^ for ") then
field = "target"
-- Rule 9: After " to " -> target (e.g., "damage to %s")
elseif strfind(before, " to ?$") then
field = "target"
-- Rule 10: After " by " -> target (e.g., "dodged by %s")
elseif strfind(before, " by ?$") then
field = "target"
-- Rule 11: After " from " -> source (e.g., "damage from %s")
elseif strfind(before, " from ?$") then
field = "source"
-- Rule 12: "You <verb> %s" -> target (e.g., "You miss %s")
elseif strfind(before, "^You %a+ ?$") then
field = "target"
-- Rule 13: End of message -> target (fallback for last %s)
elseif strfind(after, "^%.?$") then
field = "target"
else
field = "target"
end
end
-- Apply overrides (e.g., env damage: detected "target" -> call it "source")
if overrides and overrides[field] then
field = overrides[field]
end
fields[field] = i
end
return fields
end
-- Flag indicating patterns have been built
CP.ready = false
-- Helper: add a pattern entry from a WoW global string name.
-- Field mappings are auto-detected from format string content.
-- overrides: optional table mapping detected_name -> desired_name
local function addP(tbl, globalName, hitType, overrides)
local str = getglobal(globalName)
if str then
local fields = CP:DetectFields(str, overrides)
if fields then
tinsert(tbl, {CP:Sanitize(str), hitType, fields})
end
end
end
-- Build all patterns from WoW globals.
-- MUST be called AFTER InitParser modifies the globals (adds space before 's).
function CP:BuildPatterns()
-- =================== SELF MELEE HITS ===================
-- "You hit Target for Amount [School damage]."
self.selfMeleeHit = {}
addP(self.selfMeleeHit, "COMBATHITCRITSCHOOLSELFOTHER", "crit")
addP(self.selfMeleeHit, "COMBATHITSCHOOLSELFOTHER", "hit")
addP(self.selfMeleeHit, "COMBATHITCRITSELFOTHER", "crit")
addP(self.selfMeleeHit, "COMBATHITSELFOTHER", "hit")
-- =================== OTHER MELEE HITS ===================
-- "Source hits Target for Amount [School damage]."
self.otherMeleeHit = {}
addP(self.otherMeleeHit, "COMBATHITCRITSCHOOLOTHEROTHER", "crit")
addP(self.otherMeleeHit, "COMBATHITSCHOOLOTHEROTHER", "hit")
addP(self.otherMeleeHit, "COMBATHITCRITOTHEROTHER", "crit")
addP(self.otherMeleeHit, "COMBATHITOTHEROTHER", "hit")
-- =================== SELF SPELL HITS ===================
-- "Your Ability hits Target for Amount [School damage]."
self.selfSpellHit = {}
addP(self.selfSpellHit, "SPELLLOGCRITSCHOOLSELFOTHER", "crit")
addP(self.selfSpellHit, "SPELLLOGSCHOOLSELFOTHER", "hit")
addP(self.selfSpellHit, "SPELLLOGCRITSELFOTHER", "crit")
addP(self.selfSpellHit, "SPELLLOGSELFOTHER", "hit")
addP(self.selfSpellHit, "SPELLLOGCRITSCHOOLSELFSELF", "crit")
addP(self.selfSpellHit, "SPELLLOGSCHOOLSELFSELF", "hit")
addP(self.selfSpellHit, "SPELLLOGCRITSELFSELF", "crit")
addP(self.selfSpellHit, "SPELLLOGSELFSELF", "hit")
-- =================== OTHER SPELL HITS ===================
-- "Source's Ability hits Target for Amount [School damage]."
self.otherSpellHit = {}
addP(self.otherSpellHit, "SPELLLOGCRITSCHOOLOTHEROTHER", "crit")
addP(self.otherSpellHit, "SPELLLOGSCHOOLOTHEROTHER", "hit")
addP(self.otherSpellHit, "SPELLLOGCRITOTHEROTHER", "crit")
addP(self.otherSpellHit, "SPELLLOGOTHEROTHER", "hit")
-- =================== PERIODIC DAMAGE ===================
-- "Target suffers Amount School damage from Source's Ability."
self.periodicDmg = {}
addP(self.periodicDmg, "PERIODICAURADAMAGEOTHEROTHER", "dot")
addP(self.periodicDmg, "PERIODICAURADAMAGESELFOTHER", "dot")
addP(self.periodicDmg, "PERIODICAURADAMAGEOTHERSELF", "dot")
addP(self.periodicDmg, "PERIODICAURADAMAGESELFSELF", "dot")
-- =================== DAMAGE SHIELDS ===================
self.dmgShield = {}
addP(self.dmgShield, "DAMAGESHIELDSELFOTHER", "shield")
addP(self.dmgShield, "DAMAGESHIELDOTHERSELF", "shield")
addP(self.dmgShield, "DAMAGESHIELDOTHEROTHER", "shield")
-- =================== ENVIRONMENTAL DAMAGE ===================
-- Entity "suffering" env damage is called "source" in the data model
local envOverride = {target = "source"}
self.envDmgSelf = {}
addP(self.envDmgSelf, "VSENVIRONMENTALDAMAGE_FALLING_SELF", "falling")
addP(self.envDmgSelf, "VSENVIRONMENTALDAMAGE_DROWNING_SELF", "drowning")
addP(self.envDmgSelf, "VSENVIRONMENTALDAMAGE_LAVA_SELF", "lava")
addP(self.envDmgSelf, "VSENVIRONMENTALDAMAGE_SLIME_SELF", "slime")
addP(self.envDmgSelf, "VSENVIRONMENTALDAMAGE_FIRE_SELF", "fire")
addP(self.envDmgSelf, "VSENVIRONMENTALDAMAGE_FATIGUE_SELF", "fatigue")
self.envDmgOther = {}
addP(self.envDmgOther, "VSENVIRONMENTALDAMAGE_FALLING_OTHER", "falling", envOverride)
addP(self.envDmgOther, "VSENVIRONMENTALDAMAGE_DROWNING_OTHER", "drowning", envOverride)
addP(self.envDmgOther, "VSENVIRONMENTALDAMAGE_LAVA_OTHER", "lava", envOverride)
addP(self.envDmgOther, "VSENVIRONMENTALDAMAGE_SLIME_OTHER", "slime", envOverride)
addP(self.envDmgOther, "VSENVIRONMENTALDAMAGE_FIRE_OTHER", "fire", envOverride)
addP(self.envDmgOther, "VSENVIRONMENTALDAMAGE_FATIGUE_OTHER", "fatigue", envOverride)
-- =================== SELF SPELL MISSES ===================
self.selfSpellMiss = {}
addP(self.selfSpellMiss, "SPELLMISSSELFOTHER", "miss")
addP(self.selfSpellMiss, "SPELLRESISTSELFOTHER", "resist")
addP(self.selfSpellMiss, "SPELLPARRIEDSELFOTHER", "parry")
addP(self.selfSpellMiss, "SPELLDODGEDSELFOTHER", "dodge")
addP(self.selfSpellMiss, "SPELLLOGABSORBSELFOTHER", "absorb")
addP(self.selfSpellMiss, "SPELLBLOCKEDSELFOTHER", "block")
addP(self.selfSpellMiss, "SPELLIMMUNESELFOTHER", "immune")
-- =================== OTHER SPELL MISSES ===================
self.otherSpellMiss = {}
addP(self.otherSpellMiss, "SPELLMISSOTHEROTHER", "miss")
addP(self.otherSpellMiss, "SPELLRESISTOTHEROTHER", "resist")
addP(self.otherSpellMiss, "SPELLPARRIEDOTHEROTHER", "parry")
addP(self.otherSpellMiss, "SPELLDODGEDOTHEROTHER", "dodge")
addP(self.otherSpellMiss, "SPELLLOGABSORBOTHEROTHER", "absorb")
addP(self.otherSpellMiss, "SPELLBLOCKEDOTHEROTHER", "block")
addP(self.otherSpellMiss, "SPELLIMMUNEOTHEROTHER", "immune")
addP(self.otherSpellMiss, "SPELLEVADEDOTHEROTHER", "evade")
-- =================== SELF MELEE MISSES ===================
self.selfMeleeMiss = {}
addP(self.selfMeleeMiss, "MISSEDSELFOTHER", "miss")
addP(self.selfMeleeMiss, "VSPARRYSELFOTHER", "parry")
addP(self.selfMeleeMiss, "VSDODGESELFOTHER", "dodge")
addP(self.selfMeleeMiss, "VSBLOCKSELFOTHER", "block")
addP(self.selfMeleeMiss, "VSABSORBSELFOTHER", "absorb")
-- =================== OTHER MELEE MISSES ===================
self.otherMeleeMiss = {}
addP(self.otherMeleeMiss, "MISSEDOTHEROTHER", "miss")
addP(self.otherMeleeMiss, "VSPARRYOTHEROTHER", "parry")
addP(self.otherMeleeMiss, "VSDODGEOTHEROTHER", "dodge")
addP(self.otherMeleeMiss, "VSBLOCKOTHEROTHER", "block")
addP(self.otherMeleeMiss, "VSABSORBOTHEROTHER", "absorb")
self.ready = true
end
-- Debug: dump all detected patterns and their field mappings to chat.
-- Usage in-game: /script DPSMate.CombatPatterns:DebugDump()
function CP:DebugDump()
local function dump(name, tbl)
if not tbl or not tbl[1] then return end
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00=== " .. name .. " ===|r")
for i, entry in ipairs(tbl) do
local hitType = entry[2] or "nil"
local fields = entry[3]
local fieldStr = ""
if fields then
for k, v in pairs(fields) do
if fieldStr ~= "" then fieldStr = fieldStr .. ", " end
fieldStr = fieldStr .. k .. "=" .. v
end
end
DEFAULT_CHAT_FRAME:AddMessage(" " .. hitType .. ": {" .. fieldStr .. "}")
end
end
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00--- CombatPatterns Debug Dump ---|r")
dump("selfMeleeHit", self.selfMeleeHit)
dump("otherMeleeHit", self.otherMeleeHit)
dump("selfSpellHit", self.selfSpellHit)
dump("otherSpellHit", self.otherSpellHit)
dump("periodicDmg", self.periodicDmg)
dump("dmgShield", self.dmgShield)
dump("envDmgSelf", self.envDmgSelf)
dump("envDmgOther", self.envDmgOther)
dump("selfSpellMiss", self.selfSpellMiss)
dump("otherSpellMiss", self.otherSpellMiss)
dump("selfMeleeMiss", self.selfMeleeMiss)
dump("otherMeleeMiss", self.otherMeleeMiss)
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00CP.ready = " .. tostring(self.ready) .. "|r")
end
-- Debug: test auto-detection on a specific WoW global and show the result.
-- Usage: /script DPSMate.CombatPatterns:DebugGlobal("PERIODICAURADAMAGEOTHERSELF")
function CP:DebugGlobal(globalName, overrides)
local str = getglobal(globalName)
if not str then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFF0000" .. globalName .. " = nil|r")
return
end
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00" .. globalName .. "|r = \"" .. str .. "\"")
local fields = self:DetectFields(str, overrides)
if fields then
local fieldStr = ""
for k, v in pairs(fields) do
if fieldStr ~= "" then fieldStr = fieldStr .. ", " end
fieldStr = fieldStr .. k .. "=" .. v
end
DEFAULT_CHAT_FRAME:AddMessage(" Fields: {" .. fieldStr .. "}")
else
DEFAULT_CHAT_FRAME:AddMessage(" |cFFFF0000No fields detected|r")
end
DEFAULT_CHAT_FRAME:AddMessage(" Pattern: " .. (self:Sanitize(str) or "nil"))
end