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

586 lines
19 KiB
Lua

-- Modules/Tracker/TrackerManager.lua
-- Generic tracker manager for category-driven tracker frames.
local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
if not HMGT then return end
local Manager = {}
HMGT.TrackerManager = Manager
Manager.frames = Manager.frames or {}
Manager.perPlayerFrames = Manager.perPlayerFrames or {}
Manager.activeOrders = Manager.activeOrders or {}
Manager.unitByPlayer = Manager.unitByPlayer or {}
Manager.anchorLayoutSignatures = Manager.anchorLayoutSignatures or {}
Manager.nextAnchorRetryAt = Manager.nextAnchorRetryAt or {}
Manager.enabled = false
Manager.visualTicker = nil
Manager.lastEntryCount = 0
Manager._shared = Manager._shared or {}
Manager._trackerCache = Manager._trackerCache or nil
Manager._trackerCacheSignature = Manager._trackerCacheSignature or nil
Manager._displaySignatures = Manager._displaySignatures or {}
Manager._layoutDirty = Manager._layoutDirty == true
local function GetTrackerFrameKey(trackerId)
return "tracker:" .. tostring(tonumber(trackerId) or 0)
end
local function GetTrackerFrameName(trackerId)
return "GenericTracker_" .. tostring(tonumber(trackerId) or 0)
end
local function GetTrackerPlayerFrameName(trackerId, playerName)
local token = tostring(playerName or "Unknown"):gsub("[^%w_]", "_")
return string.format("%s_%s", GetTrackerFrameName(trackerId), token)
end
local function ShortName(name)
if not name then
return ""
end
local short = name:match("^[^-]+")
return short or name
end
local function IsUsableAnchorFrame(frame)
return frame
and frame.IsObjectType
and (frame:IsObjectType("Frame") or frame:IsObjectType("Button"))
end
local function GetFrameUnit(frame)
if not frame then
return nil
end
local unit = frame.unit
if not unit and frame.GetAttribute then
unit = frame:GetAttribute("unit")
end
return unit
end
local function FrameMatchesUnit(frame, unitId)
if not IsUsableAnchorFrame(frame) then
return false
end
if not unitId then
return true
end
return GetFrameUnit(frame) == unitId
end
local PLAYER_FRAME_CANDIDATES = {
"PlayerFrame",
"ElvUF_Player",
"NephUI_PlayerFrame",
"NephUIPlayerFrame",
"oUF_NephUI_Player",
"SUFUnitplayer",
}
local PARTY_FRAME_PATTERNS = {
"PartyMemberFrame%d",
"CompactPartyFrameMember%d",
"ElvUF_PartyGroup1UnitButton%d",
"ElvUF_PartyUnitButton%d",
"NephUI_PartyUnitButton%d",
"NephUI_PartyFrame%d",
"NephUIPartyFrame%d",
"oUF_NephUI_PartyUnitButton%d",
"SUFUnitparty%d",
}
local unitFrameCache = {}
local function BuildAnchorLayoutSignature(settings, ordered, unitByPlayer)
local parts = {
settings.attachToPartyFrame == true and "attach" or "stack",
tostring(settings.partyAttachSide or "RIGHT"),
tostring(tonumber(settings.partyAttachOffsetX) or 8),
tostring(tonumber(settings.partyAttachOffsetY) or 0),
tostring(settings.showBar and "bar" or "icon"),
tostring(settings.growDirection or "DOWN"),
tostring(settings.width or 250),
tostring(settings.barHeight or 20),
tostring(settings.iconSize or 32),
tostring(settings.iconCols or 6),
tostring(settings.barSpacing or 2),
tostring(settings.locked),
tostring(settings.anchorTo or "UIParent"),
tostring(settings.anchorPoint or "TOPLEFT"),
tostring(settings.anchorRelPoint or "TOPLEFT"),
tostring(settings.anchorX or settings.posX or 0),
tostring(settings.anchorY or settings.posY or 0),
}
for _, playerName in ipairs(ordered or {}) do
parts[#parts + 1] = tostring(playerName)
parts[#parts + 1] = tostring(unitByPlayer and unitByPlayer[playerName] or "")
end
return table.concat(parts, "|")
end
local function IsGroupTracker(tracker)
return type(tracker) == "table" and tracker.trackerType == "group"
end
local function ResolveNamedUnitFrame(unitId)
if unitId == "player" then
for _, frameName in ipairs(PLAYER_FRAME_CANDIDATES) do
local frame = _G[frameName]
if FrameMatchesUnit(frame, unitId) or (frameName == "PlayerFrame" and IsUsableAnchorFrame(frame)) then
return frame
end
end
return nil
end
local idx = type(unitId) == "string" and unitId:match("^party(%d+)$")
if not idx then
return nil
end
idx = tonumber(idx)
for _, pattern in ipairs(PARTY_FRAME_PATTERNS) do
local frame = _G[pattern:format(idx)]
if FrameMatchesUnit(frame, unitId) then
return frame
end
end
return nil
end
local function ScanUnitFrame(unitId)
local frame = EnumerateFrames()
local scanned = 0
while frame and scanned < 8000 do
if IsUsableAnchorFrame(frame) and GetFrameUnit(frame) == unitId then
HMGT:Debug("verbose", "TrackerAttach scan unit=%s scanned=%d found=true", tostring(unitId), scanned)
return frame
end
scanned = scanned + 1
frame = EnumerateFrames(frame)
end
HMGT:Debug("verbose", "TrackerAttach scan unit=%s scanned=%d found=false", tostring(unitId), scanned)
return nil
end
local function ResolveUnitAnchorFrame(unitId)
if not unitId then
return nil
end
local now = GetTime()
local cached = unitFrameCache[unitId]
if cached and now < (cached.expires or 0) then
if cached.frame and cached.frame:IsShown() then
return cached.frame
end
return nil
end
local frame = ResolveNamedUnitFrame(unitId)
if not frame then
frame = ScanUnitFrame(unitId)
end
local expiresIn = 1.0
if frame and frame:IsShown() then
expiresIn = 10.0
end
unitFrameCache[unitId] = {
frame = frame,
expires = now + expiresIn,
}
if frame and frame:IsShown() then
return frame
end
return nil
end
local function GetTrackerLabel(tracker)
if type(tracker) ~= "table" then
return "Tracker"
end
local name = tostring(tracker.name or ""):gsub("^%s+", ""):gsub("%s+$", "")
local trackerId = tonumber(tracker.id) or 0
if name ~= "" then
return name
end
if trackerId > 0 then
return string.format("Tracker %d", trackerId)
end
return "Tracker"
end
local function SortTrackers(trackers)
table.sort(trackers, function(a, b)
local aId = tonumber(a and a.id) or 0
local bId = tonumber(b and b.id) or 0
if aId ~= bId then
return aId < bId
end
return tostring(a and a.name or "") < tostring(b and b.name or "")
end)
return trackers
end
local function BuildTrackerCacheSignature(trackers)
local parts = { tostring(#(trackers or {})) }
for index, tracker in ipairs(trackers or {}) do
parts[#parts + 1] = tostring(index)
parts[#parts + 1] = tostring(tonumber(tracker and tracker.id) or 0)
parts[#parts + 1] = tostring(tracker and tracker.trackerType or "normal")
parts[#parts + 1] = tostring(tracker and tracker.enabled)
parts[#parts + 1] = tostring(tracker and tracker.name or "")
end
return table.concat(parts, "|")
end
local function BuildNormalDisplaySignature(frameShown, entries)
local parts = { frameShown and "1" or "0", tostring(#(entries or {})) }
for index, entry in ipairs(entries or {}) do
parts[#parts + 1] = tostring(index)
parts[#parts + 1] = tostring(entry and entry.playerName or "")
parts[#parts + 1] = tostring(entry and entry.spellEntry and entry.spellEntry.spellId or 0)
end
return table.concat(parts, "|")
end
local function BuildGroupDisplaySignature(order, byPlayer)
local parts = { tostring(#(order or {})) }
for _, playerName in ipairs(order or {}) do
parts[#parts + 1] = tostring(playerName)
parts[#parts + 1] = tostring(#((byPlayer and byPlayer[playerName]) or {}))
end
return table.concat(parts, "|")
end
Manager._shared.GetTrackerFrameKey = GetTrackerFrameKey
Manager._shared.GetTrackerFrameName = GetTrackerFrameName
Manager._shared.GetTrackerPlayerFrameName = GetTrackerPlayerFrameName
Manager._shared.ShortName = ShortName
Manager._shared.BuildAnchorLayoutSignature = BuildAnchorLayoutSignature
Manager._shared.IsGroupTracker = IsGroupTracker
Manager._shared.ResolveUnitAnchorFrame = ResolveUnitAnchorFrame
Manager._shared.GetTrackerLabel = GetTrackerLabel
Manager._shared.BuildGroupDisplaySignature = BuildGroupDisplaySignature
function Manager:GetTrackers()
local profile = HMGT and HMGT.db and HMGT.db.profile
local trackers = profile and profile.trackers or {}
local signature = BuildTrackerCacheSignature(trackers)
if self._trackerCache and self._trackerCacheSignature == signature then
return self._trackerCache
end
local result = {}
for _, tracker in ipairs(trackers) do
result[#result + 1] = tracker
end
self._trackerCache = SortTrackers(result)
self._trackerCacheSignature = signature
return self._trackerCache
end
function Manager:GetTrackerFrameKey(tracker)
if type(tracker) == "table" then
return GetTrackerFrameKey(tracker.id)
end
return GetTrackerFrameKey(tracker)
end
function Manager:MarkTrackersDirty()
self._trackerCache = nil
self._trackerCacheSignature = nil
self._layoutDirty = true
end
function Manager:MarkLayoutDirty()
self._layoutDirty = true
end
function Manager:EnsureFrame(tracker)
local frameKey = GetTrackerFrameKey(tracker.id)
local frame = self.frames[frameKey]
if not frame then
frame = HMGT.TrackerFrame:CreateTrackerFrame(GetTrackerFrameName(tracker.id), tracker)
frame._hmgtTrackerId = tonumber(tracker.id) or 0
self.frames[frameKey] = frame
end
frame._settings = tracker
HMGT.TrackerFrame:SetTitle(frame, GetTrackerLabel(tracker))
HMGT.TrackerFrame:SetLocked(frame, tracker.locked)
return frame
end
function Manager:GetAnchorFrame(tracker)
if type(tracker) ~= "table" then
return nil
end
if IsGroupTracker(tracker) then
local frameKey = GetTrackerFrameKey(tracker.id)
local order = self.activeOrders[frameKey] or {}
local frames = self.perPlayerFrames[frameKey] or {}
if order[1] and frames[order[1]] and frames[order[1]]:IsShown() then
return frames[order[1]]
end
end
return self:EnsureFrame(tracker)
end
function Manager:StopVisualTicker()
if self.visualTicker then
self.visualTicker:Cancel()
self.visualTicker = nil
end
end
function Manager:SetVisualTickerEnabled(enabled)
if enabled then
if not self.visualTicker then
self.visualTicker = C_Timer.NewTicker(0.1, function()
self:RefreshVisibleVisuals()
end)
end
else
self:StopVisualTicker()
end
end
function Manager:RefreshAnchors(force)
for _, tracker in ipairs(self:GetTrackers()) do
local frameKey = GetTrackerFrameKey(tracker.id)
if IsGroupTracker(tracker) then
local anchorFrame = self.frames[frameKey]
if anchorFrame and not anchorFrame._hmgtDragging then
HMGT.TrackerFrame:ApplyAnchor(anchorFrame)
end
self:RefreshPerGroupAnchors(tracker, force)
else
local frame = self.frames[frameKey]
if frame and (force or frame:IsShown()) then
if not frame._hmgtDragging then
HMGT.TrackerFrame:ApplyAnchor(frame)
end
frame:EnableMouse(not tracker.locked)
end
end
end
end
function Manager:InvalidateAnchorLayout()
self.anchorLayoutSignatures = {}
self.nextAnchorRetryAt = {}
self:MarkLayoutDirty()
self:RefreshAnchors(true)
end
function Manager:SetAllLocked(locked)
for _, frame in pairs(self.frames) do
HMGT.TrackerFrame:SetLocked(frame, locked)
end
for _, frameSet in pairs(self.perPlayerFrames) do
for _, frame in pairs(frameSet) do
HMGT.TrackerFrame:SetLocked(frame, locked)
end
end
end
function Manager:GetAnchorableFrames()
local frames = {}
for _, tracker in ipairs(self:GetTrackers()) do
local anchorKey = HMGT.GetTrackerAnchorKey and HMGT:GetTrackerAnchorKey(tracker.id) or nil
local frame = self:GetAnchorFrame(tracker)
if anchorKey and frame then
frames[anchorKey] = frame
end
end
return frames
end
function Manager:Enable()
self.enabled = true
self:MarkTrackersDirty()
self:UpdateDisplay()
end
function Manager:Disable()
self.enabled = false
self:StopVisualTicker()
self._layoutDirty = true
for _, frame in pairs(self.frames) do
frame:Hide()
end
for frameKey in pairs(self.perPlayerFrames) do
self:HidePlayerFrames(frameKey)
end
end
function Manager:RefreshVisibleVisuals()
if not self.enabled then
self:StopVisualTicker()
return
end
local shouldTick = false
local needsFullRefresh = false
local totalEntries = 0
for _, tracker in ipairs(self:GetTrackers()) do
local frameKey = GetTrackerFrameKey(tracker.id)
if IsGroupTracker(tracker) then
local currentOrder = self.activeOrders[frameKey] or {}
if #currentOrder > 0 then
local byPlayer, order, unitByPlayer, shouldShow = self:BuildEntriesByPlayerForTracker(tracker)
if not shouldShow or #order ~= #currentOrder then
needsFullRefresh = true
else
local byPlayerFiltered = {}
for index, playerName in ipairs(order) do
if playerName ~= currentOrder[index] then
needsFullRefresh = true
break
end
local entries = byPlayer[playerName] or {}
local tickThis = false
entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
if #entries == 0 then
needsFullRefresh = true
break
end
local frame = self.perPlayerFrames[frameKey] and self.perPlayerFrames[frameKey][playerName]
if not frame or not frame:IsShown() then
needsFullRefresh = true
break
end
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
totalEntries = totalEntries + #entries
byPlayerFiltered[playerName] = entries
if tickThis then
shouldTick = true
end
end
local newSignature = BuildGroupDisplaySignature(currentOrder, byPlayerFiltered)
if self._displaySignatures[frameKey] ~= newSignature then
needsFullRefresh = true
end
end
end
else
local frame = self.frames[frameKey]
if frame and frame:IsShown() then
local entries, shouldShow = self:BuildEntriesForTracker(tracker)
if not shouldShow then
needsFullRefresh = true
else
local tickThis = false
entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
if #entries == 0 then
needsFullRefresh = true
else
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
totalEntries = totalEntries + #entries
local newSignature = BuildNormalDisplaySignature(true, entries)
if self._displaySignatures[frameKey] ~= newSignature then
needsFullRefresh = true
end
if tickThis then
shouldTick = true
end
end
end
end
end
end
self.lastEntryCount = totalEntries
self:SetVisualTickerEnabled(shouldTick)
if needsFullRefresh then
HMGT:TriggerTrackerUpdate("layout")
end
end
function Manager:UpdateDisplay()
if not self.enabled then
self:StopVisualTicker()
return
end
local trackers = self:GetTrackers()
local activeFrames = {}
local shouldTick = false
local totalEntries = 0
local layoutDirty = self._layoutDirty == true
for _, tracker in ipairs(trackers) do
local frameKey = GetTrackerFrameKey(tracker.id)
local frame = self:EnsureFrame(tracker)
if IsGroupTracker(tracker) then
frame:Hide()
local shown, entryCount, trackerShouldTick = self:UpdatePerGroupMemberTracker(tracker)
totalEntries = totalEntries + (entryCount or 0)
if trackerShouldTick then
shouldTick = true
end
if not shown then
self:HidePlayerFrames(frameKey)
end
else
self:HidePlayerFrames(frameKey)
local entries, shouldShow = self:BuildEntriesForTracker(tracker)
if shouldShow then
local tickThis = false
entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
frame:Show()
frame:EnableMouse(not tracker.locked)
activeFrames[frameKey] = true
totalEntries = totalEntries + #entries
local newSignature = BuildNormalDisplaySignature(true, entries)
if self._displaySignatures[frameKey] ~= newSignature then
self._displaySignatures[frameKey] = newSignature
layoutDirty = true
end
if tickThis then
shouldTick = true
end
else
frame:Hide()
if self._displaySignatures[frameKey] ~= "0" then
self._displaySignatures[frameKey] = "0"
layoutDirty = true
end
end
end
end
for frameKey, frame in pairs(self.frames) do
if not activeFrames[frameKey] then
if frame:IsShown() then
layoutDirty = true
end
frame:Hide()
end
end
self.lastEntryCount = totalEntries
self:SetVisualTickerEnabled(shouldTick)
if layoutDirty then
self._layoutDirty = false
self:RefreshAnchors()
end
end