-- 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: "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) -- 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() then return true, entry.name end end local function CacheSet(guid, name, lifetime) 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].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 local found, label = CacheGet(guid) 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) elseif config.track_all_units == 1 then return CacheSet(guid, name, 60) end return CacheSet(guid, false, 10) end if hasbit(flags, TYPE_PET) or hasbit(flags, TYPE_GUARDIAN) 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) end end if config.track_all_units == 1 and name ~= UNKNOWNOBJECT then return CacheSet(guid, name, 30) end return CacheSet(guid, false, 10) 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, })