341 lines
12 KiB
Lua
341 lines
12 KiB
Lua
-- Native data sources: structured combat events from client mods instead of
|
|
-- matching localized text patterns against every combat log chat line.
|
|
--
|
|
-- ClassicAPI COMBAT_LOG_EVENT_UNFILTERED with GUIDs and unit flags
|
|
-- Nampower SPELL_DAMAGE_EVENT_*, AUTO_ATTACK_*, SPELL_HEAL_BY_* events
|
|
-- (needs GUID unit tokens from SuperWoW or ClassicAPI)
|
|
--
|
|
-- The chat parser in parser-vanilla.lua stays as the fallback. The source in
|
|
-- use is picked by parser:SelectSource() once the settings are loaded.
|
|
|
|
local parser = ShaguDPS.parser
|
|
local config = ShaguDPS.config
|
|
|
|
local autohit = "Auto Hit"
|
|
|
|
-- school names as the chat combat log prints them, by Nampower's school index
|
|
local school_index = {
|
|
[0] = "Physical", [1] = "Holy", [2] = "Fire", [3] = "Nature",
|
|
[4] = "Frost", [5] = "Shadow", [6] = "Arcane",
|
|
}
|
|
|
|
-- school names by the school bit mask the combat log events use
|
|
local school_mask = {
|
|
[1] = "Physical", [2] = "Holy", [4] = "Fire", [8] = "Nature",
|
|
[16] = "Frost", [32] = "Shadow", [64] = "Arcane",
|
|
}
|
|
|
|
-- "Reflect (Fire)" etc, built once
|
|
local reflect = {}
|
|
for _, school in pairs(school_index) do reflect[school] = "Reflect (" .. school .. ")" end
|
|
|
|
-- Lua 5.0 has no bit operators: test a single bit by arithmetic
|
|
local function hasbit(value, bit)
|
|
return math.mod(math.floor(value / bit), 2) == 1
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- unit helpers
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- GUID of a unit token, from whichever client mod provides one
|
|
local function GUIDOf(unit)
|
|
if UnitGUID then return UnitGUID(unit) end
|
|
if GetUnitGUID then return GetUnitGUID(unit) end
|
|
local _, guid = UnitExists(unit) -- SuperWoW
|
|
return guid
|
|
end
|
|
|
|
-- true when "0x..." GUID strings work as unit tokens (SuperWoW or ClassicAPI)
|
|
local guid_tokens
|
|
local function GUIDTokensWork()
|
|
if guid_tokens == nil then
|
|
local guid = GUIDOf("player")
|
|
local name = UnitName("player")
|
|
guid_tokens = (type(guid) == "string" and name and UnitName(guid) == name) and true or false
|
|
end
|
|
return guid_tokens
|
|
end
|
|
|
|
-- name of the player owning a pet, guardian or totem
|
|
local function OwnerName(guid)
|
|
if UnitOwnerGUID then
|
|
local owner = UnitOwnerGUID(guid)
|
|
if owner and UnitIsPlayer(owner) then return UnitName(owner) end
|
|
return nil
|
|
end
|
|
|
|
-- SuperWoW: "<guid>owner" is the owner's unit token
|
|
local owner = guid .. "owner"
|
|
if UnitExists(owner) and UnitIsPlayer(owner) then return UnitName(owner) end
|
|
end
|
|
|
|
-- Names of group members and their pets by GUID. Pets use the "Pet (Owner)"
|
|
-- format of the SuperWoW combat log, so the core's pet detection and merging
|
|
-- work the same for every source. Units that aren't tracked are cached as
|
|
-- false, so the expensive name scans of the core run once per unit and not on
|
|
-- every hit. Entries expire so group changes are picked up.
|
|
local cache, cache_size, cache_trackall = {}, 0, nil
|
|
|
|
local function CacheGet(guid, flags)
|
|
-- toggling "track all units" changes what is tracked
|
|
if cache_trackall ~= config.track_all_units then
|
|
cache, cache_size, cache_trackall = {}, 0, config.track_all_units
|
|
end
|
|
|
|
local entry = cache[guid]
|
|
if entry and entry.expire > GetTime() and (not flags or entry.flags == flags) then return true, entry.name end
|
|
end
|
|
|
|
local function CacheSet(guid, name, lifetime, flags)
|
|
if cache_size > 2000 then cache, cache_size = {}, 0 end
|
|
if not cache[guid] then cache_size = cache_size + 1 end
|
|
cache[guid] = cache[guid] or {}
|
|
cache[guid].flags = flags
|
|
cache[guid].name = name
|
|
cache[guid].expire = GetTime() + lifetime
|
|
return name
|
|
end
|
|
|
|
-- is this a player of the group (or anyone when tracking all units)?
|
|
local function Tracked(name)
|
|
if not name or name == UNKNOWNOBJECT or name == UNKNOWN then return nil end
|
|
if config.track_all_units == 1 then return true end
|
|
return parser:ScanName(name) == "PLAYER"
|
|
end
|
|
|
|
-- label of a pet/guardian: "Pet (Owner)" or nil when the owner isn't tracked
|
|
local function PetLabel(guid, name, owner)
|
|
name = name or UnitName(guid)
|
|
owner = owner or OwnerName(guid)
|
|
if name and owner and Tracked(owner) then
|
|
return name .. " (" .. owner .. ")"
|
|
end
|
|
end
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- ClassicAPI: COMBAT_LOG_EVENT_UNFILTERED
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- COMBATLOG_OBJECT_* bits: affiliation 1 mine, 2 party, 4 raid; unit types below
|
|
local TYPE_PLAYER, TYPE_PET, TYPE_GUARDIAN = 1024, 4096, 8192
|
|
|
|
local function CLEUSource(guid, name, flags)
|
|
if not guid or not name or not flags or flags < 0 then return nil end
|
|
|
|
-- Event flags are current even when the GUID's cached owner is not. A
|
|
-- control/reaction/type change must invalidate both positive and negative hits.
|
|
local found, label = CacheGet(guid, flags)
|
|
if found then return label end
|
|
|
|
local grouped = hasbit(flags, 1) or hasbit(flags, 2) or hasbit(flags, 4)
|
|
|
|
if hasbit(flags, TYPE_PLAYER) then
|
|
if grouped and Tracked(name) then
|
|
return CacheSet(guid, name, 60, flags)
|
|
elseif config.track_all_units == 1 then
|
|
return CacheSet(guid, name, 60, flags)
|
|
end
|
|
return CacheSet(guid, false, 10, flags)
|
|
end
|
|
|
|
if (hasbit(flags, TYPE_PET) or hasbit(flags, TYPE_GUARDIAN)) and not hasbit(flags, 64) then
|
|
local owner = hasbit(flags, 1) and UnitName("player") or nil
|
|
local label = PetLabel(guid, name, owner)
|
|
if label then return CacheSet(guid, label, 30, flags) end
|
|
end
|
|
|
|
if config.track_all_units == 1 and name ~= UNKNOWNOBJECT then
|
|
return CacheSet(guid, name, 30, flags)
|
|
end
|
|
|
|
return CacheSet(guid, false, 10, flags)
|
|
end
|
|
|
|
-- arg12 onward depend on the subevent (see the ClassicAPI documentation)
|
|
local cleu = {}
|
|
|
|
cleu.SWING_DAMAGE = function(source, target)
|
|
-- amount, overkill, school, ...
|
|
return parser:AddData(source, autohit, target, arg12, "Physical", "damage")
|
|
end
|
|
|
|
cleu.SPELL_DAMAGE = function(source, target)
|
|
-- spellId, spellName, spellSchool, amount, ...
|
|
return parser:AddData(source, arg13 or UNKNOWN, target, arg15, school_mask[arg14] or "Physical", "damage")
|
|
end
|
|
cleu.SPELL_PERIODIC_DAMAGE = cleu.SPELL_DAMAGE
|
|
cleu.RANGE_DAMAGE = cleu.SPELL_DAMAGE
|
|
|
|
cleu.DAMAGE_SHIELD = function(source, target)
|
|
local school = school_mask[arg14] or "Physical"
|
|
local spell = type(arg12) == "number" and arg12 > 0 and arg13
|
|
return parser:AddData(source, spell or reflect[school], target, arg15, school, "damage")
|
|
end
|
|
|
|
-- Old builds and unavailable target health both report zero overhealing.
|
|
-- A valid positive value proves this runtime implements the newer accounting.
|
|
local observed_overhealing = false
|
|
cleu.SPELL_HEAL = function(source, target)
|
|
local amount, over = arg15, arg16
|
|
local effective
|
|
if type(amount) == "number" and type(over) == "number" and over >= 0 and over <= amount and amount - amount == 0 then
|
|
if over > 0 then
|
|
observed_overhealing = true
|
|
effective = amount - over
|
|
elseif observed_overhealing and arg8 and UnitExists(arg8) and UnitIsVisible and UnitIsVisible(arg8) and UnitHealthMax(arg8) > 100 then
|
|
effective = amount
|
|
end
|
|
end
|
|
return parser:AddData(source, arg13 or UNKNOWN, target, amount, "Physical", "heal", effective)
|
|
end
|
|
cleu.SPELL_PERIODIC_HEAL = cleu.SPELL_HEAL
|
|
|
|
local function CLEUEvent()
|
|
local handler = cleu[arg2]
|
|
if not handler or arg3 then return end
|
|
|
|
local source = CLEUSource(arg4, arg5, arg6)
|
|
if not source then return end
|
|
|
|
handler(source, arg9 or UNKNOWN)
|
|
end
|
|
|
|
parser:RegisterSource("classicapi", {
|
|
available = function()
|
|
return CombatLogGetCurrentEventInfo and UnitOwnerGUID and true or nil
|
|
end,
|
|
why = function()
|
|
-- ClassicAPI builds before the combat log was added have the rest of the API
|
|
if UnitOwnerGUID or C_PlayerInfo then return "this ClassicAPI build has no combat log yet" end
|
|
return "ClassicAPI isn't loaded"
|
|
end,
|
|
enable = function()
|
|
parser:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED")
|
|
parser:SetScript("OnEvent", CLEUEvent)
|
|
end,
|
|
})
|
|
|
|
-------------------------------------------------------------------------------
|
|
-- Nampower: damage, auto attack and heal events
|
|
-------------------------------------------------------------------------------
|
|
|
|
local function NPSource(guid)
|
|
if not guid then return nil end
|
|
|
|
local found, label = CacheGet(guid)
|
|
if found then return label end
|
|
|
|
if UnitIsPlayer(guid) then
|
|
local name = UnitName(guid)
|
|
if Tracked(name) then return CacheSet(guid, name, 60) end
|
|
return CacheSet(guid, false, 10)
|
|
end
|
|
|
|
local label = PetLabel(guid)
|
|
if label then return CacheSet(guid, label, 30) end
|
|
|
|
if config.track_all_units == 1 then
|
|
local name = UnitName(guid)
|
|
if name then return CacheSet(guid, name, 30) end
|
|
end
|
|
|
|
return CacheSet(guid, false, 10)
|
|
end
|
|
|
|
-- target names: heals need them to find the target's health
|
|
local function NPTarget(guid)
|
|
return guid and UnitName(guid) or UNKNOWN
|
|
end
|
|
|
|
-- spell id -> name
|
|
local spell_names = {}
|
|
local function SpellName(id)
|
|
local name = spell_names[id]
|
|
if not name then
|
|
if GetSpellInfo then name = GetSpellInfo(id) end
|
|
if not name and SpellInfo then name = SpellInfo(id) end
|
|
name = name or UNKNOWN
|
|
spell_names[id] = name
|
|
end
|
|
return name
|
|
end
|
|
|
|
local nampower = {}
|
|
|
|
nampower.SPELL_DAMAGE_EVENT_SELF = function()
|
|
-- targetGuid, casterGuid, spellId, amount, mitigationStr, hitInfo, spellSchool
|
|
if not arg4 or arg4 <= 0 then return end
|
|
local source = NPSource(arg2)
|
|
if not source then return end
|
|
if arg1 == arg2 then return end -- damage to self
|
|
|
|
-- the target's name is only used to skip self damage: don't look it up
|
|
parser:AddData(source, SpellName(arg3), arg1, arg4, school_index[arg7] or "Physical", "damage")
|
|
end
|
|
nampower.SPELL_DAMAGE_EVENT_OTHER = nampower.SPELL_DAMAGE_EVENT_SELF
|
|
|
|
nampower.AUTO_ATTACK_SELF = function()
|
|
-- attackerGuid, targetGuid, totalDamage, ...
|
|
if not arg3 or arg3 <= 0 then return end
|
|
local source = NPSource(arg1)
|
|
if not source then return end
|
|
if arg1 == arg2 then return end
|
|
parser:AddData(source, autohit, arg2, arg3, "Physical", "damage")
|
|
end
|
|
nampower.AUTO_ATTACK_OTHER = nampower.AUTO_ATTACK_SELF
|
|
|
|
nampower.DAMAGE_SHIELD_SELF = function()
|
|
-- unitGuid, targetGuid, damage, school
|
|
if not arg3 or arg3 <= 0 then return end
|
|
local source = NPSource(arg1)
|
|
if not source then return end
|
|
local school = school_index[arg4] or "Physical"
|
|
parser:AddData(source, reflect[school], arg2, arg3, school, "damage")
|
|
end
|
|
nampower.DAMAGE_SHIELD_OTHER = nampower.DAMAGE_SHIELD_SELF
|
|
|
|
nampower.SPELL_HEAL_BY_SELF = function()
|
|
-- targetGuid, casterGuid, spellId, amount, critical, periodic
|
|
if not arg4 or arg4 <= 0 then return end
|
|
local source = NPSource(arg2)
|
|
if not source then return end
|
|
parser:AddData(source, SpellName(arg3), NPTarget(arg1), arg4, "Physical", "heal")
|
|
end
|
|
nampower.SPELL_HEAL_BY_OTHER = nampower.SPELL_HEAL_BY_SELF
|
|
|
|
local function NampowerEvent()
|
|
local handler = nampower[event]
|
|
if handler then handler() end
|
|
end
|
|
|
|
parser:RegisterSource("nampower", {
|
|
available = function()
|
|
return GetNampowerVersion and GUIDTokensWork() and true or nil
|
|
end,
|
|
why = function()
|
|
if not GetNampowerVersion then return "Nampower isn't loaded" end
|
|
return "needs SuperWoW or ClassicAPI for GUID unit tokens"
|
|
end,
|
|
enable = function()
|
|
-- Auto attack and heal events are off by default in Nampower. From 4.5.0
|
|
-- on, registering the event turns it on, so the saved CVars stay untouched.
|
|
-- Older versions only know the CVars.
|
|
local major, minor = 0, 0
|
|
local ok, a, b = pcall(GetNampowerVersion)
|
|
if ok then major, minor = tonumber(a) or 0, tonumber(b) or 0 end
|
|
|
|
if major < 4 or (major == 4 and minor < 5) then
|
|
for _, cvar in pairs({ "NP_EnableAutoAttackEvents", "NP_EnableSpellHealEvents" }) do
|
|
local ok, value = pcall(GetCVar, cvar)
|
|
if ok and value == "0" then pcall(SetCVar, cvar, "1") end
|
|
end
|
|
end
|
|
|
|
for event in pairs(nampower) do
|
|
parser:RegisterEvent(event)
|
|
end
|
|
parser:SetScript("OnEvent", NampowerEvent)
|
|
end,
|
|
})
|