Files
HailMaryGuildTools/Modules/Tracker/TrackerCore.lua
2026-04-24 23:43:55 +02:00

405 lines
14 KiB
Lua

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