-- load public variables into local local parser = ShaguDPS.parser local data = ShaguDPS.data local config = ShaguDPS.config local round = ShaguDPS.round -- populate all valid player units local validUnits = { ["player"] = true } for i=1,4 do validUnits["party" .. i] = true end for i=1,40 do validUnits["raid" .. i] = true end -- populate all valid player pets local validPets = { ["pet"] = true } for i=1,4 do validPets["partypet" .. i] = true end for i=1,40 do validPets["raidpet" .. i] = true end -- find unitstr by name local unit_cache = {} local function UnitByName(name) -- skip all scans if cache is still valid if unit_cache[name] and UnitName(unit_cache[name]) == name then return unit_cache[name] end -- scan players for current name for unit in pairs(validUnits) do if UnitName(unit) == name then unit_cache[name] = unit return unit end end -- scan pets for current name for unit in pairs(validPets) do if UnitName(unit) == name then unit_cache[name] = unit return unit end end end -- trim leading and trailing spaces; names repeat on every hit, so remember -- the results instead of building new strings each time local trim_cache, trim_count = {}, 0 local function trim(str) local clean = trim_cache[str] if not clean then clean = gsub(str, "^%s*(.-)%s*$", "%1") if trim_count > 1000 then trim_cache, trim_count = {}, 0 end trim_cache[str] = clean trim_count = trim_count + 1 end return clean end -- Cache labels per pet and spell; repeated hits only perform table lookups. local pet_actions = {} local function PetAction(source, action) local pet = pet_actions[source] if not pet then pet = { prefix = "Pet: " .. gsub(source, "%s%(.-%)$", "") .. " - ", spells = {} } pet_actions[source] = pet end action = action or UNKNOWN or "Unknown" local label = pet.spells[action] if not label then label = pet.prefix .. action pet.spells[action] = label end return label end local function combat() -- check if in combat if UnitAffectingCombat("player") or UnitAffectingCombat("pet") then return true end local raid = GetNumRaidMembers() local group = GetNumPartyMembers() if raid >= 1 then for i = 1, raid do -- check if any raid member is infight if UnitAffectingCombat("raid"..i) or UnitAffectingCombat("raidpet"..i) then return true end end else for i = 1, group do -- check if any group member is infight if UnitAffectingCombat("party"..i) or UnitAffectingCombat("partypet"..i) then return true end end end return nil end local start_next_segment = nil -- fight timer: the time a fight lasted, shared by everyone in it. Used for the -- "Fight Duration" time mode instead of each unit's own active time. local fight = { active = nil, start = 0, last = 0, current = 0, total = 0 } parser.fight = fight local function FightStart(now) fight.active = true fight.start = now fight.last = now fight.current = 0 if parser.OnFightStart then parser.OnFightStart() end end local function FightStop(now) if not fight.active then return end local duration = math.max(0, now - fight.start) fight.active = nil fight.current = duration fight.total = fight.total + duration if parser.OnFightEnd then parser.OnFightEnd(duration) end end -- duration of a segment (0: overall, 1: current) in seconds, never below 1 parser.GetFightTime = function(self, segment) local running = fight.active and (GetTime() - fight.start) or 0 local time if segment == 0 then time = fight.total + running else time = fight.active and running or fight.current end return math.max(1, time) end parser.ResetFight = function(self, overall) fight.active = nil fight.current = 0 if overall then fight.total = 0 end end parser.combat = CreateFrame("Frame", "ShaguDPSCombatState", UIParent) parser.combat:RegisterEvent("PLAYER_REGEN_DISABLED") parser.combat:RegisterEvent("PLAYER_REGEN_ENABLED") -- scan and trigger combat state changes parser.combat.UpdateState = function(self) local state = combat() == true and "COMBAT" or "NO_COMBAT" if not self.oldstate or self.oldstate ~= state then self.oldstate = state if state == "NO_COMBAT" then start_next_segment = true FightStop(GetTime()) end elseif state == "NO_COMBAT" and fight.active and fight.last + 5 < GetTime() then -- data that came in while nobody was in combat (a heal or a dot after the -- fight): close that short segment at its last event FightStop(fight.last) end end -- check when player leaves/enters combat parser.combat:SetScript("OnEvent", function() this:UpdateState() end) -- check each second parser.combat:SetScript("OnUpdate", function() if ( this.tick or 1) > GetTime() then return else this.tick = GetTime() + 1 end this:UpdateState() -- keep "fight duration" windows ticking while a fight runs if fight.active and config.time_mode == 1 then for id, callback in pairs(parser.callbacks.refresh) do callback() end end end) parser.ScanName = function(self, name) -- ignore invalid messages if not name then return end -- check if name matches a real player for unit, _ in pairs(validUnits) do if UnitExists(unit) and UnitName(unit) == name then if UnitIsPlayer(unit) then local _, class = UnitClass(unit) data["classes"][name] = class return "PLAYER" end end end -- detect SuperWoW pet owner local match, _, owner = string.find(name, "%((.*)%)", 1) if match and owner then if parser:ScanName(owner) == "PLAYER" then data["classes"][name] = owner return "PET" end end -- check if name matches a player pet for unit, _ in pairs(validPets) do if UnitExists(unit) and UnitName(unit) == name then -- parse and set pet owners if strsub(unit,0,3) == "pet" then data["classes"][name] = UnitName("player") elseif strsub(unit,0,8) == "partypet" then data["classes"][name] = UnitName("party" .. strsub(unit,9)) elseif strsub(unit,0,7) == "raidpet" then data["classes"][name] = UnitName("raid" .. strsub(unit,8)) end return "PET" end end -- assign class other if tracking of all units is set if config.track_all_units == 1 then data["classes"][name] = data["classes"][name] or "__other__" return "OTHER" else return nil end end -- Dispel counters share segment boundaries with damage/healing, without timelines. data.dispels_done = data.dispels_done or { [0] = {}, [1] = {} } data.dispels_received = data.dispels_received or { [0] = {}, [1] = {} } local function PrepareSegment(source) if start_next_segment and data.classes[source] and data.classes[source] ~= "__other__" then data.damage[1], data.heal[1] = {}, {} data.dispels_done[1], data.dispels_received[1] = {}, {} start_next_segment = nil FightStart(GetTime()) end end local function CountDispel(view, name, other, aura, ability) for segment = 0, 1 do local entry = view[segment][name] if not entry then entry = { _sum = 0, _ctime = 1, _auras = {}, _players = {}, _abilities = {} } view[segment][name] = entry end entry._abilities = entry._abilities or {} local spell = ability or "Unknown ability" entry._abilities[spell] = (entry._abilities[spell] or 0) + 1 entry._sum = entry._sum + 1 entry._auras[aura] = (entry._auras[aura] or 0) + 1 entry._players[other] = (entry._players[other] or 0) + 1 end end parser.AddDispel = function(self, source, target, aura, ability) if type(source) ~= "string" or type(target) ~= "string" or type(aura) ~= "string" then return end local done = self:ScanName(source) == "PLAYER" local received = self:ScanName(target) == "PLAYER" if not done and not received then return end PrepareSegment(done and source or target) if done then CountDispel(data.dispels_done, source, target, aura, ability) end if received then CountDispel(data.dispels_received, target, source, aura, ability) end if not fight.active then FightStart(GetTime()) end fight.last = GetTime() for _, callback in pairs(self.callbacks.refresh) do callback() end end -- Listen independently of the selected damage parser; never count failed casts. local dispels = CreateFrame("Frame") ShaguDPS.dispels = dispels dispels.available = CombatLogGetCurrentEventInfo and true or nil if dispels.available then dispels:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") end dispels:SetScript("OnEvent", function() if event == "COMBAT_LOG_EVENT_UNFILTERED" and arg2 == "SPELL_DISPEL" and not arg3 then local ability = type(arg12) == "number" and arg12 > 0 and type(arg13) == "string" and arg13 or nil parser:AddDispel(arg5, arg9, arg16 or (arg15 and ("Aura #" .. arg15)), ability) end end) parser.AddData = function(self, source, action, target, value, school, datatype, effective_override) -- abort on invalid input if type(source) ~= "string" then return end if not tonumber(value) then return end -- trim leading and trailing spaces source = trim(source) -- prevent self-damage from being tracked if datatype == "damage" and source == target then return end PrepareSegment(source) -- calculate effective value (heal) local effective = 0 if datatype == "heal" then local unitstr local override = type(effective_override) == "number" and effective_override >= 0 and effective_override <= tonumber(value) if override then effective = effective_override else unitstr = UnitByName(target) end if unitstr then -- calculate the effective healing of the current data effective = math.max(0, math.min(UnitHealthMax(unitstr) - UnitHealth(unitstr), value)) end end -- write both (overall and current segment) for segment = 0, 1 do local entry = data[datatype][segment] -- detect source and write initial table if not entry[source] then local type = parser:ScanName(source) if type == "PET" then -- create owner table if not yet existing local owner = data["classes"][source] if not entry[owner] and parser:ScanName(owner) then entry[owner] = { ["_sum"] = 0, ["_ctime"] = 1 } end elseif not type then -- invalid or disabled unit type break end -- create base damage table entry[source] = { ["_sum"] = 0, ["_ctime"] = 1 } end -- write pet damage into owners data if enabled local action, source = action, source if config.merge_pets == 1 and -- merge pets? data["classes"][source] ~= "__other__" and -- valid unit? entry[data["classes"][source]] -- has owner? then -- remove pet data entry[source] = nil action = PetAction(source, action) source = data["classes"][source] -- write data into owner if not entry[source] then entry[source] = { ["_sum"] = 0, ["_ctime"] = 1 } end end if entry[source] then -- write overall value and per spell entry[source][action] = (entry[source][action] or 0) + tonumber(value) entry[source]["_sum"] = (entry[source]["_sum"] or 0) + tonumber(value) if datatype == "heal" then -- write effective healing sumary entry[source]["_esum"] = (entry[source]["_esum"] or 0) + tonumber(effective) -- write effective healing per spell entry[source]["_effective"] = entry[source]["_effective"] or {} entry[source]["_effective"][action] = (entry[source]["_effective"][action] or 0) + tonumber(effective) end -- the first data of a session starts the fight timer too if segment == 1 then if not fight.active then FightStart(GetTime()) end fight.last = GetTime() end entry[source]["_ctime"] = entry[source]["_ctime"] or 1 entry[source]["_tick"] = entry[source]["_tick"] or GetTime() if entry[source]["_tick"] + 5 < GetTime() then entry[source]["_tick"] = GetTime() entry[source]["_ctime"] = entry[source]["_ctime"] + 5 else entry[source]["_ctime"] = entry[source]["_ctime"] + (GetTime() - entry[source]["_tick"]) entry[source]["_tick"] = GetTime() end end end for id, callback in pairs(parser.callbacks.refresh) do callback() end end parser.callbacks = { ["refresh"] = {} } -- data sources: each parser file registers one. Only one is active at a time, -- picked by the "source" setting (1: best available, then a forced one). -- available() returns true when the client can feed it -- enable() registers its events and sets parser's OnEvent handler local source_order = { "classicapi", "nampower", "combatlog" } local source_ids = { classicapi = 2, nampower = 3, combatlog = 4 } parser.sources = {} parser.source_labels = { classicapi = "ClassicAPI", nampower = "Nampower", combatlog = "Chat Combat Log", } parser.RegisterSource = function(self, name, source) self.sources[name] = source end parser.SelectSource = function(self) local wanted = config.source or 1 local pick for _, name in pairs(source_order) do local src = self.sources[name] if src and (wanted == 1 or source_ids[name] == wanted) and src.available() then pick = name break end end -- a forced source that is missing falls back to the best available one, -- and says why it couldn't be used self.unavailable = nil if not pick and wanted ~= 1 then for name, id in pairs(source_ids) do if id == wanted then local src = self.sources[name] self.unavailable = name self.unavailable_reason = src and src.why and src.why() or "not loaded" end end end if not pick then for _, name in pairs(source_order) do local src = self.sources[name] if src and src.available() then pick = name break end end end if pick == self.active_source then return pick end self:UnregisterAllEvents() self:SetScript("OnEvent", nil) if self.active_source and self.sources[self.active_source].disable then self.sources[self.active_source].disable() end self.active_source = pick if pick then self.sources[pick].enable() end return pick end