local ADDON_NAME = "HailMaryGuildTools" local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME) if not HMGT then return end HMGT.TrackerCore = HMGT.TrackerCore or {} local function EntryNeedsVisualTicker(entry) if type(entry) ~= "table" then return false end local remaining = tonumber(entry.remaining) or 0 if remaining > 0 then return true end local maxCharges = tonumber(entry.maxCharges) or 0 local currentCharges = tonumber(entry.currentCharges) if maxCharges > 0 and currentCharges ~= nil and currentCharges < maxCharges then return true end return false end function HMGT:IsGroupTrackerConfig(tracker) return type(tracker) == "table" and tracker.trackerType == "group" end function HMGT:GetTrackerSpellPool(categories) if HMGT_SpellData and type(HMGT_SpellData.GetSpellPoolForCategories) == "function" then return HMGT_SpellData.GetSpellPoolForCategories(categories) end return {} end function HMGT:GetTrackerSpellsForPlayer(classToken, specIndex, categories) if HMGT_SpellData and type(HMGT_SpellData.GetSpellsForCategories) == "function" then return HMGT_SpellData.GetSpellsForCategories(classToken, specIndex, categories) end return {} end function HMGT:GetTrackerPlayers(tracker) local players = {} local ownName = self:NormalizePlayerName(UnitName("player")) local ownClass = select(2, UnitClass("player")) local includeOwnPlayer = true if self:IsGroupTrackerConfig(tracker) then includeOwnPlayer = tracker.includeSelfFrame == true end if includeOwnPlayer then players[#players + 1] = { name = ownName, class = ownClass, isOwn = true, unitId = "player", } end if IsInRaid() then for i = 1, GetNumGroupMembers() do local unitId = "raid" .. i local name = self:NormalizePlayerName(UnitName(unitId)) local class = select(2, UnitClass(unitId)) if name and name ~= ownName then players[#players + 1] = { name = name, class = class, unitId = unitId, } end end elseif IsInGroup() then for i = 1, GetNumGroupMembers() - 1 do local unitId = "party" .. i local name = self:NormalizePlayerName(UnitName(unitId)) local class = select(2, UnitClass(unitId)) if name and name ~= ownName then players[#players + 1] = { name = name, class = class, unitId = unitId, } end end end return players end function HMGT:CollectTrackerEntriesForPlayer(tracker, playerInfo) local entries = {} if type(tracker) ~= "table" or type(playerInfo) ~= "table" then return entries end local playerName = playerInfo.name if not playerName then return entries end local pData = self.playerData[playerName] local classToken = pData and pData.class or playerInfo.class if not classToken then return entries end local specIndex if playerInfo.isOwn then specIndex = GetSpecialization() if not specIndex or specIndex == 0 then return entries end else specIndex = pData and pData.specIndex or nil if not specIndex or tonumber(specIndex) <= 0 then return entries end end local talents = pData and pData.talents or {} local spells = self:GetTrackerSpellsForPlayer(classToken, specIndex, tracker.categories) for _, spellEntry in ipairs(spells) do if tracker.enabledSpells[spellEntry.spellId] ~= false then local remaining, total, currentCharges, maxCharges = self:GetCooldownInfo(playerName, spellEntry.spellId) local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents) local isAvailabilitySpell = self:IsAvailabilitySpell(spellEntry) local include = self:ShouldDisplayEntry(tracker, remaining, currentCharges, maxCharges, spellEntry) local spellKnown = self:IsTrackedSpellKnownForPlayer(playerName, spellEntry.spellId) local hasPartialCharges = (tonumber(maxCharges) or 0) > 0 and (tonumber(currentCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0) local hasActiveCd = ((remaining or 0) > 0) or hasPartialCharges if not spellKnown and not hasActiveCd then include = false end if isAvailabilitySpell and not spellKnown then include = false end if not playerInfo.isOwn and isAvailabilitySpell and not self:HasAvailabilityState(playerName, spellEntry.spellId) then include = false end if include then entries[#entries + 1] = { playerName = playerName, class = classToken, spellEntry = spellEntry, remaining = remaining, total = total > 0 and total or effectiveCd, currentCharges = currentCharges, maxCharges = maxCharges, } end end end return entries end local function CopyEntriesForPreview(entries, playerName) local copies = {} for _, entry in ipairs(entries or {}) do local nextEntry = {} for key, value in pairs(entry) do nextEntry[key] = value end nextEntry.playerName = playerName copies[#copies + 1] = nextEntry end return copies end function HMGT:BuildPartyPreviewEntries(entries, resolveUnitAnchorFrame) local byPlayer = {} local order = {} local unitByPlayer = {} for index = 1, 4 do local unitId = "party" .. index if not resolveUnitAnchorFrame or resolveUnitAnchorFrame(unitId) then local playerName = string.format("Party %d", index) local playerEntries = CopyEntriesForPreview(entries, playerName) if #playerEntries > 0 then byPlayer[playerName] = playerEntries order[#order + 1] = playerName unitByPlayer[playerName] = unitId end end end return byPlayer, order, unitByPlayer, #order > 0 end function HMGT:CollectTrackerEntries(tracker) local entries = {} local players = self:GetTrackerPlayers(tracker) for _, playerInfo in ipairs(players) do local playerEntries = self:CollectTrackerEntriesForPlayer(tracker, playerInfo) for _, entry in ipairs(playerEntries) do entries[#entries + 1] = entry end end return entries end function HMGT:CollectTrackerTestEntries(tracker) local playerName = self:NormalizePlayerName(UnitName("player")) or "Player" local classToken = select(2, UnitClass("player")) if not classToken then return {} end local entries = {} local pData = self.playerData[playerName] local talents = pData and pData.talents or {} local spells = self:GetTrackerSpellsForPlayer(classToken, GetSpecialization() or 0, tracker.categories) for _, spellEntry in ipairs(spells) do if tracker.enabledSpells[spellEntry.spellId] ~= false then local remaining, total, currentCharges, maxCharges = self:GetCooldownInfo(playerName, spellEntry.spellId) local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents) local isAvailabilitySpell = self:IsAvailabilitySpell(spellEntry) local spellKnown = self:IsTrackedSpellKnownForPlayer(playerName, spellEntry.spellId) local hasPartialCharges = (tonumber(maxCharges) or 0) > 0 and (tonumber(currentCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0) local hasActiveCd = ((remaining or 0) > 0) or hasPartialCharges local hasAvailabilityState = isAvailabilitySpell and self:HasAvailabilityState(playerName, spellEntry.spellId) if spellKnown or hasActiveCd or hasAvailabilityState then entries[#entries + 1] = { playerName = playerName, class = classToken, spellEntry = spellEntry, remaining = remaining, total = total > 0 and total or effectiveCd, currentCharges = currentCharges, maxCharges = maxCharges, } end end end return entries end function HMGT:BuildEntriesForTracker(tracker, trackerKey) local key = trackerKey or tostring(tonumber(tracker and tracker.id) or 0) if tracker and tracker.testMode then return self:CollectTrackerTestEntries(tracker), true end if tracker and tracker.demoMode then return self:GetDemoEntries(key, self:GetTrackerSpellPool(tracker.categories), tracker), true end if not tracker or not tracker.enabled or not self:IsVisibleForCurrentGroup(tracker) then return {}, false end return self:CollectTrackerEntries(tracker), true end function HMGT:BuildEntriesByPlayerForTracker(tracker, trackerKey, resolveUnitAnchorFrame) local key = trackerKey or tostring(tonumber(tracker and tracker.id) or 0) local ownName = self:NormalizePlayerName(UnitName("player")) or "Player" if tracker.testMode then local entries = self:CollectTrackerTestEntries(tracker) if self:IsGroupTrackerConfig(tracker) and tracker.attachToPartyFrame == true then return self:BuildPartyPreviewEntries(entries, resolveUnitAnchorFrame) end local byPlayer, order, unitByPlayer = {}, {}, {} if #entries > 0 then byPlayer[ownName] = entries order[1] = ownName unitByPlayer[ownName] = "player" end return byPlayer, order, unitByPlayer, true end if tracker.demoMode then local entries = self:GetDemoEntries(key, self:GetTrackerSpellPool(tracker.categories), tracker) if self:IsGroupTrackerConfig(tracker) and tracker.attachToPartyFrame == true then return self:BuildPartyPreviewEntries(entries, resolveUnitAnchorFrame) end for _, entry in ipairs(entries) do entry.playerName = ownName end local byPlayer, order, unitByPlayer = {}, {}, {} if #entries > 0 then byPlayer[ownName] = entries order[1] = ownName unitByPlayer[ownName] = "player" end return byPlayer, order, unitByPlayer, true end if not tracker.enabled or not self:IsVisibleForCurrentGroup(tracker) then return {}, {}, {}, false end if IsInRaid() or not IsInGroup() then return {}, {}, {}, false end local byPlayer, order, unitByPlayer = {}, {}, {} for _, playerInfo in ipairs(self:GetTrackerPlayers(tracker)) do local entries = self:CollectTrackerEntriesForPlayer(tracker, playerInfo) if #entries > 0 then local playerName = playerInfo.name byPlayer[playerName] = entries order[#order + 1] = playerName unitByPlayer[playerName] = playerInfo.unitId end end return byPlayer, order, unitByPlayer, true end function HMGT:FinalizeTrackerEntries(tracker, entries, trackerKey) local result = entries or {} if self.FilterDisplayEntries then result = self:FilterDisplayEntries(tracker, result) or result end if self.SortDisplayEntries then self:SortDisplayEntries(result, trackerKey) end local shouldTick = false for _, entry in ipairs(result) do if EntryNeedsVisualTicker(entry) then shouldTick = true break end end return result, shouldTick end function HMGT:TriggerTrackerUpdate(reason) local function normalizeReason(value) if value == true then return "trackers" elseif value == "trackers" or value == "layout" or value == "visual" then return value end return "full" end local function mergeReasons(current, incoming) local priority = { visual = 1, layout = 2, trackers = 3, full = 4, } current = normalizeReason(current) incoming = normalizeReason(incoming) if (priority[incoming] or 4) >= (priority[current] or 4) then return incoming end return current end self._trackerUpdateMinDelay = self._trackerUpdateMinDelay or 0.08 self._trackerUpdatePending = true self._trackerUpdateReason = mergeReasons(self._trackerUpdateReason, reason) if HMGT.TrackerManager then local normalizedReason = normalizeReason(reason) if normalizedReason == "trackers" then HMGT.TrackerManager:MarkTrackersDirty() elseif normalizedReason == "layout" then HMGT.TrackerManager:MarkLayoutDirty() end end if self._updateScheduled then return end local now = GetTime() local last = self._lastTrackerUpdateAt or 0 local delay = math.max(0, self._trackerUpdateMinDelay - (now - last)) self._updateScheduled = true self:ScheduleTimer(function() self._updateScheduled = nil if not self._trackerUpdatePending then return end self._trackerUpdatePending = nil self._lastTrackerUpdateAt = GetTime() local pendingReason = self._trackerUpdateReason self._trackerUpdateReason = nil local function profileModule(name, fn) if not fn then return end local t0 = debugprofilestop and debugprofilestop() or nil fn() local t1 = debugprofilestop and debugprofilestop() or nil if t0 and t1 then local mod = HMGT[name] local count = mod and mod.lastEntryCount or 0 self:Debug("verbose", "UIUpdate %s took %.2fms entries=%s", tostring(name), t1 - t0, tostring(count)) end end profileModule("TrackerManager", HMGT.TrackerManager and function() if pendingReason == "visual" and HMGT.TrackerManager.RefreshVisibleVisuals then HMGT.TrackerManager:RefreshVisibleVisuals() else HMGT.TrackerManager:UpdateDisplay() end end or nil) if self._trackerUpdatePending then self:TriggerTrackerUpdate() end end, delay) end