347 lines
16 KiB
Lua
347 lines
16 KiB
Lua
-- AnySCT parse.lua -- pure functions, no WoW API (only an optional getglobal for live
|
|
-- GlobalStrings), so everything here runs offline under Lua 5.0.
|
|
--
|
|
-- Three entry points:
|
|
-- AnySCT_ParseMelee(msg) chat row of a melee swing -> result table or nil
|
|
-- AnySCT_ParseSpell(msg) chat row of a special attack -> result table or nil
|
|
-- AnySCT_DecodeAutoAttack nampower AUTO_ATTACK_OTHER numbers -> result table or nil
|
|
--
|
|
-- Result table: { kind, amount, blocked, absorbed, resisted, attacker, victim, spell, school }
|
|
-- kind is one of hit crit glancing crushing miss dodge parry block evade immune deflect absorb resist.
|
|
|
|
AnySCT_Parse = {}
|
|
local P = AnySCT_Parse
|
|
|
|
-- Live GlobalString if the client has one, else the enUS literal.
|
|
local function gs(name, default)
|
|
local v
|
|
if getglobal then v = getglobal(name) end
|
|
if type(v) == "string" then return v end
|
|
return default
|
|
end
|
|
|
|
-- printf-style GlobalString -> Lua 5.0 pattern (no anchors). %s -> (.-), %d -> (%d+); literal text
|
|
-- is escaped. Positional placeholders (%1$s) are read in textual order. Second result: the
|
|
-- placeholder kinds in order.
|
|
function AnySCT_GsToPattern(template)
|
|
local parts = {}
|
|
local kinds = {}
|
|
local i, len = 1, string.len(template)
|
|
while i <= len do
|
|
local c = string.sub(template, i, i)
|
|
if c == "%" then
|
|
local rest = string.sub(template, i)
|
|
local _, e, _, t = string.find(rest, "^%%(%d*)%$?([sdfg])")
|
|
if e then
|
|
if t == "s" then
|
|
table.insert(parts, "(.-)")
|
|
else
|
|
table.insert(parts, "(%d+)")
|
|
end
|
|
table.insert(kinds, t)
|
|
i = i + e
|
|
else
|
|
-- literal percent ("%%") or a stray one
|
|
local n = string.sub(template, i + 1, i + 1)
|
|
table.insert(parts, "%%")
|
|
if n == "%" then i = i + 2 else i = i + 1 end
|
|
end
|
|
else
|
|
if string.find(c, "[%(%)%.%+%-%*%?%[%]%^%$]") then
|
|
table.insert(parts, "%" .. c)
|
|
else
|
|
table.insert(parts, c)
|
|
end
|
|
i = i + 1
|
|
end
|
|
end
|
|
return table.concat(parts, ""), kinds
|
|
end
|
|
|
|
-- ---- form tables ----------------------------------------------------------------------------
|
|
-- { globalName, enUS literal, kind, field names in placeholder order }. Order matters: the more
|
|
-- specific string (CRIT / SCHOOL) must be tried before the plain one.
|
|
local F_ATT = { "attacker", "victim", "amount", "school" }
|
|
local F_ATN = { "attacker", "victim", "amount" }
|
|
local F_AV = { "attacker", "victim" }
|
|
local F_SP = { "attacker", "spell", "victim", "amount", "school" }
|
|
local F_SPN = { "attacker", "spell", "victim", "amount" }
|
|
local F_SPV = { "attacker", "spell", "victim" }
|
|
local F_A = { "attacker" }
|
|
local F_SPA = { "attacker", "spell" }
|
|
|
|
local MELEE_DEFS = {
|
|
{ "COMBATHITCRITSCHOOLOTHEROTHER", "%s crits %s for %d %s damage.", "crit", F_ATT },
|
|
{ "COMBATHITCRITOTHEROTHER", "%s crits %s for %d.", "crit", F_ATN },
|
|
{ "COMBATHITSCHOOLOTHEROTHER", "%s hits %s for %d %s damage.", "hit", F_ATT },
|
|
{ "COMBATHITOTHEROTHER", "%s hits %s for %d.", "hit", F_ATN },
|
|
{ "MISSEDOTHEROTHER", "%s misses %s.", "miss", F_AV },
|
|
{ "VSBLOCKOTHEROTHER", "%s attacks. %s blocks.", "block", F_AV },
|
|
{ "VSDODGEOTHEROTHER", "%s attacks. %s dodges.", "dodge", F_AV },
|
|
{ "VSPARRYOTHEROTHER", "%s attacks. %s parries.", "parry", F_AV },
|
|
{ "VSIMMUNEOTHEROTHER", "%s attacks but %s is immune.", "immune", F_AV },
|
|
{ "VSEVADEOTHEROTHER", "%s attacks. %s evades.", "evade", F_AV },
|
|
{ "VSDEFLECTOTHEROTHER", "%s attacks. %s deflects.", "deflect", F_AV },
|
|
{ "VSABSORBOTHEROTHER", "%s attacks. %s absorbs all the damage.", "absorb", F_AV },
|
|
{ "VSRESISTOTHEROTHER", "%s attacks. %s resists all the damage.", "resist", F_AV },
|
|
-- attacks on the player: the victim is "you" (set from the 5th entry)
|
|
{ "VSDODGEOTHERSELF", "%s attacks. You dodge.", "dodge", F_A, true },
|
|
{ "VSPARRYOTHERSELF", "%s attacks. You parry.", "parry", F_A, true },
|
|
{ "VSBLOCKOTHERSELF", "%s attacks. You block.", "block", F_A, true },
|
|
{ "VSIMMUNEOTHERSELF", "%s attacks but you are immune.", "immune", F_A, true },
|
|
{ "VSEVADEOTHERSELF", "%s attacks. You evade.", "evade", F_A, true },
|
|
{ "VSDEFLECTOTHERSELF", "%s attacks. You deflect.", "deflect", F_A, true },
|
|
{ "VSABSORBOTHERSELF", "%s attacks. You absorb all the damage.", "absorb", F_A, true },
|
|
{ "VSRESISTOTHERSELF", "%s attacks. You resist all the damage.", "resist", F_A, true },
|
|
}
|
|
|
|
local SPELL_DEFS = {
|
|
{ "SPELLLOGCRITSCHOOLOTHEROTHER", "%s's %s crits %s for %d %s damage.", "crit", F_SP },
|
|
{ "SPELLLOGCRITOTHEROTHER", "%s's %s crits %s for %d.", "crit", F_SPN },
|
|
{ "SPELLLOGSCHOOLOTHEROTHER", "%s's %s hits %s for %d %s damage.", "hit", F_SP },
|
|
{ "SPELLLOGOTHEROTHER", "%s's %s hits %s for %d.", "hit", F_SPN },
|
|
{ "SPELLLOGABSORBOTHEROTHER", "%s's %s is absorbed by %s.", "absorb", F_SPV },
|
|
{ "SPELLMISSOTHEROTHER", "%s's %s missed %s.", "miss", F_SPV },
|
|
{ "SPELLDODGEDOTHEROTHER", "%s's %s was dodged by %s.", "dodge", F_SPV },
|
|
{ "SPELLPARRIEDOTHEROTHER", "%s's %s was parried by %s.", "parry", F_SPV },
|
|
{ "SPELLBLOCKEDOTHEROTHER", "%s's %s was blocked by %s.", "block", F_SPV },
|
|
{ "SPELLRESISTOTHEROTHER", "%s's %s was resisted by %s.", "resist", F_SPV },
|
|
{ "SPELLIMMUNEOTHEROTHER", "%s's %s fails. %s is immune.", "immune", F_SPV },
|
|
{ "SPELLEVADEDOTHEROTHER", "%s's %s was evaded by %s.", "evade", F_SPV },
|
|
{ "SPELLMISSOTHERSELF", "%s's %s misses you.", "miss", F_SPA, true },
|
|
{ "SPELLDODGEDOTHERSELF", "%s's %s was dodged.", "dodge", F_SPA, true },
|
|
{ "SPELLPARRIEDOTHERSELF", "%s's %s was parried.", "parry", F_SPA, true },
|
|
{ "SPELLBLOCKEDOTHERSELF", "%s's %s was blocked.", "block", F_SPA, true },
|
|
{ "SPELLRESISTOTHERSELF", "%s's %s was resisted.", "resist", F_SPA, true },
|
|
{ "SPELLEVADEDOTHERSELF", "%s's %s was evaded.", "evade", F_SPA, true },
|
|
{ "SPELLDEFLECTEDOTHERSELF", "%s's %s was deflected.", "deflect", F_SPA, true },
|
|
{ "SPELLIMMUNEOTHERSELF", "%s's %s failed. You are immune.", "immune", F_SPA, true },
|
|
{ "SPELLLOGABSORBOTHERSELF", "You absorb %s's %s.", "absorb", F_SPA, true },
|
|
}
|
|
|
|
-- Heal rows (CHAT_MSG_SPELL_*_BUFF and the PERIODIC_*_BUFFS channels). Result: kind "heal", crit,
|
|
-- periodic, attacker = healer ("you" for the player), victim, spell, amount.
|
|
local F_HEAL = { "attacker", "spell", "victim", "amount" }
|
|
local F_HEALS = { "spell", "victim", "amount" } -- "Your Spell heals Victim for N."
|
|
local F_HOT = { "victim", "amount", "attacker", "spell" }
|
|
local F_HOTS = { "victim", "amount", "spell" } -- "Victim gains N health from your Spell."
|
|
local HEAL_DEFS = {
|
|
{ "HEALEDCRITOTHEROTHER", "%s's %s critically heals %s for %d.", "healcrit", F_HEAL },
|
|
{ "HEALEDOTHEROTHER", "%s's %s heals %s for %d.", "heal", F_HEAL },
|
|
{ "HEALEDCRITSELFOTHER", "Your %s critically heals %s for %d.", "healcrit", F_HEALS, nil, true },
|
|
{ "HEALEDSELFOTHER", "Your %s heals %s for %d.", "heal", F_HEALS, nil, true },
|
|
{ "PERIODICAURAHEALOTHEROTHER", "%s gains %d health from %s's %s.", "hot", F_HOT },
|
|
{ "PERIODICAURAHEALSELFOTHER", "%s gains %d health from your %s.", "hot", F_HOTS, nil, true },
|
|
}
|
|
|
|
-- trailers appended to the end of a row; the third entry names what they set on the result
|
|
local TRAILER_DEFS = {
|
|
{ "ABSORB_TRAILER", " (%d absorbed)", "absorbed", {} },
|
|
{ "BLOCK_TRAILER", " (%d blocked)", "blocked", {} },
|
|
{ "RESIST_TRAILER", " (%d resisted)", "resisted", {} },
|
|
{ "GLANCING_TRAILER", " (glancing)", "glancing", {} },
|
|
{ "CRUSHING_TRAILER", " (crushing)", "crushing", {} },
|
|
}
|
|
|
|
local meleeForms, spellForms, healForms, trailers
|
|
|
|
local function compile(defs, anchored)
|
|
local out = {}
|
|
for i = 1, table.getn(defs) do
|
|
local d = defs[i]
|
|
local pat = AnySCT_GsToPattern(gs(d[1], d[2]))
|
|
if anchored then pat = "^" .. pat .. "$" else pat = pat .. "$" end
|
|
out[i] = { pat = pat, kind = d[3], key = d[3], fields = d[4], nf = table.getn(d[4]), selfVictim = d[5],
|
|
selfAttacker = d[6] }
|
|
end
|
|
return out
|
|
end
|
|
|
|
-- Rebuild all compiled patterns (call again if the GlobalStrings change, e.g. in tests).
|
|
function AnySCT_Parse_Rebuild()
|
|
meleeForms = compile(MELEE_DEFS, true)
|
|
spellForms = compile(SPELL_DEFS, true)
|
|
healForms = compile(HEAL_DEFS, true)
|
|
trailers = compile(TRAILER_DEFS, false)
|
|
end
|
|
|
|
-- Strip trailers from the end of msg (repeatedly: they can stack). Returns the stripped message
|
|
-- and an info table { absorbed=, blocked=, resisted=, glancing=true, crushing=true }.
|
|
local info = {} -- reused: one parse at a time, read before the next call
|
|
local caps = {} -- reused capture buffer
|
|
|
|
local function stripTrailers(msg)
|
|
if not trailers then AnySCT_Parse_Rebuild() end
|
|
info.absorbed, info.blocked, info.resisted, info.glancing, info.crushing = nil, nil, nil, nil, nil
|
|
local changed = true
|
|
while changed do
|
|
changed = false
|
|
for i = 1, table.getn(trailers) do
|
|
local t = trailers[i]
|
|
local s, _, n = string.find(msg, t.pat)
|
|
if s then
|
|
msg = string.sub(msg, 1, s - 1)
|
|
if n then info[t.key] = (info[t.key] or 0) + tonumber(n) else info[t.key] = true end
|
|
changed = true
|
|
end
|
|
end
|
|
end
|
|
return msg, info
|
|
end
|
|
AnySCT_StripTrailers = stripTrailers
|
|
|
|
local function matchForms(forms, msg)
|
|
for i = 1, table.getn(forms) do
|
|
local f = forms[i]
|
|
local s, _, c1, c2, c3, c4, c5 = string.find(msg, f.pat)
|
|
if s then
|
|
caps[1], caps[2], caps[3], caps[4], caps[5] = c1, c2, c3, c4, c5
|
|
local r = { kind = f.kind }
|
|
for j = 1, f.nf do r[f.fields[j]] = caps[j] end
|
|
if r.amount then r.amount = tonumber(r.amount) end
|
|
if f.selfVictim then r.victim = "you" end
|
|
if f.selfAttacker then r.attacker = "you" end
|
|
return r
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- Fold trailer info into the result. Same precedence as the auto-attack decoder:
|
|
-- crit > crushing > glancing > block > hit. A plain "hit" is promoted; a crit stays a crit.
|
|
local function applyMods(r, info)
|
|
r.absorbed = (type(info.absorbed) == "number") and info.absorbed or 0
|
|
r.blocked = (type(info.blocked) == "number") and info.blocked or 0
|
|
r.resisted = (type(info.resisted) == "number") and info.resisted or 0
|
|
if not r.amount then r.amount = 0 end
|
|
if r.kind == "hit" then
|
|
if info.crushing then r.kind = "crushing"
|
|
elseif info.glancing then r.kind = "glancing"
|
|
elseif r.blocked > 0 then r.kind = "block" end
|
|
end
|
|
return r
|
|
end
|
|
|
|
function AnySCT_ParseMelee(msg)
|
|
if type(msg) ~= "string" then return nil end
|
|
if not meleeForms then AnySCT_Parse_Rebuild() end
|
|
local body, info = stripTrailers(msg)
|
|
local r = matchForms(meleeForms, body)
|
|
if not r then return nil end
|
|
-- "Tank's Heroic Strike hits Boss for 5." also fits the melee shape; that is a spell row
|
|
if string.find(r.attacker, "'s ", 1, true) then return nil end
|
|
return applyMods(r, info)
|
|
end
|
|
|
|
function AnySCT_ParseSpell(msg)
|
|
if type(msg) ~= "string" then return nil end
|
|
if not spellForms then AnySCT_Parse_Rebuild() end
|
|
local body, info = stripTrailers(msg)
|
|
local r = matchForms(spellForms, body)
|
|
if not r then return nil end
|
|
return applyMods(r, info)
|
|
end
|
|
|
|
function AnySCT_ParseHeal(msg)
|
|
if type(msg) ~= "string" then return nil end
|
|
if not healForms then AnySCT_Parse_Rebuild() end
|
|
local r = matchForms(healForms, msg)
|
|
if not r then return nil end
|
|
if r.kind == "healcrit" then r.kind = "heal"; r.crit = true
|
|
elseif r.kind == "hot" then r.kind = "heal"; r.periodic = true end
|
|
r.absorbed, r.blocked, r.resisted = 0, 0, 0
|
|
return r
|
|
end
|
|
|
|
-- nampower SPELL_HEAL_BY_OTHER: the crit flag is not documented for this client, so heals decode as
|
|
-- non-crit unless the caller passes an explicit crit argument (true or 1).
|
|
function AnySCT_DecodeHeal(amount, crit)
|
|
local r = { kind = "heal", amount = tonumber(amount) or 0, absorbed = 0, blocked = 0, resisted = 0 }
|
|
if crit == true or crit == 1 then r.crit = true end
|
|
return r
|
|
end
|
|
|
|
-- ---- nampower AUTO_ATTACK_OTHER decoder ---------------------------------------------------------
|
|
-- hitInfo flags (server UnitDefines.h): MISS 0x10, ABSORB 0x20, RESIST 0x40, CRITICALHIT 0x80,
|
|
-- GLANCING 0x4000, CRUSHING 0x8000. Lua 5.0 has no bit ops, so test a flag with floor and mod.
|
|
local function bitset(v, flag)
|
|
return math.mod(math.floor(v / flag), 2) == 1
|
|
end
|
|
P.bitset = bitset
|
|
|
|
local F_MISS, F_ABSORB, F_RESIST, F_CRIT, F_GLANCING, F_CRUSHING = 16, 32, 64, 128, 16384, 32768
|
|
|
|
-- victimState -> kind for the states that mean "no damage dealt"
|
|
local STATE_KIND = { [2] = "dodge", [3] = "parry", [6] = "evade", [7] = "immune", [8] = "deflect" }
|
|
|
|
-- victimState 0 unaffected, 1 normal, 2 dodge, 3 parry, 4 interrupt (ignored -> nil), 5 block
|
|
-- (partial: damage goes through), 6 evade, 7 immune, 8 deflect.
|
|
-- Precedence for damaging hits: crit > crushing > glancing > block > hit.
|
|
function AnySCT_DecodeAutoAttack(damage, hitInfo, victimState, blocked, absorbed, resisted)
|
|
damage = tonumber(damage) or 0
|
|
hitInfo = tonumber(hitInfo) or 0
|
|
victimState = tonumber(victimState) or 1
|
|
blocked = tonumber(blocked) or 0
|
|
absorbed = tonumber(absorbed) or 0
|
|
resisted = tonumber(resisted) or 0
|
|
local r = { kind = "hit", amount = damage, blocked = blocked, absorbed = absorbed, resisted = resisted }
|
|
|
|
if victimState == 4 then return nil end
|
|
local sk = STATE_KIND[victimState]
|
|
if sk then
|
|
r.kind = sk
|
|
r.amount = 0
|
|
return r
|
|
end
|
|
if bitset(hitInfo, F_MISS) then
|
|
r.kind = "miss"
|
|
r.amount = 0
|
|
return r
|
|
end
|
|
if damage <= 0 then
|
|
if absorbed > 0 or bitset(hitInfo, F_ABSORB) then r.kind = "absorb"
|
|
elseif resisted > 0 or bitset(hitInfo, F_RESIST) then r.kind = "resist"
|
|
elseif victimState == 5 then r.kind = "block"
|
|
elseif victimState == 0 then r.kind = "miss"
|
|
end
|
|
return r
|
|
end
|
|
if bitset(hitInfo, F_CRIT) then r.kind = "crit"
|
|
elseif bitset(hitInfo, F_CRUSHING) then r.kind = "crushing"
|
|
elseif bitset(hitInfo, F_GLANCING) then r.kind = "glancing"
|
|
elseif victimState == 5 then r.kind = "block"
|
|
end
|
|
return r
|
|
end
|
|
|
|
-- ---- nampower spell events ---------------------------------------------------------------------
|
|
-- SPELL_DAMAGE_EVENT_OTHER: amount, mitigation "absorb,block,resist" (comma separated string),
|
|
-- hitInfo (SpellHitType, crit = 0x02). Argument layout as documented for nampower; spell hits seen in game with nampower 4.6.2.
|
|
function AnySCT_DecodeSpellDamage(amount, mitigation, hitInfo)
|
|
amount = tonumber(amount) or 0
|
|
hitInfo = tonumber(hitInfo) or 0
|
|
local absorbed, blocked, resisted = 0, 0, 0
|
|
if type(mitigation) == "string" then
|
|
local _, _, a, b, r = string.find(mitigation, "^%s*(-?%d*)%s*,%s*(-?%d*)%s*,%s*(-?%d*)%s*$")
|
|
absorbed, blocked, resisted = tonumber(a) or 0, tonumber(b) or 0, tonumber(r) or 0
|
|
end
|
|
local r = { kind = "hit", amount = amount, absorbed = absorbed, blocked = blocked, resisted = resisted }
|
|
if amount <= 0 then
|
|
if absorbed > 0 then r.kind = "absorb"
|
|
elseif resisted > 0 then r.kind = "resist"
|
|
elseif blocked > 0 then r.kind = "block" end
|
|
elseif bitset(hitInfo, 2) then
|
|
r.kind = "crit"
|
|
end
|
|
return r
|
|
end
|
|
|
|
-- SPELL_MISS_OTHER missType: the vanilla SpellMissInfo enum (unverified for this client).
|
|
local MISS_KIND = { [1] = "miss", [2] = "resist", [3] = "dodge", [4] = "parry", [5] = "block",
|
|
[6] = "evade", [7] = "immune", [8] = "immune", [9] = "deflect", [10] = "absorb" }
|
|
function AnySCT_DecodeSpellMiss(missType)
|
|
local k = MISS_KIND[tonumber(missType) or 0]
|
|
if not k then return nil end
|
|
return { kind = k, amount = 0, absorbed = 0, blocked = 0, resisted = 0 }
|
|
end
|