dev-v.2.1.0 #10
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,17 @@ HailMaryGuildToolsOptions.lua
|
||||
# ────── Tracker ──────────────────────────────────────────────────────
|
||||
Modules\Tracker\Frame.lua
|
||||
Modules\Tracker\SpellDatabase.lua
|
||||
Modules\Tracker\SingleFrameTrackerBase.lua
|
||||
Modules\Tracker\TrackerCore.lua
|
||||
Modules\Tracker\TrackerState.lua
|
||||
Modules\Tracker\TrackerPlayerState.lua
|
||||
Modules\Tracker\TrackerBridge.lua
|
||||
Modules\Tracker\TrackerDataProvider.lua
|
||||
Modules\Tracker\TrackerSync.lua
|
||||
Modules\Tracker\TrackerAvailability.lua
|
||||
Modules\Tracker\TrackerDetection.lua
|
||||
Modules\Tracker\InterruptTracker\InterruptTracker.lua
|
||||
Modules\Tracker\RaidCooldownTracker\RaidCooldownTracker.lua
|
||||
Modules\Tracker\GroupCooldownTracker\GroupCooldownTracker.lua
|
||||
|
||||
Modules\Tracker\InterruptTracker\InterruptSpellDatabase.lua
|
||||
Modules\Tracker\RaidcooldownTracker\RaidCooldownSpellDatabase.lua
|
||||
|
||||
@@ -1,692 +1,65 @@
|
||||
-- Modules/GroupCooldownTracker.lua
|
||||
-- Group-Cooldown-Tracker Modul (ein Frame pro Spieler in der Gruppe)
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||
|
||||
local GCT = HMGT:NewModule("GroupCooldownTracker")
|
||||
HMGT.GroupCooldownTracker = GCT
|
||||
local module = HMGT:NewModule("GroupCooldownTracker")
|
||||
HMGT.GroupCooldownTracker = module
|
||||
|
||||
GCT.frame = nil
|
||||
GCT.frames = {}
|
||||
|
||||
local function SanitizeFrameToken(name)
|
||||
if not name or name == "" then return "Unknown" end
|
||||
return name:gsub("[^%w_]", "_")
|
||||
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
|
||||
local unit = GetFrameUnit(frame)
|
||||
return unit == unitId
|
||||
end
|
||||
|
||||
local PLAYER_FRAME_CANDIDATES = {
|
||||
"PlayerFrame",
|
||||
"ElvUF_Player",
|
||||
"NephUI_PlayerFrame",
|
||||
"NephUIPlayerFrame",
|
||||
"oUF_NephUI_Player",
|
||||
"SUFUnitplayer",
|
||||
module.definition = {
|
||||
moduleName = "GroupCooldownTracker",
|
||||
dbKey = "groupCooldownTracker",
|
||||
trackerType = "group",
|
||||
trackerKey = "groupCooldownTracker",
|
||||
title = function()
|
||||
return L["GCD_TITLE"]
|
||||
end,
|
||||
categories = { "tank", "defensive", "healing", "cc", "utility", "offensive", "lust", "interrupt" },
|
||||
}
|
||||
|
||||
local PARTY_FRAME_PATTERNS = {
|
||||
"PartyMemberFrame%d", -- Blizzard alt
|
||||
"CompactPartyFrameMember%d", -- Blizzard modern
|
||||
"ElvUF_PartyGroup1UnitButton%d", -- ElvUI
|
||||
"ElvUF_PartyUnitButton%d", -- ElvUI variant
|
||||
"NephUI_PartyUnitButton%d", -- NephUI (common naming variants)
|
||||
"NephUI_PartyFrame%d",
|
||||
"NephUIPartyFrame%d",
|
||||
"oUF_NephUI_PartyUnitButton%d",
|
||||
"SUFUnitparty%d", -- Shadowed Unit Frames
|
||||
}
|
||||
|
||||
local unitFrameCache = {}
|
||||
|
||||
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
|
||||
|
||||
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 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) then
|
||||
local unit = GetFrameUnit(frame)
|
||||
if unit == unitId then
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach scan unit=%s scanned=%d found=true", tostring(unitId), scanned)
|
||||
return frame
|
||||
end
|
||||
end
|
||||
scanned = scanned + 1
|
||||
frame = EnumerateFrames(frame)
|
||||
end
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach 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
|
||||
|
||||
function GCT:GetFrameIdForPlayer(playerName)
|
||||
return "GroupCooldownTracker_" .. SanitizeFrameToken(playerName)
|
||||
end
|
||||
|
||||
function GCT:GetPlayerFrame(playerName)
|
||||
if not playerName then return nil end
|
||||
return self.frames[playerName]
|
||||
end
|
||||
|
||||
function GCT:GetAnchorableFrames()
|
||||
return self.frames
|
||||
end
|
||||
|
||||
function GCT:EnsurePlayerFrame(playerName)
|
||||
local frame = self.frames[playerName]
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if frame then
|
||||
return frame
|
||||
end
|
||||
|
||||
frame = HMGT.TrackerFrame:CreateTrackerFrame(self:GetFrameIdForPlayer(playerName), s)
|
||||
frame._hmgtPlayerName = playerName
|
||||
self.frames[playerName] = frame
|
||||
return frame
|
||||
end
|
||||
|
||||
function GCT:HideAllFrames()
|
||||
for _, frame in pairs(self.frames) do
|
||||
frame:Hide()
|
||||
end
|
||||
self.activeOrder = nil
|
||||
self.unitByPlayer = nil
|
||||
self.frame = nil
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
|
||||
function GCT:SetLockedAll(locked)
|
||||
for _, frame in pairs(self.frames) do
|
||||
HMGT.TrackerFrame:SetLocked(frame, locked)
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:EnsureUpdateTicker()
|
||||
if self.updateTicker then
|
||||
return
|
||||
end
|
||||
self.updateTicker = C_Timer.NewTicker(0.1, function()
|
||||
self:UpdateDisplay()
|
||||
end)
|
||||
end
|
||||
|
||||
function GCT:StopUpdateTicker()
|
||||
if self.updateTicker then
|
||||
self.updateTicker:Cancel()
|
||||
self.updateTicker = nil
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:SetUpdateTickerEnabled(enabled)
|
||||
if enabled then
|
||||
self:EnsureUpdateTicker()
|
||||
else
|
||||
self:StopUpdateTicker()
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:InvalidateAnchorLayout()
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
|
||||
function GCT:RefreshAnchors(force)
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if not s then return end
|
||||
|
||||
local ordered = {}
|
||||
for _, playerName in ipairs(self.activeOrder or {}) do
|
||||
local frame = self.frames[playerName]
|
||||
if frame and frame:IsShown() then
|
||||
table.insert(ordered, playerName)
|
||||
end
|
||||
end
|
||||
|
||||
if #ordered == 0 then
|
||||
self.frame = nil
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = nil
|
||||
return
|
||||
end
|
||||
|
||||
local now = GetTime()
|
||||
local signature = BuildAnchorLayoutSignature(s, ordered, self.unitByPlayer)
|
||||
if not force and self._lastAnchorLayoutSignature == signature then
|
||||
local retryAt = tonumber(self._nextAnchorRetryAt) or 0
|
||||
if retryAt <= 0 or now < retryAt then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
-- Do not force anchor updates while user is dragging a tracker frame.
|
||||
for _, playerName in ipairs(ordered) do
|
||||
local frame = self.frames[playerName]
|
||||
if frame and frame._hmgtDragging then
|
||||
return
|
||||
end
|
||||
function module:GetDefinition()
|
||||
return self.definition
|
||||
end
|
||||
|
||||
local primaryName = ordered[1]
|
||||
local primary = self.frames[primaryName]
|
||||
self.frame = primary
|
||||
|
||||
if s.attachToPartyFrame == true then
|
||||
local side = s.partyAttachSide or "RIGHT"
|
||||
local extraX = tonumber(s.partyAttachOffsetX) or 8
|
||||
local extraY = tonumber(s.partyAttachOffsetY) or 0
|
||||
local growsUp = s.showBar == true and s.growDirection == "UP"
|
||||
local barHeight = tonumber(s.barHeight) or 20
|
||||
local growUpAttachOffset = barHeight + 20
|
||||
local prevPlaced = nil
|
||||
local missingTargets = 0
|
||||
|
||||
for i = 1, #ordered do
|
||||
local playerName = ordered[i]
|
||||
local frame = self.frames[playerName]
|
||||
local unitId = self.unitByPlayer and self.unitByPlayer[playerName]
|
||||
local target = ResolveUnitAnchorFrame(unitId)
|
||||
local contentTopInset = HMGT.TrackerFrame.GetContentTopInset and HMGT.TrackerFrame:GetContentTopInset(frame) or 0
|
||||
|
||||
frame:ClearAllPoints()
|
||||
if target then
|
||||
if side == "LEFT" then
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMRIGHT", target, "TOPLEFT", -extraX, extraY - growUpAttachOffset)
|
||||
else
|
||||
frame:SetPoint("TOPRIGHT", target, "TOPLEFT", -extraX, extraY + contentTopInset)
|
||||
end
|
||||
else
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", target, "TOPRIGHT", extraX, extraY - growUpAttachOffset)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", target, "TOPRIGHT", extraX, extraY + contentTopInset)
|
||||
end
|
||||
end
|
||||
elseif prevPlaced then
|
||||
missingTargets = missingTargets + 1
|
||||
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach fallback-stack player=%s unit=%s", tostring(playerName), tostring(unitId))
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", prevPlaced, "TOPLEFT", 0, (s.barSpacing or 2) + 10)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", prevPlaced, "BOTTOMLEFT", 0, -((s.barSpacing or 2) + 10))
|
||||
end
|
||||
else
|
||||
missingTargets = missingTargets + 1
|
||||
HMGT:DebugScoped("info", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach fallback-anchor player=%s unit=%s (no party frame found)", tostring(playerName), tostring(unitId))
|
||||
HMGT.TrackerFrame:ApplyAnchor(frame)
|
||||
end
|
||||
|
||||
frame:EnableMouse(false)
|
||||
prevPlaced = frame
|
||||
end
|
||||
if missingTargets > 0 then
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = now + 1.0
|
||||
else
|
||||
self._lastAnchorLayoutSignature = signature
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
HMGT.TrackerFrame:ApplyAnchor(primary)
|
||||
primary:EnableMouse(not s.locked)
|
||||
|
||||
local gap = (s.barSpacing or 2) + 10
|
||||
local growsUp = s.showBar == true and s.growDirection == "UP"
|
||||
for i = 2, #ordered do
|
||||
local prev = self.frames[ordered[i - 1]]
|
||||
local frame = self.frames[ordered[i]]
|
||||
frame:ClearAllPoints()
|
||||
if growsUp then
|
||||
frame:SetPoint("BOTTOMLEFT", prev, "TOPLEFT", 0, gap)
|
||||
else
|
||||
frame:SetPoint("TOPLEFT", prev, "BOTTOMLEFT", 0, -gap)
|
||||
end
|
||||
frame:EnableMouse(false)
|
||||
end
|
||||
self._lastAnchorLayoutSignature = signature
|
||||
self._nextAnchorRetryAt = nil
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- ENABLE / DISABLE
|
||||
-- ============================================================
|
||||
|
||||
function GCT:Enable()
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if not s.enabled and not s.demoMode and not s.testMode then return end
|
||||
|
||||
self:UpdateDisplay()
|
||||
end
|
||||
|
||||
function GCT:Disable()
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- DISPLAY UPDATE
|
||||
-- ============================================================
|
||||
|
||||
function GCT:UpdateDisplay()
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
if not s then return end
|
||||
|
||||
if s.testMode then
|
||||
local entries, playerName = HMGT:GetOwnTestEntries(HMGT_SpellData.GroupCooldowns, s, {
|
||||
deferChargeCooldownUntilEmpty = false,
|
||||
})
|
||||
local byPlayer = { [playerName] = {} }
|
||||
for _, entry in ipairs(entries) do
|
||||
entry.playerName = playerName
|
||||
table.insert(byPlayer[playerName], entry)
|
||||
end
|
||||
|
||||
self.activeOrder = { playerName }
|
||||
self.unitByPlayer = { [playerName] = "player" }
|
||||
self.lastEntryCount = 0
|
||||
local active = {}
|
||||
local shownOrder = {}
|
||||
local shouldTick = false
|
||||
for _, pName in ipairs(self.activeOrder) do
|
||||
local frame = self:EnsurePlayerFrame(pName)
|
||||
HMGT.TrackerFrame:SetLocked(frame, s.locked)
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", L["GCD_TITLE"], ShortName(pName)))
|
||||
local displayEntries = byPlayer[pName]
|
||||
if HMGT.FilterDisplayEntries then
|
||||
displayEntries = HMGT:FilterDisplayEntries(s, displayEntries) or displayEntries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(displayEntries, "groupCooldownTracker")
|
||||
end
|
||||
if #displayEntries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, displayEntries, true)
|
||||
self.lastEntryCount = self.lastEntryCount + #displayEntries
|
||||
frame:Show()
|
||||
active[pName] = true
|
||||
shownOrder[#shownOrder + 1] = pName
|
||||
for _, entry in ipairs(displayEntries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
self.activeOrder = shownOrder
|
||||
|
||||
for pn, frame in pairs(self.frames) do
|
||||
if not active[pn] then
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
self:RefreshAnchors()
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
return
|
||||
end
|
||||
|
||||
if s.demoMode then
|
||||
local entries = HMGT:GetDemoEntries("groupCooldownTracker", HMGT_SpellData.GroupCooldowns, s)
|
||||
local playerName = HMGT:NormalizePlayerName(UnitName("player")) or "DemoPlayer"
|
||||
local byPlayer = { [playerName] = {} }
|
||||
for _, entry in ipairs(entries) do
|
||||
entry.playerName = playerName
|
||||
table.insert(byPlayer[playerName], entry)
|
||||
end
|
||||
|
||||
self.activeOrder = { playerName }
|
||||
self.unitByPlayer = { [playerName] = "player" }
|
||||
self.lastEntryCount = 0
|
||||
local active = {}
|
||||
local shownOrder = {}
|
||||
local shouldTick = false
|
||||
for _, playerName in ipairs(self.activeOrder) do
|
||||
local frame = self:EnsurePlayerFrame(playerName)
|
||||
HMGT.TrackerFrame:SetLocked(frame, s.locked)
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", L["GCD_TITLE"], ShortName(playerName)))
|
||||
local displayEntries = byPlayer[playerName]
|
||||
if HMGT.FilterDisplayEntries then
|
||||
displayEntries = HMGT:FilterDisplayEntries(s, displayEntries) or displayEntries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(displayEntries, "groupCooldownTracker")
|
||||
end
|
||||
if #displayEntries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, displayEntries, true)
|
||||
self.lastEntryCount = self.lastEntryCount + #displayEntries
|
||||
frame:Show()
|
||||
active[playerName] = true
|
||||
shownOrder[#shownOrder + 1] = playerName
|
||||
shouldTick = true
|
||||
else
|
||||
frame:Hide()
|
||||
function module:GetSettings()
|
||||
local profile = HMGT.db and HMGT.db.profile
|
||||
return profile and profile[self.definition.dbKey] or nil
|
||||
end
|
||||
end
|
||||
self.activeOrder = shownOrder
|
||||
|
||||
for pn, frame in pairs(self.frames) do
|
||||
if not active[pn] then
|
||||
frame:Hide()
|
||||
end
|
||||
function module:Enable()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.Enable then
|
||||
HMGT.TrackerManager:Enable()
|
||||
end
|
||||
|
||||
self:RefreshAnchors()
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
return
|
||||
end
|
||||
|
||||
if IsInRaid() or not IsInGroup() then
|
||||
self.lastEntryCount = 0
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
return
|
||||
function module:Disable()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.UpdateDisplay then
|
||||
HMGT.TrackerManager:UpdateDisplay()
|
||||
end
|
||||
if not s.enabled then
|
||||
self.lastEntryCount = 0
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
return
|
||||
end
|
||||
if not HMGT:IsVisibleForCurrentGroup(s) then
|
||||
self.lastEntryCount = 0
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
return
|
||||
end
|
||||
|
||||
local entriesByPlayer, order, unitByPlayer = self:CollectEntriesByPlayer()
|
||||
self.activeOrder = order
|
||||
self.unitByPlayer = unitByPlayer
|
||||
self.lastEntryCount = 0
|
||||
local active = {}
|
||||
local shownOrder = {}
|
||||
local shouldTick = false
|
||||
|
||||
for _, playerName in ipairs(order) do
|
||||
local frame = self:EnsurePlayerFrame(playerName)
|
||||
HMGT.TrackerFrame:SetLocked(frame, s.locked)
|
||||
HMGT.TrackerFrame:SetTitle(frame, string.format("%s - %s", L["GCD_TITLE"], ShortName(playerName)))
|
||||
|
||||
local entries = entriesByPlayer[playerName] or {}
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(s, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries, "groupCooldownTracker")
|
||||
end
|
||||
if #entries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
self.lastEntryCount = self.lastEntryCount + #entries
|
||||
frame:Show()
|
||||
active[playerName] = true
|
||||
shownOrder[#shownOrder + 1] = playerName
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
function module:SetLockedAll(locked)
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.SetAllLocked then
|
||||
HMGT.TrackerManager:SetAllLocked(locked)
|
||||
end
|
||||
end
|
||||
self.activeOrder = shownOrder
|
||||
|
||||
for pn, frame in pairs(self.frames) do
|
||||
if not active[pn] then
|
||||
frame:Hide()
|
||||
function module:RefreshAnchors(force)
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.RefreshAnchors then
|
||||
HMGT.TrackerManager:RefreshAnchors(force)
|
||||
end
|
||||
end
|
||||
|
||||
self:RefreshAnchors()
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
function module:InvalidateAnchorLayout()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.InvalidateAnchorLayout then
|
||||
HMGT.TrackerManager:InvalidateAnchorLayout()
|
||||
end
|
||||
|
||||
function GCT:CollectEntriesByPlayer()
|
||||
local s = HMGT.db.profile.groupCooldownTracker
|
||||
local byPlayer = {}
|
||||
local playerOrder = {}
|
||||
local unitByPlayer = {}
|
||||
local players = self:GetGroupPlayers()
|
||||
|
||||
for _, playerInfo in ipairs(players) do
|
||||
repeat
|
||||
local name = playerInfo.name
|
||||
if not name then break end
|
||||
|
||||
local pData = HMGT.playerData[name]
|
||||
local class = pData and pData.class or playerInfo.class
|
||||
local specIdx
|
||||
if playerInfo.isOwn then
|
||||
specIdx = GetSpecialization()
|
||||
if not specIdx or specIdx == 0 then break end
|
||||
else
|
||||
specIdx = pData and pData.specIndex or nil
|
||||
if not specIdx or tonumber(specIdx) <= 0 then break end
|
||||
end
|
||||
local talents = pData and pData.talents or {}
|
||||
if not class then break end
|
||||
|
||||
local knownCDs = HMGT_SpellData.GetSpellsForSpec(class, specIdx, HMGT_SpellData.GroupCooldowns)
|
||||
local entries = {}
|
||||
for _, spellEntry in ipairs(knownCDs) do
|
||||
if s.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, curCharges, maxCharges = HMGT:GetCooldownInfo(name, spellEntry.spellId, {
|
||||
deferChargeCooldownUntilEmpty = false,
|
||||
})
|
||||
local isAvailabilitySpell = HMGT.IsAvailabilitySpell and HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local hasChargeSpell = (tonumber(maxCharges) or 0) > 1
|
||||
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0 and (tonumber(curCharges) or tonumber(maxCharges) or 0) < (tonumber(maxCharges) or 0)
|
||||
local include = HMGT:ShouldDisplayEntry(s, remaining, curCharges, maxCharges, spellEntry)
|
||||
local spellKnown = HMGT:IsTrackedSpellKnownForPlayer(name, spellEntry.spellId)
|
||||
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 then
|
||||
if isAvailabilitySpell and not HMGT:HasAvailabilityState(name, spellEntry.spellId) then
|
||||
include = false
|
||||
function module:GetAnchorableFrames()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.GetAnchorableFrames then
|
||||
return HMGT.TrackerManager:GetAnchorableFrames()
|
||||
end
|
||||
return {}
|
||||
end
|
||||
if include then
|
||||
table.insert(entries, {
|
||||
playerName = name,
|
||||
class = class,
|
||||
spellEntry = spellEntry,
|
||||
remaining = remaining,
|
||||
total = total > 0 and total or effectiveCd,
|
||||
currentCharges = curCharges,
|
||||
maxCharges = maxCharges,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if #entries > 0 then
|
||||
byPlayer[name] = entries
|
||||
table.insert(playerOrder, name)
|
||||
unitByPlayer[name] = playerInfo.unitId
|
||||
end
|
||||
until true
|
||||
end
|
||||
|
||||
table.sort(playerOrder, function(a, b)
|
||||
local own = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
if a == own and b ~= own then return true end
|
||||
if b == own and a ~= own then return false end
|
||||
return a < b
|
||||
end)
|
||||
|
||||
return byPlayer, playerOrder, unitByPlayer
|
||||
end
|
||||
|
||||
function GCT:GetGroupPlayers()
|
||||
local players = {}
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
local settings = HMGT.db and HMGT.db.profile and HMGT.db.profile.groupCooldownTracker
|
||||
|
||||
if settings and settings.includeSelfFrame == true and ownName then
|
||||
table.insert(players, {
|
||||
name = ownName,
|
||||
class = select(2, UnitClass("player")),
|
||||
unitId = "player",
|
||||
isOwn = true,
|
||||
})
|
||||
end
|
||||
|
||||
if IsInGroup() and not IsInRaid() then
|
||||
for i = 1, GetNumGroupMembers() - 1 do
|
||||
local unitId = "party" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
table.insert(players, {name = name, class = class, unitId = unitId})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return players
|
||||
end
|
||||
|
||||
|
||||
@@ -42,54 +42,13 @@ function Manager:HidePlayerFrames(frameKey)
|
||||
end
|
||||
|
||||
function Manager:BuildEntriesByPlayerForTracker(tracker)
|
||||
local frameKey = S.GetTrackerFrameKey(tracker.id)
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player")) or "Player"
|
||||
if tracker.testMode then
|
||||
local entries = self:CollectTestEntries(tracker)
|
||||
if S.IsGroupTracker(tracker) and tracker.attachToPartyFrame == true then
|
||||
return S.BuildPartyPreviewEntries(entries)
|
||||
return HMGT:BuildEntriesByPlayerForTracker(
|
||||
tracker,
|
||||
self:GetTrackerFrameKey(tracker),
|
||||
function(unitId)
|
||||
return S.ResolveUnitAnchorFrame(unitId)
|
||||
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 = HMGT:GetDemoEntries(frameKey, S.GetTrackerSpellPool(tracker.categories), tracker)
|
||||
if S.IsGroupTracker(tracker) and tracker.attachToPartyFrame == true then
|
||||
return S.BuildPartyPreviewEntries(entries)
|
||||
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 HMGT:IsVisibleForCurrentGroup(tracker) then
|
||||
return {}, {}, {}, false
|
||||
end
|
||||
if IsInRaid() or not IsInGroup() then
|
||||
return {}, {}, {}, false
|
||||
end
|
||||
local byPlayer, order, unitByPlayer = {}, {}, {}
|
||||
for _, playerInfo in ipairs(S.GetGroupPlayers(tracker)) do
|
||||
local entries = S.CollectEntriesForPlayer(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 Manager:RefreshPerGroupAnchors(tracker, force)
|
||||
@@ -206,11 +165,10 @@ function Manager:UpdatePerGroupMemberTracker(tracker)
|
||||
for _, playerName in ipairs(order) do
|
||||
local frame = self:EnsurePlayerFrame(tracker, playerName)
|
||||
local entries = byPlayer[playerName] or {}
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
local tickThis = false
|
||||
entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
|
||||
if tickThis then
|
||||
shouldTick = true
|
||||
end
|
||||
if #entries > 0 then
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
@@ -219,12 +177,6 @@ function Manager:UpdatePerGroupMemberTracker(tracker)
|
||||
shownOrder[#shownOrder + 1] = playerName
|
||||
shownByPlayer[playerName] = entries
|
||||
entryCount = entryCount + #entries
|
||||
for _, entry in ipairs(entries) do
|
||||
if S.EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
|
||||
@@ -1,21 +1,40 @@
|
||||
-- Modules/InterruptTracker.lua
|
||||
-- Interrupt tracker based on the shared single-frame tracker base.
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||
|
||||
local Base = HMGT.SingleFrameTrackerBase
|
||||
if not Base then return end
|
||||
local module = HMGT:NewModule("InterruptTracker")
|
||||
HMGT.InterruptTracker = module
|
||||
|
||||
Base:CreateModule("InterruptTracker", {
|
||||
profileKey = "interruptTracker",
|
||||
frameName = "InterruptTracker",
|
||||
module.definition = {
|
||||
moduleName = "InterruptTracker",
|
||||
dbKey = "interruptTracker",
|
||||
trackerType = "normal",
|
||||
trackerKey = "interruptTracker",
|
||||
title = function()
|
||||
return L["IT_TITLE"]
|
||||
end,
|
||||
demoKey = "interruptTracker",
|
||||
database = function()
|
||||
return HMGT_SpellData.Interrupts
|
||||
end,
|
||||
})
|
||||
categories = { "interrupt" },
|
||||
}
|
||||
|
||||
function module:GetDefinition()
|
||||
return self.definition
|
||||
end
|
||||
|
||||
function module:GetSettings()
|
||||
local profile = HMGT.db and HMGT.db.profile
|
||||
return profile and profile[self.definition.dbKey] or nil
|
||||
end
|
||||
|
||||
function module:Enable()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.Enable then
|
||||
HMGT.TrackerManager:Enable()
|
||||
end
|
||||
end
|
||||
|
||||
function module:Disable()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.UpdateDisplay then
|
||||
HMGT.TrackerManager:UpdateDisplay()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,66 +3,15 @@ local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT or not HMGT.TrackerManager then return end
|
||||
|
||||
local Manager = HMGT.TrackerManager
|
||||
local S = Manager._shared or {}
|
||||
|
||||
function Manager:CollectEntries(tracker)
|
||||
local entries = {}
|
||||
local players = S.GetGroupPlayers(tracker)
|
||||
for _, playerInfo in ipairs(players) do
|
||||
local playerEntries = S.CollectEntriesForPlayer(tracker, playerInfo)
|
||||
for _, entry in ipairs(playerEntries) do
|
||||
entries[#entries + 1] = entry
|
||||
end
|
||||
end
|
||||
return entries
|
||||
return HMGT:CollectTrackerEntries(tracker)
|
||||
end
|
||||
|
||||
function Manager:CollectTestEntries(tracker)
|
||||
local playerName = HMGT:NormalizePlayerName(UnitName("player")) or "Player"
|
||||
local classToken = select(2, UnitClass("player"))
|
||||
if not classToken then
|
||||
return {}
|
||||
end
|
||||
|
||||
local entries = {}
|
||||
local pData = HMGT.playerData[playerName]
|
||||
local talents = pData and pData.talents or {}
|
||||
local spells = S.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 = HMGT:GetCooldownInfo(playerName, spellEntry.spellId)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local isAvailabilitySpell = HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local spellKnown = HMGT: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 HMGT: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
|
||||
return HMGT:CollectTrackerTestEntries(tracker)
|
||||
end
|
||||
|
||||
function Manager:BuildEntriesForTracker(tracker)
|
||||
if tracker.testMode then
|
||||
return self:CollectTestEntries(tracker), true
|
||||
end
|
||||
if tracker.demoMode then
|
||||
return HMGT:GetDemoEntries(S.GetTrackerFrameKey(tracker.id), S.GetTrackerSpellPool(tracker.categories), tracker), true
|
||||
end
|
||||
if not tracker.enabled or not HMGT:IsVisibleForCurrentGroup(tracker) then
|
||||
return {}, false
|
||||
end
|
||||
return self:CollectEntries(tracker), true
|
||||
return HMGT:BuildEntriesForTracker(tracker, self:GetTrackerFrameKey(tracker))
|
||||
end
|
||||
|
||||
@@ -1,21 +1,40 @@
|
||||
-- Modules/RaidCooldownTracker.lua
|
||||
-- Raid cooldown tracker based on the shared single-frame tracker base.
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||
|
||||
local Base = HMGT.SingleFrameTrackerBase
|
||||
if not Base then return end
|
||||
local module = HMGT:NewModule("RaidCooldownTracker")
|
||||
HMGT.RaidCooldownTracker = module
|
||||
|
||||
Base:CreateModule("RaidCooldownTracker", {
|
||||
profileKey = "raidCooldownTracker",
|
||||
frameName = "RaidCooldownTracker",
|
||||
module.definition = {
|
||||
moduleName = "RaidCooldownTracker",
|
||||
dbKey = "raidCooldownTracker",
|
||||
trackerType = "normal",
|
||||
trackerKey = "raidCooldownTracker",
|
||||
title = function()
|
||||
return L["RCD_TITLE"]
|
||||
end,
|
||||
demoKey = "raidCooldownTracker",
|
||||
database = function()
|
||||
return HMGT_SpellData.RaidCooldowns
|
||||
end,
|
||||
})
|
||||
categories = { "lust", "defensive", "healing", "tank", "utility", "offensive", "cc", "interrupt" },
|
||||
}
|
||||
|
||||
function module:GetDefinition()
|
||||
return self.definition
|
||||
end
|
||||
|
||||
function module:GetSettings()
|
||||
local profile = HMGT.db and HMGT.db.profile
|
||||
return profile and profile[self.definition.dbKey] or nil
|
||||
end
|
||||
|
||||
function module:Enable()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.Enable then
|
||||
HMGT.TrackerManager:Enable()
|
||||
end
|
||||
end
|
||||
|
||||
function module:Disable()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.UpdateDisplay then
|
||||
HMGT.TrackerManager:UpdateDisplay()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,305 +0,0 @@
|
||||
-- Modules/Tracker/SingleFrameTrackerBase.lua
|
||||
-- Shared implementation for single-frame tracker modules.
|
||||
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.SingleFrameTrackerBase = HMGT.SingleFrameTrackerBase or {}
|
||||
local Base = HMGT.SingleFrameTrackerBase
|
||||
|
||||
local function GetDefaultGroupPlayers()
|
||||
local players = {}
|
||||
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
local ownClass = select(2, UnitClass("player"))
|
||||
table.insert(players, { name = ownName, class = ownClass, isOwn = true, unitId = "player" })
|
||||
|
||||
if IsInRaid() then
|
||||
for i = 1, GetNumGroupMembers() do
|
||||
local unitId = "raid" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
table.insert(players, { name = name, class = class, unitId = unitId })
|
||||
end
|
||||
end
|
||||
elseif IsInGroup() then
|
||||
for i = 1, GetNumGroupMembers() - 1 do
|
||||
local unitId = "party" .. i
|
||||
local name = HMGT:NormalizePlayerName(UnitName(unitId))
|
||||
local class = select(2, UnitClass(unitId))
|
||||
if name and name ~= ownName then
|
||||
table.insert(players, { name = name, class = class, unitId = unitId })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return players
|
||||
end
|
||||
|
||||
local function ResolveConfigValue(configValue, self)
|
||||
if type(configValue) == "function" then
|
||||
return configValue(self)
|
||||
end
|
||||
return configValue
|
||||
end
|
||||
|
||||
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 Base:Create(config)
|
||||
local tracker = {
|
||||
frame = nil,
|
||||
updateTicker = nil,
|
||||
lastEntryCount = 0,
|
||||
}
|
||||
|
||||
function tracker:GetSettings()
|
||||
return HMGT.db.profile[config.profileKey]
|
||||
end
|
||||
|
||||
function tracker:GetDatabase()
|
||||
return ResolveConfigValue(config.database, self) or {}
|
||||
end
|
||||
|
||||
function tracker:GetTitle()
|
||||
return ResolveConfigValue(config.title, self) or config.frameName
|
||||
end
|
||||
|
||||
function tracker:GetDemoKey()
|
||||
return ResolveConfigValue(config.demoKey, self) or config.profileKey
|
||||
end
|
||||
|
||||
function tracker:GetCooldownInfoOpts()
|
||||
return ResolveConfigValue(config.cooldownInfoOpts, self)
|
||||
end
|
||||
|
||||
function tracker:GetGroupPlayers()
|
||||
local custom = ResolveConfigValue(config.groupPlayersProvider, self)
|
||||
if type(custom) == "table" then
|
||||
return custom
|
||||
end
|
||||
return GetDefaultGroupPlayers()
|
||||
end
|
||||
|
||||
function tracker:EnsureUpdateTicker()
|
||||
if self.updateTicker then
|
||||
return
|
||||
end
|
||||
self.updateTicker = C_Timer.NewTicker(0.1, function()
|
||||
self:UpdateDisplay()
|
||||
end)
|
||||
end
|
||||
|
||||
function tracker:StopUpdateTicker()
|
||||
if self.updateTicker then
|
||||
self.updateTicker:Cancel()
|
||||
self.updateTicker = nil
|
||||
end
|
||||
end
|
||||
|
||||
function tracker:SetUpdateTickerEnabled(enabled)
|
||||
if enabled then
|
||||
self:EnsureUpdateTicker()
|
||||
else
|
||||
self:StopUpdateTicker()
|
||||
end
|
||||
end
|
||||
|
||||
function tracker:Enable()
|
||||
local s = self:GetSettings()
|
||||
if not s.enabled and not s.demoMode and not s.testMode then return end
|
||||
|
||||
if not self.frame then
|
||||
self.frame = HMGT.TrackerFrame:CreateTrackerFrame(config.frameName, s)
|
||||
HMGT.TrackerFrame:SetTitle(self.frame, self:GetTitle())
|
||||
end
|
||||
|
||||
if HMGT:IsVisibleForCurrentGroup(s) then
|
||||
self.frame:Show()
|
||||
else
|
||||
self.frame:Hide()
|
||||
end
|
||||
self:UpdateDisplay()
|
||||
end
|
||||
|
||||
function tracker:Disable()
|
||||
self:StopUpdateTicker()
|
||||
if self.frame then
|
||||
self.frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
function tracker:UpdateDisplay()
|
||||
if not self.frame then
|
||||
self:StopUpdateTicker()
|
||||
return
|
||||
end
|
||||
|
||||
local s = self:GetSettings()
|
||||
local database = self:GetDatabase()
|
||||
local cooldownInfoOpts = self:GetCooldownInfoOpts()
|
||||
|
||||
if s.testMode then
|
||||
HMGT.TrackerFrame:SetLocked(self.frame, s.locked)
|
||||
local entries = HMGT:GetOwnTestEntries(database, s, cooldownInfoOpts)
|
||||
self.lastEntryCount = #entries
|
||||
HMGT.TrackerFrame:UpdateFrame(self.frame, entries)
|
||||
self.frame:Show()
|
||||
local shouldTick = false
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
return
|
||||
end
|
||||
|
||||
if s.demoMode then
|
||||
HMGT.TrackerFrame:SetLocked(self.frame, s.locked)
|
||||
local entries = HMGT:GetDemoEntries(self:GetDemoKey(), database, s)
|
||||
self.lastEntryCount = #entries
|
||||
HMGT.TrackerFrame:UpdateFrame(self.frame, entries)
|
||||
self.frame:Show()
|
||||
self:SetUpdateTickerEnabled(#entries > 0)
|
||||
return
|
||||
end
|
||||
|
||||
if not s.enabled then
|
||||
self.lastEntryCount = 0
|
||||
self.frame:Hide()
|
||||
self:StopUpdateTicker()
|
||||
return
|
||||
end
|
||||
|
||||
if not HMGT:IsVisibleForCurrentGroup(s) then
|
||||
self.lastEntryCount = 0
|
||||
self.frame:Hide()
|
||||
self:StopUpdateTicker()
|
||||
return
|
||||
end
|
||||
|
||||
HMGT.TrackerFrame:SetLocked(self.frame, s.locked)
|
||||
|
||||
local entries = self:CollectEntries()
|
||||
self.lastEntryCount = #entries
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries, config.profileKey)
|
||||
end
|
||||
|
||||
HMGT.TrackerFrame:UpdateFrame(self.frame, entries)
|
||||
self.frame:Show()
|
||||
|
||||
local shouldTick = false
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
self:SetUpdateTickerEnabled(shouldTick)
|
||||
end
|
||||
|
||||
function tracker:CollectEntries()
|
||||
local entries = {}
|
||||
local s = self:GetSettings()
|
||||
local database = self:GetDatabase()
|
||||
local cooldownInfoOpts = self:GetCooldownInfoOpts()
|
||||
local players = self:GetGroupPlayers()
|
||||
|
||||
for _, playerInfo in ipairs(players) do
|
||||
repeat
|
||||
local name = playerInfo.name
|
||||
local pData = HMGT.playerData[name]
|
||||
local class = pData and pData.class or playerInfo.class
|
||||
local specIdx
|
||||
if playerInfo.isOwn then
|
||||
specIdx = GetSpecialization()
|
||||
if not specIdx or specIdx == 0 then break end
|
||||
else
|
||||
specIdx = pData and pData.specIndex or nil
|
||||
if not specIdx or tonumber(specIdx) <= 0 then break end
|
||||
end
|
||||
local talents = pData and pData.talents or {}
|
||||
|
||||
if not class then break end
|
||||
|
||||
local knownSpells = HMGT_SpellData.GetSpellsForSpec(class, specIdx, database)
|
||||
|
||||
for _, spellEntry in ipairs(knownSpells) do
|
||||
if s.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, curCharges, maxCharges = HMGT:GetCooldownInfo(name, spellEntry.spellId, cooldownInfoOpts)
|
||||
local isAvailabilitySpell = HMGT.IsAvailabilitySpell and HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local include = HMGT:ShouldDisplayEntry(s, remaining, curCharges, maxCharges, spellEntry)
|
||||
local spellKnown = HMGT:IsTrackedSpellKnownForPlayer(name, spellEntry.spellId)
|
||||
local hasPartialCharges = (tonumber(maxCharges) or 0) > 0
|
||||
and (tonumber(curCharges) 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 then
|
||||
if isAvailabilitySpell and not HMGT:HasAvailabilityState(name, spellEntry.spellId) then
|
||||
include = false
|
||||
end
|
||||
end
|
||||
|
||||
if include then
|
||||
entries[#entries + 1] = {
|
||||
playerName = name,
|
||||
class = class,
|
||||
spellEntry = spellEntry,
|
||||
remaining = remaining,
|
||||
total = total > 0 and total or effectiveCd,
|
||||
currentCharges = curCharges,
|
||||
maxCharges = maxCharges,
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
until true
|
||||
end
|
||||
|
||||
return entries
|
||||
end
|
||||
|
||||
return tracker
|
||||
end
|
||||
|
||||
function Base:CreateModule(moduleName, config, ...)
|
||||
if type(moduleName) ~= "string" or moduleName == "" then
|
||||
return self:Create(config)
|
||||
end
|
||||
|
||||
local module = HMGT:NewModule(moduleName, ...)
|
||||
local tracker = self:Create(config)
|
||||
for key, value in pairs(tracker) do
|
||||
module[key] = value
|
||||
end
|
||||
HMGT[moduleName] = module
|
||||
return module
|
||||
end
|
||||
169
Modules/Tracker/TrackerAvailability.lua
Normal file
169
Modules/Tracker/TrackerAvailability.lua
Normal file
@@ -0,0 +1,169 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.TrackerAvailability = HMGT.TrackerAvailability or {}
|
||||
|
||||
local internals = HMGT.TrackerInternals or {}
|
||||
local GetPlayerAuraApplications = internals.GetPlayerAuraApplications
|
||||
local GetSpellCastCountInfo = internals.GetSpellCastCountInfo
|
||||
|
||||
function HMGT:GetOwnAvailabilityProgress(spellEntry)
|
||||
local availability = self:GetAvailabilityConfig(spellEntry)
|
||||
if not availability then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local required = self:GetAvailabilityRequiredCount(spellEntry)
|
||||
if required <= 0 then
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
local current = 0
|
||||
if availability.type == "auraStacks" then
|
||||
current = GetPlayerAuraApplications and GetPlayerAuraApplications(availability.auraSpellId) or 0
|
||||
if current <= 0 then
|
||||
local fallbackSpellId = tonumber(availability.fallbackSpellCountId)
|
||||
or tonumber(availability.progressSpellId)
|
||||
or tonumber(spellEntry and spellEntry.spellId)
|
||||
if fallbackSpellId and fallbackSpellId > 0 and GetSpellCastCountInfo then
|
||||
current = GetSpellCastCountInfo(fallbackSpellId)
|
||||
end
|
||||
end
|
||||
else
|
||||
return nil, nil
|
||||
end
|
||||
|
||||
current = math.max(0, math.min(required, tonumber(current) or 0))
|
||||
return current, required
|
||||
end
|
||||
|
||||
function HMGT:GetAvailabilityState(playerName, spellId)
|
||||
local state = self:GetAvailabilityStateEntry(playerName, spellId)
|
||||
if not state then
|
||||
return nil, nil
|
||||
end
|
||||
return tonumber(state.current) or 0, tonumber(state.max) or 0
|
||||
end
|
||||
|
||||
function HMGT:HasAvailabilityState(playerName, spellId)
|
||||
local _, max = self:GetAvailabilityState(playerName, spellId)
|
||||
return (tonumber(max) or 0) > 0
|
||||
end
|
||||
|
||||
function HMGT:StoreAvailabilityState(playerName, spellId, current, max, spellEntry)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
local sid = tonumber(spellId)
|
||||
if not normalizedName or not sid or sid <= 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
local maxCount = math.max(0, math.floor((tonumber(max) or 0) + 0.5))
|
||||
if maxCount <= 0 then
|
||||
return self:ClearAvailabilityState(normalizedName, sid)
|
||||
end
|
||||
|
||||
local currentCount = math.max(0, math.min(maxCount, math.floor((tonumber(current) or 0) + 0.5)))
|
||||
local previous = self:GetAvailabilityStateEntry(normalizedName, sid)
|
||||
local changed = (not previous)
|
||||
or (tonumber(previous.current) or -1) ~= currentCount
|
||||
or (tonumber(previous.max) or -1) ~= maxCount
|
||||
|
||||
self:SetAvailabilityStateEntry(normalizedName, sid, {
|
||||
current = currentCount,
|
||||
max = maxCount,
|
||||
spellEntry = spellEntry,
|
||||
updatedAt = GetTime(),
|
||||
})
|
||||
|
||||
return changed
|
||||
end
|
||||
|
||||
function HMGT:RefreshOwnAvailabilitySpell(spellEntry)
|
||||
if not self:IsAvailabilitySpell(spellEntry) then
|
||||
return false
|
||||
end
|
||||
|
||||
local playerName = self:NormalizePlayerName(UnitName("player"))
|
||||
if not playerName then
|
||||
return false
|
||||
end
|
||||
|
||||
local current, max = self:GetOwnAvailabilityProgress(spellEntry)
|
||||
if (tonumber(max) or 0) > 0 then
|
||||
local pData = self.playerData[playerName]
|
||||
if pData and type(pData.knownSpells) == "table" then
|
||||
pData.knownSpells[tonumber(spellEntry.spellId)] = true
|
||||
end
|
||||
end
|
||||
return self:StoreAvailabilityState(playerName, spellEntry.spellId, current, max, spellEntry)
|
||||
end
|
||||
|
||||
function HMGT:RefreshOwnAvailabilityStates()
|
||||
local playerName = self:NormalizePlayerName(UnitName("player"))
|
||||
local pData = playerName and self.playerData[playerName]
|
||||
if not pData or not pData.class or not pData.specIndex then
|
||||
return false
|
||||
end
|
||||
|
||||
local changed = false
|
||||
local groupCooldowns = HMGT_SpellData.GetSpellsForSpec(pData.class, pData.specIndex, HMGT_SpellData.GroupCooldowns)
|
||||
for _, spellEntry in ipairs(groupCooldowns or {}) do
|
||||
if self:IsAvailabilitySpell(spellEntry) and self:RefreshOwnAvailabilitySpell(spellEntry) then
|
||||
changed = true
|
||||
end
|
||||
end
|
||||
|
||||
if self:PruneAvailabilityStates(playerName, pData.knownSpells or {}) then
|
||||
changed = true
|
||||
end
|
||||
|
||||
return changed
|
||||
end
|
||||
|
||||
function HMGT:RefreshAndPublishOwnAvailabilityStates()
|
||||
local playerName = self:NormalizePlayerName(UnitName("player"))
|
||||
local pData = playerName and self.playerData[playerName]
|
||||
if not pData or not pData.class or not pData.specIndex then
|
||||
return false
|
||||
end
|
||||
|
||||
local changed = false
|
||||
local groupCooldowns = HMGT_SpellData.GetSpellsForSpec(pData.class, pData.specIndex, HMGT_SpellData.GroupCooldowns)
|
||||
for _, spellEntry in ipairs(groupCooldowns or {}) do
|
||||
if self:IsAvailabilitySpell(spellEntry) and self:RefreshOwnAvailabilitySpell(spellEntry) then
|
||||
self:PublishOwnSpellState(spellEntry.spellId, { sendLegacy = true })
|
||||
changed = true
|
||||
end
|
||||
end
|
||||
|
||||
if self:PruneAvailabilityStates(playerName, pData.knownSpells or {}) then
|
||||
changed = true
|
||||
end
|
||||
|
||||
return changed
|
||||
end
|
||||
|
||||
function HMGT:SendOwnAvailabilityStates(target)
|
||||
local playerName = self:NormalizePlayerName(UnitName("player"))
|
||||
local pData = playerName and self.playerData[playerName]
|
||||
if not pData or not pData.class or not pData.specIndex then
|
||||
return 0
|
||||
end
|
||||
|
||||
self:RefreshOwnAvailabilityStates()
|
||||
|
||||
local sent = 0
|
||||
local groupCooldowns = HMGT_SpellData.GetSpellsForSpec(pData.class, pData.specIndex, HMGT_SpellData.GroupCooldowns)
|
||||
for _, spellEntry in ipairs(groupCooldowns or {}) do
|
||||
if self:IsAvailabilitySpell(spellEntry) and self:IsTrackedSpellKnownForPlayer(playerName, spellEntry.spellId) then
|
||||
local current, max = self:GetAvailabilityState(playerName, spellEntry.spellId)
|
||||
if (tonumber(max) or 0) > 0 then
|
||||
self:BroadcastAvailabilityState(spellEntry.spellId, current, max, target)
|
||||
sent = sent + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return sent
|
||||
end
|
||||
186
Modules/Tracker/TrackerBridge.lua
Normal file
186
Modules/Tracker/TrackerBridge.lua
Normal file
@@ -0,0 +1,186 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.TrackerBridge = HMGT.TrackerBridge or {}
|
||||
|
||||
function HMGT:RegisterExternalAddonSource(sourceName)
|
||||
local source = tostring(sourceName or "")
|
||||
if source == "" then
|
||||
return false
|
||||
end
|
||||
self.externalAddonSources = self.externalAddonSources or {}
|
||||
self.externalAddonSources[source] = true
|
||||
return true
|
||||
end
|
||||
|
||||
function HMGT:GetCanonicalExternalSpellEntry(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 or not HMGT_SpellData then
|
||||
return nil, sid
|
||||
end
|
||||
|
||||
local spellEntry = HMGT_SpellData.InterruptLookup and HMGT_SpellData.InterruptLookup[sid]
|
||||
or HMGT_SpellData.CooldownLookup and HMGT_SpellData.CooldownLookup[sid]
|
||||
if not spellEntry then
|
||||
return nil, sid
|
||||
end
|
||||
|
||||
return spellEntry, tonumber(spellEntry.spellId) or sid
|
||||
end
|
||||
|
||||
function HMGT:InferClassFromSpellEntry(spellEntry)
|
||||
if type(spellEntry) ~= "table" or type(spellEntry.classes) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local foundClass
|
||||
for key, value in pairs(spellEntry.classes) do
|
||||
local classToken = type(value) == "string" and value or key
|
||||
if foundClass and foundClass ~= classToken then
|
||||
return nil
|
||||
end
|
||||
foundClass = classToken
|
||||
end
|
||||
return foundClass
|
||||
end
|
||||
|
||||
function HMGT:ApplyExternalKnownSpell(sourceName, playerName, spellId, class, cooldown)
|
||||
local source = tostring(sourceName or "External")
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
local sid = tonumber(spellId)
|
||||
if not normalizedName or normalizedName == "" or not sid or sid <= 0 then
|
||||
return false, "invalid_args"
|
||||
end
|
||||
if not self:IsPlayerInCurrentGroup(normalizedName) then
|
||||
return false, "not_in_group"
|
||||
end
|
||||
|
||||
local spellEntry, canonicalSid = self:GetCanonicalExternalSpellEntry(sid)
|
||||
if not spellEntry or not canonicalSid or canonicalSid <= 0 then
|
||||
return false, "unknown_spell"
|
||||
end
|
||||
sid = canonicalSid
|
||||
|
||||
self:RegisterExternalAddonSource(source)
|
||||
local previous = self.playerData[normalizedName] or {}
|
||||
local knownSpells = previous.knownSpells
|
||||
if type(knownSpells) ~= "table" then
|
||||
knownSpells = {}
|
||||
end
|
||||
knownSpells[sid] = true
|
||||
|
||||
local classToken = class or previous.class or self:InferClassFromSpellEntry(spellEntry)
|
||||
|
||||
self.playerData[normalizedName] = {
|
||||
class = classToken,
|
||||
specIndex = previous.specIndex,
|
||||
talentHash = previous.talentHash,
|
||||
talents = previous.talents or {},
|
||||
knownSpells = knownSpells,
|
||||
externalSource = source,
|
||||
}
|
||||
|
||||
if tonumber(cooldown) and tonumber(cooldown) > 0 then
|
||||
spellEntry._hmgtExternalBaseCd = tonumber(cooldown)
|
||||
end
|
||||
|
||||
self:TriggerTrackerUpdate("trackers")
|
||||
return true
|
||||
end
|
||||
|
||||
function HMGT:ApplyExternalSpecInfo(sourceName, playerName, class, specId, talentHash)
|
||||
local source = tostring(sourceName or "External")
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
local spec = tonumber(specId)
|
||||
local classToken = class and tostring(class) or self:GetClassTokenForSpecId(spec)
|
||||
if not normalizedName or normalizedName == "" or not classToken or classToken == "" or not spec or spec <= 0 then
|
||||
return false, "invalid_args"
|
||||
end
|
||||
if not self:IsPlayerInCurrentGroup(normalizedName) then
|
||||
return false, "not_in_group"
|
||||
end
|
||||
|
||||
self:RegisterExternalAddonSource(source)
|
||||
local previous = self.playerData[normalizedName] or {}
|
||||
local knownSpells = previous.knownSpells
|
||||
if type(knownSpells) ~= "table" then
|
||||
knownSpells = {}
|
||||
end
|
||||
|
||||
if HMGT_SpellData and type(HMGT_SpellData.GetSpellsForSpec) == "function" then
|
||||
for _, datasetName in ipairs({ "Interrupts", "RaidCooldowns", "GroupCooldowns" }) do
|
||||
local dataset = HMGT_SpellData[datasetName]
|
||||
for _, spellEntry in ipairs(HMGT_SpellData.GetSpellsForSpec(classToken, spec, dataset)) do
|
||||
local sid = tonumber(spellEntry and spellEntry.spellId)
|
||||
if sid and sid > 0 then
|
||||
knownSpells[sid] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.playerData[normalizedName] = {
|
||||
class = classToken,
|
||||
specIndex = spec,
|
||||
talentHash = talentHash or previous.talentHash,
|
||||
talents = self:ParseTalentHash(talentHash or previous.talentHash),
|
||||
knownSpells = knownSpells,
|
||||
externalSource = source,
|
||||
}
|
||||
|
||||
self:PruneAvailabilityStates(normalizedName, knownSpells)
|
||||
self:TriggerTrackerUpdate("trackers")
|
||||
return true
|
||||
end
|
||||
|
||||
function HMGT:ApplyExternalCooldown(sourceName, playerName, spellId, cooldown)
|
||||
local source = tostring(sourceName or "External")
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
local sid = tonumber(spellId)
|
||||
local cd = tonumber(cooldown)
|
||||
if not normalizedName or normalizedName == "" or not sid or sid <= 0 or not cd or cd <= 0 then
|
||||
return false, "invalid_args"
|
||||
end
|
||||
if not self:IsPlayerInCurrentGroup(normalizedName) then
|
||||
return false, "not_in_group"
|
||||
end
|
||||
|
||||
local spellEntry, canonicalSid = self:GetCanonicalExternalSpellEntry(sid)
|
||||
if not spellEntry or not canonicalSid or canonicalSid <= 0 then
|
||||
return false, "unknown_spell"
|
||||
end
|
||||
sid = canonicalSid
|
||||
|
||||
self:RegisterExternalAddonSource(source)
|
||||
self:ApplyExternalKnownSpell(source, normalizedName, sid, nil, cd)
|
||||
self:HandleRemoteSpellCast(normalizedName, sid, GetServerTime(), nil, nil, nil, cd)
|
||||
return true
|
||||
end
|
||||
|
||||
function HMGT:IsPlayerInCurrentGroup(playerName)
|
||||
local target = self:NormalizePlayerName(playerName)
|
||||
if not target then return false end
|
||||
local own = self:NormalizePlayerName(UnitName("player"))
|
||||
if target == own then return true end
|
||||
|
||||
if IsInRaid() then
|
||||
for i = 1, GetNumGroupMembers() do
|
||||
local n = self:NormalizePlayerName(UnitName("raid" .. i))
|
||||
if n == target then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
if IsInGroup() then
|
||||
for i = 1, GetNumSubgroupMembers() do
|
||||
local n = self:NormalizePlayerName(UnitName("party" .. i))
|
||||
if n == target then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
404
Modules/Tracker/TrackerCore.lua
Normal file
404
Modules/Tracker/TrackerCore.lua
Normal file
@@ -0,0 +1,404 @@
|
||||
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
|
||||
268
Modules/Tracker/TrackerDataProvider.lua
Normal file
268
Modules/Tracker/TrackerDataProvider.lua
Normal file
@@ -0,0 +1,268 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.TrackerDataProvider = HMGT.TrackerDataProvider or {}
|
||||
|
||||
local internals = HMGT.TrackerInternals or {}
|
||||
local SafeApiNumber = internals.SafeApiNumber
|
||||
local GetSpellChargesInfo = internals.GetSpellChargesInfo
|
||||
local GetSpellCooldownInfo = internals.GetSpellCooldownInfo
|
||||
|
||||
function HMGT:GetCooldownInfo(playerName, spellId, opts)
|
||||
opts = opts or {}
|
||||
local deferUntilEmpty = opts.deferChargeCooldownUntilEmpty and true or false
|
||||
local spellEntry = HMGT_SpellData.InterruptLookup[spellId]
|
||||
or HMGT_SpellData.CooldownLookup[spellId]
|
||||
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||
local isOwnPlayer = playerName == ownName
|
||||
local pData = isOwnPlayer and self.playerData[ownName] or nil
|
||||
local talents = pData and pData.talents or {}
|
||||
local effectiveCd = spellEntry and HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents) or 0
|
||||
local knownMaxCharges, knownChargeDuration = 0, 0
|
||||
if spellEntry and isOwnPlayer then
|
||||
knownMaxCharges, knownChargeDuration = self:GetKnownChargeInfo(spellEntry, talents, spellId, effectiveCd)
|
||||
end
|
||||
|
||||
if self:IsAvailabilitySpell(spellEntry) then
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
if normalizedName == ownName then
|
||||
local current, max = self:GetOwnAvailabilityProgress(spellEntry)
|
||||
if (tonumber(max) or 0) > 0 then
|
||||
self:StoreAvailabilityState(ownName, spellId, current, max, spellEntry)
|
||||
return 0, 0, current, max
|
||||
end
|
||||
else
|
||||
local current, max = self:GetAvailabilityState(normalizedName, spellId)
|
||||
if (tonumber(max) or 0) > 0 then
|
||||
return 0, 0, current, max
|
||||
end
|
||||
end
|
||||
return 0, 0, nil, nil
|
||||
end
|
||||
|
||||
local cdData = self:GetActiveCooldown(playerName, spellId)
|
||||
|
||||
if isOwnPlayer and not (InCombatLockdown and InCombatLockdown()) then
|
||||
local charges, maxCharges, chargeStart, chargeDuration = nil, nil, nil, nil
|
||||
if GetSpellChargesInfo then
|
||||
charges, maxCharges, chargeStart, chargeDuration = GetSpellChargesInfo(spellId)
|
||||
end
|
||||
charges = SafeApiNumber and SafeApiNumber(charges, 0) or tonumber(charges) or 0
|
||||
maxCharges = SafeApiNumber and SafeApiNumber(maxCharges, 0) or tonumber(maxCharges) or 0
|
||||
chargeStart = SafeApiNumber and SafeApiNumber(chargeStart) or tonumber(chargeStart)
|
||||
chargeDuration = SafeApiNumber and SafeApiNumber(chargeDuration, 0) or tonumber(chargeDuration) or 0
|
||||
|
||||
if maxCharges > 0 then
|
||||
local tempChargeState = {
|
||||
currentCharges = charges,
|
||||
maxCharges = maxCharges,
|
||||
chargeStart = chargeStart,
|
||||
chargeDuration = chargeDuration,
|
||||
duration = chargeDuration,
|
||||
}
|
||||
local remaining, total, curCharges, maxChargeCount = self:ResolveChargeState(tempChargeState)
|
||||
self:StoreKnownChargeInfo(spellId, maxChargeCount, total > 0 and total or chargeDuration)
|
||||
if (curCharges or 0) < maxChargeCount and remaining <= 0 and GetSpellCooldownInfo then
|
||||
local cdStart, cdDuration = GetSpellCooldownInfo(spellId)
|
||||
if cdDuration > 0 then
|
||||
remaining = math.max(0, cdDuration - (GetTime() - cdStart))
|
||||
total = math.max(total or 0, cdDuration)
|
||||
end
|
||||
end
|
||||
if deferUntilEmpty and (curCharges or 0) > 0 then
|
||||
remaining = 0
|
||||
end
|
||||
return remaining, total, curCharges, maxChargeCount
|
||||
end
|
||||
|
||||
if GetSpellCooldownInfo then
|
||||
local cdStart, cdDuration = GetSpellCooldownInfo(spellId)
|
||||
cdStart = tonumber(cdStart) or 0
|
||||
cdDuration = tonumber(cdDuration) or 0
|
||||
if cdDuration > 0 then
|
||||
local remaining = math.max(0, cdDuration - (GetTime() - cdStart))
|
||||
remaining = math.max(0, math.min(cdDuration, remaining))
|
||||
if cdData and (tonumber(cdData.maxCharges) or 0) <= 0 then
|
||||
local cachedRemaining = (tonumber(cdData.duration) or 0) - (GetTime() - (tonumber(cdData.startTime) or GetTime()))
|
||||
cachedRemaining = math.max(0, math.min(tonumber(cdData.duration) or cachedRemaining, cachedRemaining))
|
||||
local cachedDuration = math.max(0, tonumber(cdData.duration) or 0)
|
||||
if cachedDuration > 2.0 and cachedRemaining > 2.0 and cdDuration < math.max(2.0, cachedDuration * 0.35) then
|
||||
return cachedRemaining, cachedDuration, nil, nil
|
||||
end
|
||||
end
|
||||
return remaining, cdDuration, nil, nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not cdData then
|
||||
if isOwnPlayer and knownMaxCharges > 1 then
|
||||
return 0, math.max(0, knownChargeDuration or effectiveCd or 0), knownMaxCharges, knownMaxCharges
|
||||
end
|
||||
return 0, 0, nil, nil
|
||||
end
|
||||
if (tonumber(cdData.maxCharges) or 0) > 0 then
|
||||
local remaining, chargeDur, charges, maxCharges = self:ResolveChargeState(cdData)
|
||||
self:StoreKnownChargeInfo(spellId, maxCharges, chargeDur)
|
||||
if deferUntilEmpty and charges > 0 then
|
||||
remaining = 0
|
||||
end
|
||||
return remaining, chargeDur, charges, maxCharges
|
||||
end
|
||||
if isOwnPlayer and knownMaxCharges > 1 then
|
||||
local remaining = (tonumber(cdData.duration) or 0) - (GetTime() - (tonumber(cdData.startTime) or GetTime()))
|
||||
remaining = math.max(0, math.min(tonumber(cdData.duration) or remaining, remaining))
|
||||
local currentCharges = knownMaxCharges
|
||||
if remaining > 0 then
|
||||
currentCharges = math.max(0, knownMaxCharges - 1)
|
||||
end
|
||||
if deferUntilEmpty and currentCharges > 0 then
|
||||
remaining = 0
|
||||
end
|
||||
return remaining, math.max(0, knownChargeDuration or effectiveCd or 0), currentCharges, knownMaxCharges
|
||||
end
|
||||
local remaining = cdData.duration - (GetTime() - cdData.startTime)
|
||||
remaining = math.max(0, math.min(cdData.duration, remaining))
|
||||
return remaining, cdData.duration, nil, nil
|
||||
end
|
||||
|
||||
function HMGT:ShouldDisplayEntry(settings, remaining, currentCharges, maxCharges, spellEntry)
|
||||
local rem = tonumber(remaining) or 0
|
||||
local cur = tonumber(currentCharges) or 0
|
||||
local max = tonumber(maxCharges) or 0
|
||||
local soon = tonumber(settings.readySoonSec) or 0
|
||||
local isAvailabilitySpell = spellEntry and self:IsAvailabilitySpell(spellEntry) or false
|
||||
local isReady
|
||||
|
||||
if isAvailabilitySpell then
|
||||
isReady = max > 0 and cur >= max
|
||||
else
|
||||
isReady = rem <= 0 or (max > 0 and cur > 0)
|
||||
end
|
||||
|
||||
if settings.showOnlyReady then
|
||||
return isReady
|
||||
end
|
||||
if soon > 0 then
|
||||
if isAvailabilitySpell then
|
||||
return isReady
|
||||
end
|
||||
return isReady or rem <= soon
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local DEFAULT_CATEGORY_PRIORITY = {
|
||||
interrupt = 1,
|
||||
lust = 2,
|
||||
defensive = 3,
|
||||
tank = 4,
|
||||
healing = 5,
|
||||
offensive = 6,
|
||||
utility = 7,
|
||||
cc = 8,
|
||||
}
|
||||
|
||||
local TRACKER_CATEGORY_PRIORITY = {
|
||||
interruptTracker = {
|
||||
interrupt = 1,
|
||||
defensive = 2,
|
||||
utility = 3,
|
||||
cc = 4,
|
||||
healing = 5,
|
||||
tank = 6,
|
||||
offensive = 7,
|
||||
lust = 8,
|
||||
},
|
||||
raidCooldownTracker = {
|
||||
lust = 1,
|
||||
defensive = 2,
|
||||
healing = 3,
|
||||
tank = 4,
|
||||
utility = 5,
|
||||
offensive = 6,
|
||||
cc = 7,
|
||||
interrupt = 8,
|
||||
},
|
||||
groupCooldownTracker = {
|
||||
tank = 1,
|
||||
defensive = 2,
|
||||
healing = 3,
|
||||
cc = 4,
|
||||
utility = 5,
|
||||
offensive = 6,
|
||||
lust = 7,
|
||||
interrupt = 8,
|
||||
},
|
||||
}
|
||||
|
||||
local function GetCategoryPriority(category, trackerKey)
|
||||
local cat = tostring(category or "utility")
|
||||
local trackerOrder = trackerKey and TRACKER_CATEGORY_PRIORITY[trackerKey]
|
||||
if trackerOrder and trackerOrder[cat] then
|
||||
return trackerOrder[cat]
|
||||
end
|
||||
local order = HMGT_SpellData and HMGT_SpellData.CategoryOrder
|
||||
if type(order) == "table" then
|
||||
for idx, key in ipairs(order) do
|
||||
if key == cat then
|
||||
return idx
|
||||
end
|
||||
end
|
||||
return #order + 10
|
||||
end
|
||||
return DEFAULT_CATEGORY_PRIORITY[cat] or 99
|
||||
end
|
||||
|
||||
function HMGT:SortDisplayEntries(entries, trackerKey)
|
||||
if type(entries) ~= "table" then return end
|
||||
table.sort(entries, function(a, b)
|
||||
local aRemaining = tonumber(a and a.remaining) or 0
|
||||
local bRemaining = tonumber(b and b.remaining) or 0
|
||||
local aActive = aRemaining > 0
|
||||
local bActive = bRemaining > 0
|
||||
if aActive ~= bActive then
|
||||
return aActive
|
||||
end
|
||||
|
||||
local aEntry = a and a.spellEntry
|
||||
local bEntry = b and b.spellEntry
|
||||
|
||||
local aPriority = tonumber(aEntry and aEntry.priority) or GetCategoryPriority(aEntry and aEntry.category, trackerKey)
|
||||
local bPriority = tonumber(bEntry and bEntry.priority) or GetCategoryPriority(bEntry and bEntry.category, trackerKey)
|
||||
if aPriority ~= bPriority then
|
||||
return aPriority < bPriority
|
||||
end
|
||||
|
||||
if aActive and aRemaining ~= bRemaining then
|
||||
return aRemaining < bRemaining
|
||||
end
|
||||
|
||||
local aTotal = tonumber(a and a.total)
|
||||
or tonumber(aEntry and HMGT_SpellData.GetBaseCooldown and HMGT_SpellData.GetBaseCooldown(aEntry))
|
||||
or tonumber(aEntry and aEntry.cooldown)
|
||||
or 0
|
||||
local bTotal = tonumber(b and b.total)
|
||||
or tonumber(bEntry and HMGT_SpellData.GetBaseCooldown and HMGT_SpellData.GetBaseCooldown(bEntry))
|
||||
or tonumber(bEntry and bEntry.cooldown)
|
||||
or 0
|
||||
if (not aActive) and aTotal ~= bTotal then
|
||||
return aTotal > bTotal
|
||||
end
|
||||
|
||||
if aRemaining ~= bRemaining then
|
||||
return aRemaining < bRemaining
|
||||
end
|
||||
|
||||
local aName = tostring(a and a.playerName or "")
|
||||
local bName = tostring(b and b.playerName or "")
|
||||
if aName ~= bName then
|
||||
return aName < bName
|
||||
end
|
||||
|
||||
local aSpell = tonumber(aEntry and aEntry.spellId) or 0
|
||||
local bSpell = tonumber(bEntry and bEntry.spellId) or 0
|
||||
return aSpell < bSpell
|
||||
end)
|
||||
end
|
||||
524
Modules/Tracker/TrackerDetection.lua
Normal file
524
Modules/Tracker/TrackerDetection.lua
Normal file
@@ -0,0 +1,524 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.TrackerDetection = HMGT.TrackerDetection or {}
|
||||
|
||||
local internals = HMGT.TrackerInternals or {}
|
||||
local GetSpellChargesInfo = internals.GetSpellChargesInfo
|
||||
local GetSpellCooldownInfo = internals.GetSpellCooldownInfo
|
||||
local GetGlobalCooldownInfo = internals.GetGlobalCooldownInfo
|
||||
local GetSpellDebugLabel = internals.GetSpellDebugLabel
|
||||
local BuildCooldownStateFingerprint = internals.BuildCooldownStateFingerprint
|
||||
local ApplyOwnCooldownReducers = internals.ApplyOwnCooldownReducers
|
||||
local ApplyObservedCooldownReducers = internals.ApplyObservedCooldownReducers
|
||||
|
||||
function HMGT:HandleOwnSpellCast(spellId)
|
||||
local isInterrupt = HMGT_SpellData.InterruptLookup[spellId] ~= nil
|
||||
local isCooldown = HMGT_SpellData.CooldownLookup[spellId] ~= nil
|
||||
if not isInterrupt and not isCooldown then return end
|
||||
|
||||
local spellEntry = HMGT_SpellData.InterruptLookup[spellId]
|
||||
or HMGT_SpellData.CooldownLookup[spellId]
|
||||
spellId = tonumber(spellEntry and spellEntry.spellId) or spellId
|
||||
local name = self:NormalizePlayerName(UnitName("player"))
|
||||
local pData = self.playerData[name]
|
||||
local talents = pData and pData.talents or {}
|
||||
if self:IsAvailabilitySpell(spellEntry) then
|
||||
self:LogTrackedSpellCast(name, spellEntry, {
|
||||
stateKind = "availability",
|
||||
required = HMGT_SpellData.GetEffectiveAvailabilityRequired(spellEntry, talents),
|
||||
})
|
||||
if self:RefreshOwnAvailabilitySpell(spellEntry) then
|
||||
self:PublishOwnSpellState(spellId, { sendLegacy = true })
|
||||
end
|
||||
self:TriggerTrackerUpdate()
|
||||
return
|
||||
end
|
||||
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local now = GetTime()
|
||||
local inCombat = InCombatLockdown and InCombatLockdown()
|
||||
local cur, max, chargeStart, chargeDuration = nil, nil, nil, nil
|
||||
if not inCombat and GetSpellChargesInfo then
|
||||
cur, max, chargeStart, chargeDuration = GetSpellChargesInfo(spellId)
|
||||
end
|
||||
local cachedMaxCharges, cachedChargeDuration = self:GetKnownChargeInfo(
|
||||
spellEntry,
|
||||
talents,
|
||||
spellId,
|
||||
(not inCombat and tonumber(chargeDuration) and tonumber(chargeDuration) > 0) and tonumber(chargeDuration) or effectiveCd
|
||||
)
|
||||
local inferredMaxCharges, inferredChargeDuration = HMGT_SpellData.GetEffectiveChargeInfo(
|
||||
spellEntry,
|
||||
talents,
|
||||
(not inCombat and tonumber(max) and tonumber(max) > 0) and tonumber(max) or ((cachedMaxCharges > 0) and cachedMaxCharges or nil),
|
||||
(not inCombat and tonumber(chargeDuration) and tonumber(chargeDuration) > 0) and tonumber(chargeDuration)
|
||||
or ((cachedChargeDuration > 0) and cachedChargeDuration or effectiveCd)
|
||||
)
|
||||
|
||||
local hasCharges = ((tonumber(max) or 0) > 1) or (tonumber(inferredMaxCharges) or 0) > 1
|
||||
local currentCharges = 0
|
||||
local maxCharges = 0
|
||||
local chargeDur = 0
|
||||
local chargeStartTime = nil
|
||||
|
||||
local startTime = now
|
||||
local duration = effectiveCd
|
||||
local expiresIn = effectiveCd
|
||||
|
||||
local existingCd = self:GetActiveCooldown(name, spellId)
|
||||
if existingCd and (tonumber(existingCd.maxCharges) or 0) > 0 then
|
||||
self:ResolveChargeState(existingCd, now)
|
||||
end
|
||||
|
||||
if hasCharges then
|
||||
maxCharges = math.max(1, tonumber(max) or cachedMaxCharges or tonumber(inferredMaxCharges) or 1)
|
||||
currentCharges = tonumber(cur)
|
||||
if currentCharges == nil then
|
||||
local prevCharges = existingCd and tonumber(existingCd.currentCharges)
|
||||
local prevMax = existingCd and tonumber(existingCd.maxCharges)
|
||||
if prevCharges and prevMax and prevMax == maxCharges then
|
||||
currentCharges = math.max(0, prevCharges - 1)
|
||||
else
|
||||
currentCharges = math.max(0, maxCharges - 1)
|
||||
end
|
||||
end
|
||||
currentCharges = math.max(0, math.min(maxCharges, currentCharges))
|
||||
|
||||
chargeDur = tonumber(chargeDuration)
|
||||
or cachedChargeDuration
|
||||
or tonumber(inferredChargeDuration)
|
||||
or tonumber(effectiveCd)
|
||||
or 0
|
||||
chargeDur = math.max(0, chargeDur)
|
||||
self:StoreKnownChargeInfo(spellId, maxCharges, chargeDur)
|
||||
|
||||
if currentCharges < maxCharges and chargeDur > 0 then
|
||||
chargeStartTime = tonumber(chargeStart) or now
|
||||
local missing = maxCharges - currentCharges
|
||||
startTime = chargeStartTime
|
||||
duration = missing * chargeDur
|
||||
expiresIn = math.max(0, duration - (now - startTime))
|
||||
else
|
||||
startTime = now
|
||||
duration = 0
|
||||
expiresIn = 0
|
||||
end
|
||||
end
|
||||
|
||||
self:Debug(
|
||||
"verbose",
|
||||
"HandleOwnSpellCast name=%s spellId=%s cd=%.2f charges=%s/%s",
|
||||
tostring(name),
|
||||
tostring(spellId),
|
||||
tonumber(effectiveCd) or 0,
|
||||
hasCharges and tostring(currentCharges) or "-",
|
||||
hasCharges and tostring(maxCharges) or "-"
|
||||
)
|
||||
|
||||
self._cdNonce = (self._cdNonce or 0) + 1
|
||||
local nonce = self._cdNonce
|
||||
|
||||
self:SetActiveCooldown(name, spellId, {
|
||||
startTime = startTime,
|
||||
duration = duration,
|
||||
spellEntry = spellEntry,
|
||||
currentCharges = hasCharges and currentCharges or nil,
|
||||
maxCharges = hasCharges and maxCharges or nil,
|
||||
chargeStart = hasCharges and chargeStartTime or nil,
|
||||
chargeDuration = hasCharges and chargeDur or nil,
|
||||
_nonce = nonce,
|
||||
})
|
||||
|
||||
self:LogTrackedSpellCast(name, spellEntry, {
|
||||
cooldown = effectiveCd,
|
||||
currentCharges = hasCharges and currentCharges or nil,
|
||||
maxCharges = hasCharges and maxCharges or nil,
|
||||
chargeCooldown = hasCharges and chargeDur or nil,
|
||||
})
|
||||
|
||||
if expiresIn > 0 then
|
||||
self:ScheduleTimer(function()
|
||||
local current = self:GetActiveCooldown(name, spellId)
|
||||
if current and current._nonce == nonce then
|
||||
self:ClearActiveCooldown(name, spellId)
|
||||
self:PublishOwnSpellState(spellId)
|
||||
self:TriggerTrackerUpdate()
|
||||
end
|
||||
end, expiresIn)
|
||||
end
|
||||
|
||||
self:PublishOwnSpellState(spellId, { sendLegacy = true })
|
||||
self:TriggerTrackerUpdate()
|
||||
end
|
||||
|
||||
function HMGT:RefreshOwnCooldownStateFromGame(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid then return false end
|
||||
if InCombatLockdown and InCombatLockdown() then
|
||||
return false
|
||||
end
|
||||
|
||||
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||
if not ownName then return false end
|
||||
|
||||
local spellEntry = HMGT_SpellData.InterruptLookup[sid]
|
||||
or HMGT_SpellData.CooldownLookup[sid]
|
||||
if not spellEntry or self:IsAvailabilitySpell(spellEntry) then
|
||||
return false
|
||||
end
|
||||
sid = tonumber(spellEntry.spellId) or sid
|
||||
|
||||
local existing = self:GetActiveCooldown(ownName, sid)
|
||||
local before = BuildCooldownStateFingerprint and BuildCooldownStateFingerprint(existing) or "nil"
|
||||
local now = GetTime()
|
||||
local pData = self.playerData[ownName]
|
||||
local talents = pData and pData.talents or {}
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local cur, max, chargeStart, chargeDuration = nil, nil, nil, nil
|
||||
if GetSpellChargesInfo then
|
||||
cur, max, chargeStart, chargeDuration = GetSpellChargesInfo(sid)
|
||||
end
|
||||
local inferredMaxCharges, inferredChargeDuration = HMGT_SpellData.GetEffectiveChargeInfo(
|
||||
spellEntry,
|
||||
talents,
|
||||
(tonumber(max) and tonumber(max) > 0) and tonumber(max) or nil,
|
||||
(tonumber(chargeDuration) and tonumber(chargeDuration) > 0) and tonumber(chargeDuration) or effectiveCd
|
||||
)
|
||||
|
||||
local hasCharges = ((tonumber(max) or 0) > 1) or (tonumber(inferredMaxCharges) or 0) > 1
|
||||
|
||||
if hasCharges then
|
||||
local maxCharges = math.max(1, tonumber(max) or tonumber(inferredMaxCharges) or 1)
|
||||
local currentCharges = tonumber(cur)
|
||||
if currentCharges == nil then
|
||||
currentCharges = maxCharges
|
||||
end
|
||||
currentCharges = math.max(0, math.min(maxCharges, currentCharges))
|
||||
|
||||
local chargeDur = tonumber(chargeDuration) or tonumber(inferredChargeDuration) or tonumber(effectiveCd) or 0
|
||||
chargeDur = math.max(0, chargeDur)
|
||||
|
||||
if currentCharges < maxCharges and chargeDur > 0 then
|
||||
local chargeStartTime = tonumber(chargeStart) or now
|
||||
local missing = maxCharges - currentCharges
|
||||
local updatedEntry = self:SetActiveCooldown(ownName, sid, {
|
||||
startTime = chargeStartTime,
|
||||
duration = missing * chargeDur,
|
||||
spellEntry = spellEntry,
|
||||
currentCharges = currentCharges,
|
||||
maxCharges = maxCharges,
|
||||
chargeStart = chargeStartTime,
|
||||
chargeDuration = chargeDur,
|
||||
})
|
||||
self:RefreshCooldownExpiryTimer(ownName, sid, updatedEntry)
|
||||
else
|
||||
self:ClearActiveCooldown(ownName, sid)
|
||||
end
|
||||
else
|
||||
local cooldownStart, cooldownDuration = 0, 0
|
||||
if GetSpellCooldownInfo then
|
||||
cooldownStart, cooldownDuration = GetSpellCooldownInfo(sid)
|
||||
end
|
||||
cooldownStart = tonumber(cooldownStart) or 0
|
||||
cooldownDuration = tonumber(cooldownDuration) or 0
|
||||
local gcdStart, gcdDuration = 0, 0
|
||||
if GetGlobalCooldownInfo then
|
||||
gcdStart, gcdDuration = GetGlobalCooldownInfo()
|
||||
end
|
||||
gcdStart = tonumber(gcdStart) or 0
|
||||
gcdDuration = tonumber(gcdDuration) or 0
|
||||
local existingDuration = tonumber(existing and existing.duration) or 0
|
||||
local existingStart = tonumber(existing and existing.startTime) or now
|
||||
local existingRemaining = math.max(0, existingDuration - (now - existingStart))
|
||||
|
||||
local isLikelyGlobalCooldown = cooldownDuration > 0
|
||||
and gcdDuration > 0
|
||||
and math.abs(cooldownDuration - gcdDuration) <= 0.15
|
||||
and (tonumber(effectiveCd) or 0) > (gcdDuration + 1.0)
|
||||
|
||||
local isSuspiciousShortRefresh = cooldownDuration > 0
|
||||
and existingRemaining > 2.0
|
||||
and existingDuration > 2.0
|
||||
and cooldownDuration < math.max(2.0, existingDuration * 0.35)
|
||||
and cooldownDuration < math.max(2.0, (tonumber(effectiveCd) or 0) * 0.35)
|
||||
|
||||
if isLikelyGlobalCooldown or isSuspiciousShortRefresh then
|
||||
self:DebugScoped(
|
||||
"verbose",
|
||||
"TrackedSpells",
|
||||
"Ignore suspicious refresh for %s: spellCD=%.3f gcd=%.3f existing=%.3f remaining=%.3f effective=%.3f",
|
||||
GetSpellDebugLabel and GetSpellDebugLabel(sid) or tostring(sid),
|
||||
cooldownDuration,
|
||||
gcdDuration,
|
||||
existingDuration,
|
||||
existingRemaining,
|
||||
tonumber(effectiveCd) or 0
|
||||
)
|
||||
return false
|
||||
end
|
||||
|
||||
if cooldownDuration > 0 then
|
||||
local updatedEntry = self:SetActiveCooldown(ownName, sid, {
|
||||
startTime = cooldownStart,
|
||||
duration = cooldownDuration,
|
||||
spellEntry = spellEntry,
|
||||
})
|
||||
self:RefreshCooldownExpiryTimer(ownName, sid, updatedEntry)
|
||||
else
|
||||
self:ClearActiveCooldown(ownName, sid)
|
||||
end
|
||||
end
|
||||
|
||||
local after = BuildCooldownStateFingerprint and BuildCooldownStateFingerprint(self:GetActiveCooldown(ownName, sid)) or "nil"
|
||||
return before ~= after
|
||||
end
|
||||
|
||||
function HMGT:DidOwnInterruptSucceed(triggerSpellId, talents)
|
||||
local sid = tonumber(triggerSpellId)
|
||||
if not sid then return false end
|
||||
|
||||
local spellEntry = HMGT_SpellData and HMGT_SpellData.InterruptLookup and HMGT_SpellData.InterruptLookup[sid]
|
||||
if not spellEntry then return false end
|
||||
sid = tonumber(spellEntry.spellId) or sid
|
||||
|
||||
local observedDuration = 0
|
||||
if GetSpellCooldownInfo then
|
||||
local _, duration = GetSpellCooldownInfo(sid)
|
||||
observedDuration = duration
|
||||
end
|
||||
observedDuration = tonumber(observedDuration) or 0
|
||||
if observedDuration <= 0 then return false end
|
||||
|
||||
local expectedDuration = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
expectedDuration = tonumber(expectedDuration) or 0
|
||||
if expectedDuration <= 0 then return false end
|
||||
|
||||
return observedDuration < (expectedDuration - 0.05)
|
||||
end
|
||||
|
||||
function HMGT:HandleOwnCooldownReductionTrigger(triggerSpellId)
|
||||
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||
if not ownName then return end
|
||||
|
||||
local pData = self.playerData[ownName]
|
||||
local classToken = pData and pData.class or select(2, UnitClass("player"))
|
||||
local specIndex = pData and pData.specIndex or GetSpecialization()
|
||||
local talents = pData and pData.talents or {}
|
||||
if not classToken or not specIndex then return end
|
||||
|
||||
local reducers = HMGT_SpellData.GetCooldownReducersForCast(classToken, specIndex, triggerSpellId, talents)
|
||||
if not reducers or #reducers == 0 then return end
|
||||
|
||||
local instantReducers = {}
|
||||
local observedInstantReducers = {}
|
||||
local successReducers = {}
|
||||
local observedSuccessReducers = {}
|
||||
for _, reducer in ipairs(reducers) do
|
||||
local observed = type(reducer.observe) == "table"
|
||||
if reducer.requireInterruptSuccess then
|
||||
if observed then
|
||||
observedSuccessReducers[#observedSuccessReducers + 1] = reducer
|
||||
else
|
||||
successReducers[#successReducers + 1] = reducer
|
||||
end
|
||||
else
|
||||
if observed then
|
||||
observedInstantReducers[#observedInstantReducers + 1] = reducer
|
||||
else
|
||||
instantReducers[#instantReducers + 1] = reducer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local castTs = GetServerTime()
|
||||
if #instantReducers > 0 and ApplyOwnCooldownReducers then
|
||||
ApplyOwnCooldownReducers(self, ownName, triggerSpellId, instantReducers, castTs)
|
||||
end
|
||||
if #observedInstantReducers > 0 and ApplyObservedCooldownReducers then
|
||||
ApplyObservedCooldownReducers(self, ownName, observedInstantReducers)
|
||||
end
|
||||
|
||||
if #successReducers > 0 or #observedSuccessReducers > 0 then
|
||||
local function ApplySuccessReducers()
|
||||
if not self:DidOwnInterruptSucceed(triggerSpellId, talents) then
|
||||
return false
|
||||
end
|
||||
if #successReducers > 0 and ApplyOwnCooldownReducers then
|
||||
ApplyOwnCooldownReducers(self, ownName, triggerSpellId, successReducers, castTs)
|
||||
end
|
||||
if #observedSuccessReducers > 0 and ApplyObservedCooldownReducers then
|
||||
ApplyObservedCooldownReducers(self, ownName, observedSuccessReducers)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if not ApplySuccessReducers() then
|
||||
C_Timer.After(0.12, function()
|
||||
if not self or not self.playerData or not self.playerData[ownName] then
|
||||
return
|
||||
end
|
||||
ApplySuccessReducers()
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function HMGT:HandleRemoteSpellCast(playerName, spellId, castTimestamp, curCharges, maxCharges, chargeRemaining, chargeDuration)
|
||||
local spellEntry = HMGT_SpellData.InterruptLookup[spellId]
|
||||
or HMGT_SpellData.CooldownLookup[spellId]
|
||||
if not spellEntry then return end
|
||||
spellId = tonumber(spellEntry.spellId) or spellId
|
||||
if self:IsAvailabilitySpell(spellEntry) then return end
|
||||
|
||||
local pData = self.playerData[playerName]
|
||||
local talents = pData and pData.talents or {}
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
|
||||
castTimestamp = tonumber(castTimestamp) or GetServerTime()
|
||||
local existingEntry = self:GetActiveCooldown(playerName, spellId)
|
||||
if (tonumber(maxCharges) or 0) <= 0 and existingEntry and existingEntry.lastCastTimestamp then
|
||||
local prevTs = tonumber(existingEntry.lastCastTimestamp) or 0
|
||||
if math.abs(prevTs - castTimestamp) <= 1 then
|
||||
return
|
||||
end
|
||||
end
|
||||
local now = GetTime()
|
||||
local elapsed = math.max(0, GetServerTime() - castTimestamp)
|
||||
|
||||
local incomingCur = tonumber(curCharges) or 0
|
||||
local incomingMax = tonumber(maxCharges) or 0
|
||||
local incomingChargeRemaining = tonumber(chargeRemaining) or 0
|
||||
local incomingChargeDuration = tonumber(chargeDuration) or 0
|
||||
|
||||
local inferredMaxCharges, inferredChargeDuration = HMGT_SpellData.GetEffectiveChargeInfo(
|
||||
spellEntry,
|
||||
talents,
|
||||
(incomingMax > 0) and incomingMax or nil,
|
||||
(incomingChargeDuration > 0) and incomingChargeDuration or effectiveCd
|
||||
)
|
||||
local hasCharges = (incomingMax > 1) or (tonumber(inferredMaxCharges) or 0) > 1
|
||||
|
||||
local currentCharges = 0
|
||||
local maxChargeCount = 0
|
||||
local chargeDur = 0
|
||||
local nextChargeRemaining = 0
|
||||
local chargeStartTime = nil
|
||||
local startTime, duration, expiresIn
|
||||
|
||||
if hasCharges then
|
||||
maxChargeCount = math.max(1, (incomingMax > 0 and incomingMax) or tonumber(inferredMaxCharges) or 1)
|
||||
chargeDur = tonumber(incomingChargeDuration) or tonumber(inferredChargeDuration) or tonumber(effectiveCd) or 0
|
||||
chargeDur = math.max(0, chargeDur)
|
||||
if chargeDur <= 0 then
|
||||
chargeDur = math.max(0, tonumber(effectiveCd) or 0)
|
||||
end
|
||||
|
||||
if incomingMax > 0 then
|
||||
currentCharges = math.max(0, math.min(maxChargeCount, incomingCur))
|
||||
nextChargeRemaining = math.max(0, math.min(chargeDur, incomingChargeRemaining - elapsed))
|
||||
if currentCharges < maxChargeCount and chargeDur > 0 then
|
||||
chargeStartTime = now - math.max(0, chargeDur - nextChargeRemaining)
|
||||
end
|
||||
else
|
||||
local existing = self:GetActiveCooldown(playerName, spellId)
|
||||
if existing and (tonumber(existing.maxCharges) or 0) == maxChargeCount then
|
||||
self:ResolveChargeState(existing, now)
|
||||
local prevCharges = tonumber(existing.currentCharges) or maxChargeCount
|
||||
local prevStart = tonumber(existing.chargeStart)
|
||||
local prevDur = tonumber(existing.chargeDuration) or chargeDur
|
||||
if prevDur > 0 then
|
||||
chargeDur = prevDur
|
||||
end
|
||||
|
||||
currentCharges = math.max(0, prevCharges - 1)
|
||||
if currentCharges < maxChargeCount and chargeDur > 0 then
|
||||
if prevCharges >= maxChargeCount then
|
||||
chargeStartTime = now
|
||||
else
|
||||
chargeStartTime = prevStart or now
|
||||
end
|
||||
nextChargeRemaining = math.max(0, chargeDur - (now - chargeStartTime))
|
||||
end
|
||||
else
|
||||
currentCharges = math.max(0, maxChargeCount - 1)
|
||||
if currentCharges < maxChargeCount and chargeDur > 0 then
|
||||
chargeStartTime = now
|
||||
nextChargeRemaining = chargeDur
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if currentCharges >= maxChargeCount and maxChargeCount > 0 then
|
||||
currentCharges = math.max(0, maxChargeCount - 1)
|
||||
if chargeDur > 0 then
|
||||
chargeStartTime = now
|
||||
nextChargeRemaining = chargeDur
|
||||
end
|
||||
end
|
||||
|
||||
if currentCharges < maxChargeCount and chargeDur > 0 then
|
||||
chargeStartTime = chargeStartTime or now
|
||||
local missing = maxChargeCount - currentCharges
|
||||
startTime = chargeStartTime
|
||||
duration = missing * chargeDur
|
||||
expiresIn = math.max(0, duration - (now - startTime))
|
||||
else
|
||||
startTime = now
|
||||
duration = 0
|
||||
expiresIn = 0
|
||||
end
|
||||
else
|
||||
local remaining = effectiveCd - elapsed
|
||||
if remaining <= 0 then return end
|
||||
startTime = now - elapsed
|
||||
duration = effectiveCd
|
||||
expiresIn = remaining
|
||||
end
|
||||
|
||||
self:Debug(
|
||||
"verbose",
|
||||
"HandleRemoteSpellCast name=%s spellId=%s elapsed=%.2f expiresIn=%.2f charges=%s/%s",
|
||||
tostring(playerName),
|
||||
tostring(spellId),
|
||||
tonumber(elapsed) or 0,
|
||||
tonumber(expiresIn) or 0,
|
||||
hasCharges and tostring(currentCharges) or "-",
|
||||
hasCharges and tostring(maxChargeCount) or "-"
|
||||
)
|
||||
|
||||
self._cdNonce = (self._cdNonce or 0) + 1
|
||||
local nonce = self._cdNonce
|
||||
|
||||
self:SetActiveCooldown(playerName, spellId, {
|
||||
startTime = startTime,
|
||||
duration = duration,
|
||||
spellEntry = spellEntry,
|
||||
currentCharges = hasCharges and currentCharges or nil,
|
||||
maxCharges = hasCharges and maxChargeCount or nil,
|
||||
chargeStart = hasCharges and chargeStartTime or nil,
|
||||
chargeDuration = hasCharges and chargeDur or nil,
|
||||
lastCastTimestamp = castTimestamp,
|
||||
_nonce = nonce,
|
||||
})
|
||||
|
||||
self:LogTrackedSpellCast(playerName, spellEntry, {
|
||||
cooldown = effectiveCd,
|
||||
currentCharges = hasCharges and currentCharges or nil,
|
||||
maxCharges = hasCharges and maxChargeCount or nil,
|
||||
chargeCooldown = hasCharges and chargeDur or nil,
|
||||
})
|
||||
|
||||
if expiresIn > 0 then
|
||||
self:ScheduleTimer(function()
|
||||
local current = self:GetActiveCooldown(playerName, spellId)
|
||||
if current and current._nonce == nonce then
|
||||
self:ClearActiveCooldown(playerName, spellId)
|
||||
self:TriggerTrackerUpdate()
|
||||
end
|
||||
end, expiresIn)
|
||||
end
|
||||
|
||||
self:TriggerTrackerUpdate()
|
||||
end
|
||||
@@ -94,25 +94,6 @@ local PARTY_FRAME_PATTERNS = {
|
||||
|
||||
local unitFrameCache = {}
|
||||
|
||||
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
|
||||
|
||||
local function BuildAnchorLayoutSignature(settings, ordered, unitByPlayer)
|
||||
local parts = {
|
||||
settings.attachToPartyFrame == true and "attach" or "stack",
|
||||
@@ -222,55 +203,6 @@ local function ResolveUnitAnchorFrame(unitId)
|
||||
return nil
|
||||
end
|
||||
|
||||
local function GetGroupPlayers(tracker)
|
||||
local players = {}
|
||||
|
||||
local ownName = HMGT:NormalizePlayerName(UnitName("player"))
|
||||
local ownClass = select(2, UnitClass("player"))
|
||||
local includeOwnPlayer = true
|
||||
if IsGroupTracker(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 = HMGT: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 = HMGT: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
|
||||
|
||||
local function GetTrackerLabel(tracker)
|
||||
if type(tracker) ~= "table" then
|
||||
return "Tracker"
|
||||
@@ -287,136 +219,6 @@ local function GetTrackerLabel(tracker)
|
||||
return "Tracker"
|
||||
end
|
||||
|
||||
local function GetTrackerSpellPool(categories)
|
||||
if HMGT_SpellData and type(HMGT_SpellData.GetSpellPoolForCategories) == "function" then
|
||||
return HMGT_SpellData.GetSpellPoolForCategories(categories)
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
local function GetTrackerSpellsForPlayer(classToken, specIndex, categories)
|
||||
if HMGT_SpellData and type(HMGT_SpellData.GetSpellsForCategories) == "function" then
|
||||
return HMGT_SpellData.GetSpellsForCategories(classToken, specIndex, categories)
|
||||
end
|
||||
return {}
|
||||
end
|
||||
|
||||
local function CollectEntriesForPlayer(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 = HMGT.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 = GetTrackerSpellsForPlayer(classToken, specIndex, tracker.categories)
|
||||
for _, spellEntry in ipairs(spells) do
|
||||
if tracker.enabledSpells[spellEntry.spellId] ~= false then
|
||||
local remaining, total, currentCharges, maxCharges = HMGT:GetCooldownInfo(playerName, spellEntry.spellId)
|
||||
local effectiveCd = HMGT_SpellData.GetEffectiveCooldown(spellEntry, talents)
|
||||
local isAvailabilitySpell = HMGT:IsAvailabilitySpell(spellEntry)
|
||||
local include = HMGT:ShouldDisplayEntry(tracker, remaining, currentCharges, maxCharges, spellEntry)
|
||||
local spellKnown = HMGT: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 HMGT: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
|
||||
|
||||
local function GetAvailablePartyPreviewUnits()
|
||||
local units = {}
|
||||
for index = 1, 4 do
|
||||
local unitId = "party" .. index
|
||||
if ResolveUnitAnchorFrame(unitId) then
|
||||
units[#units + 1] = {
|
||||
playerName = string.format("Party %d", index),
|
||||
unitId = unitId,
|
||||
}
|
||||
end
|
||||
end
|
||||
return units
|
||||
end
|
||||
|
||||
local function BuildPartyPreviewEntries(entries)
|
||||
local byPlayer = {}
|
||||
local order = {}
|
||||
local unitByPlayer = {}
|
||||
local previewUnits = GetAvailablePartyPreviewUnits()
|
||||
|
||||
for _, previewUnit in ipairs(previewUnits) do
|
||||
local playerName = previewUnit.playerName
|
||||
local playerEntries = CopyEntriesForPreview(entries, playerName)
|
||||
if #playerEntries > 0 then
|
||||
byPlayer[playerName] = playerEntries
|
||||
order[#order + 1] = playerName
|
||||
unitByPlayer[playerName] = previewUnit.unitId
|
||||
end
|
||||
end
|
||||
|
||||
return byPlayer, order, unitByPlayer, #order > 0
|
||||
end
|
||||
|
||||
local function SortTrackers(trackers)
|
||||
table.sort(trackers, function(a, b)
|
||||
local aId = tonumber(a and a.id) or 0
|
||||
@@ -467,13 +269,7 @@ Manager._shared.ShortName = ShortName
|
||||
Manager._shared.BuildAnchorLayoutSignature = BuildAnchorLayoutSignature
|
||||
Manager._shared.IsGroupTracker = IsGroupTracker
|
||||
Manager._shared.ResolveUnitAnchorFrame = ResolveUnitAnchorFrame
|
||||
Manager._shared.GetGroupPlayers = GetGroupPlayers
|
||||
Manager._shared.GetTrackerLabel = GetTrackerLabel
|
||||
Manager._shared.GetTrackerSpellPool = GetTrackerSpellPool
|
||||
Manager._shared.GetTrackerSpellsForPlayer = GetTrackerSpellsForPlayer
|
||||
Manager._shared.CollectEntriesForPlayer = CollectEntriesForPlayer
|
||||
Manager._shared.BuildPartyPreviewEntries = BuildPartyPreviewEntries
|
||||
Manager._shared.EntryNeedsVisualTicker = EntryNeedsVisualTicker
|
||||
Manager._shared.BuildGroupDisplaySignature = BuildGroupDisplaySignature
|
||||
|
||||
function Manager:GetTrackers()
|
||||
@@ -492,6 +288,13 @@ function Manager:GetTrackers()
|
||||
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
|
||||
@@ -648,12 +451,8 @@ function Manager:RefreshVisibleVisuals()
|
||||
break
|
||||
end
|
||||
local entries = byPlayer[playerName] or {}
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
end
|
||||
local tickThis = false
|
||||
entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
|
||||
if #entries == 0 then
|
||||
needsFullRefresh = true
|
||||
break
|
||||
@@ -666,11 +465,8 @@ function Manager:RefreshVisibleVisuals()
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
totalEntries = totalEntries + #entries
|
||||
byPlayerFiltered[playerName] = entries
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
if tickThis then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
local newSignature = BuildGroupDisplaySignature(currentOrder, byPlayerFiltered)
|
||||
@@ -686,12 +482,8 @@ function Manager:RefreshVisibleVisuals()
|
||||
if not shouldShow then
|
||||
needsFullRefresh = true
|
||||
else
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
end
|
||||
local tickThis = false
|
||||
entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
|
||||
if #entries == 0 then
|
||||
needsFullRefresh = true
|
||||
else
|
||||
@@ -701,11 +493,8 @@ function Manager:RefreshVisibleVisuals()
|
||||
if self._displaySignatures[frameKey] ~= newSignature then
|
||||
needsFullRefresh = true
|
||||
end
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
if tickThis then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -751,12 +540,8 @@ function Manager:UpdateDisplay()
|
||||
local entries, shouldShow = self:BuildEntriesForTracker(tracker)
|
||||
|
||||
if shouldShow then
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
end
|
||||
local tickThis = false
|
||||
entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
|
||||
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
frame:Show()
|
||||
@@ -769,11 +554,8 @@ function Manager:UpdateDisplay()
|
||||
layoutDirty = true
|
||||
end
|
||||
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
if tickThis then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
|
||||
65
Modules/Tracker/TrackerPlayerState.lua
Normal file
65
Modules/Tracker/TrackerPlayerState.lua
Normal file
@@ -0,0 +1,65 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.TrackerPlayerState = HMGT.TrackerPlayerState or {}
|
||||
|
||||
local internals = HMGT.TrackerInternals or {}
|
||||
local IsSpellKnownLocally = internals.IsSpellKnownLocally
|
||||
|
||||
function HMGT:CollectOwnAvailableTrackerSpells(classToken, specIndex)
|
||||
local class = classToken or select(2, UnitClass("player"))
|
||||
local spec = tonumber(specIndex) or tonumber(GetSpecialization())
|
||||
if not class or not spec or spec <= 0 then
|
||||
return {}
|
||||
end
|
||||
if not HMGT_SpellData or type(HMGT_SpellData.GetSpellsForSpec) ~= "function" then
|
||||
return {}
|
||||
end
|
||||
|
||||
local knownSpells = {}
|
||||
for _, datasetName in ipairs({ "Interrupts", "RaidCooldowns", "GroupCooldowns" }) do
|
||||
local dataset = HMGT_SpellData[datasetName]
|
||||
if type(dataset) == "table" then
|
||||
local spells = HMGT_SpellData.GetSpellsForSpec(class, spec, dataset)
|
||||
for _, entry in ipairs(spells) do
|
||||
local sid = tonumber(entry.spellId)
|
||||
if sid and sid > 0 and IsSpellKnownLocally and IsSpellKnownLocally(sid) then
|
||||
knownSpells[sid] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||
local ownCDs = ownName and self:GetPlayerCooldownMap(ownName, false)
|
||||
if ownCDs then
|
||||
for sid in pairs(ownCDs) do
|
||||
sid = tonumber(sid)
|
||||
if sid and sid > 0 then
|
||||
knownSpells[sid] = true
|
||||
end
|
||||
end
|
||||
end
|
||||
return knownSpells
|
||||
end
|
||||
|
||||
function HMGT:IsTrackedSpellKnownForPlayer(playerName, spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||
local pData = normalizedName and self.playerData[normalizedName]
|
||||
if pData and type(pData.knownSpells) == "table" and pData.knownSpells[sid] == true then
|
||||
return true
|
||||
end
|
||||
|
||||
if normalizedName and ownName and normalizedName == ownName and IsSpellKnownLocally then
|
||||
return IsSpellKnownLocally(sid)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
410
Modules/Tracker/TrackerState.lua
Normal file
410
Modules/Tracker/TrackerState.lua
Normal file
@@ -0,0 +1,410 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.TrackerState = HMGT.TrackerState or {}
|
||||
|
||||
function HMGT:EnsureTrackerStateTables()
|
||||
self.playerData = self.playerData or {}
|
||||
self.activeCDs = self.activeCDs or {}
|
||||
self.availabilityStates = self.availabilityStates or {}
|
||||
self.localSpellStateRevisions = self.localSpellStateRevisions or {}
|
||||
self.remoteSpellStateRevisions = self.remoteSpellStateRevisions or {}
|
||||
self.knownChargeInfo = self.knownChargeInfo or {}
|
||||
end
|
||||
|
||||
function HMGT:ResetTrackerState()
|
||||
self.playerData = {}
|
||||
self.activeCDs = {}
|
||||
self.availabilityStates = {}
|
||||
self.localSpellStateRevisions = {}
|
||||
self.remoteSpellStateRevisions = {}
|
||||
self.knownChargeInfo = {}
|
||||
end
|
||||
|
||||
function HMGT:GetPlayerCooldownMap(playerName, create)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
if not normalizedName then
|
||||
return nil
|
||||
end
|
||||
self:EnsureTrackerStateTables()
|
||||
if create then
|
||||
self.activeCDs[normalizedName] = self.activeCDs[normalizedName] or {}
|
||||
end
|
||||
return self.activeCDs[normalizedName]
|
||||
end
|
||||
|
||||
function HMGT:GetAvailabilityStateMap(playerName, create)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
if not normalizedName then
|
||||
return nil
|
||||
end
|
||||
self:EnsureTrackerStateTables()
|
||||
if create then
|
||||
self.availabilityStates[normalizedName] = self.availabilityStates[normalizedName] or {}
|
||||
end
|
||||
return self.availabilityStates[normalizedName]
|
||||
end
|
||||
|
||||
function HMGT:GetAvailabilityStateEntry(playerName, spellId)
|
||||
local sid = tonumber(spellId)
|
||||
local states = self:GetAvailabilityStateMap(playerName, false)
|
||||
return states and sid and states[sid] or nil
|
||||
end
|
||||
|
||||
function HMGT:SetAvailabilityStateEntry(playerName, spellId, stateData)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 or type(stateData) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
local states = self:GetAvailabilityStateMap(playerName, true)
|
||||
if not states then
|
||||
return nil
|
||||
end
|
||||
states[sid] = stateData
|
||||
return stateData
|
||||
end
|
||||
|
||||
function HMGT:ClearAvailabilityState(playerName, spellId)
|
||||
local sid = tonumber(spellId)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
if not normalizedName or not sid or sid <= 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
local states = self.availabilityStates and self.availabilityStates[normalizedName]
|
||||
if not states or not states[sid] then
|
||||
return false
|
||||
end
|
||||
|
||||
states[sid] = nil
|
||||
if not next(states) then
|
||||
self.availabilityStates[normalizedName] = nil
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function HMGT:GetActiveCooldown(playerName, spellId)
|
||||
local sid = tonumber(spellId)
|
||||
local cooldowns = self:GetPlayerCooldownMap(playerName, false)
|
||||
return cooldowns and sid and cooldowns[sid] or nil
|
||||
end
|
||||
|
||||
function HMGT:SetActiveCooldown(playerName, spellId, cdData)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 or type(cdData) ~= "table" then
|
||||
return nil
|
||||
end
|
||||
local cooldowns = self:GetPlayerCooldownMap(playerName, true)
|
||||
if not cooldowns then
|
||||
return nil
|
||||
end
|
||||
cooldowns[sid] = cdData
|
||||
return cdData
|
||||
end
|
||||
|
||||
function HMGT:ClearActiveCooldown(playerName, spellId)
|
||||
local sid = tonumber(spellId)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
if not normalizedName or not sid or sid <= 0 then
|
||||
return false
|
||||
end
|
||||
|
||||
local cooldowns = self.activeCDs and self.activeCDs[normalizedName]
|
||||
if not cooldowns or not cooldowns[sid] then
|
||||
return false
|
||||
end
|
||||
|
||||
cooldowns[sid] = nil
|
||||
if not next(cooldowns) then
|
||||
self.activeCDs[normalizedName] = nil
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function HMGT:ClearPlayerCooldowns(playerName)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
if not normalizedName then
|
||||
return false
|
||||
end
|
||||
if self.activeCDs and self.activeCDs[normalizedName] then
|
||||
self.activeCDs[normalizedName] = nil
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function HMGT:GetLocalSpellStateRevision(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then
|
||||
return 0
|
||||
end
|
||||
self:EnsureTrackerStateTables()
|
||||
return tonumber(self.localSpellStateRevisions[sid]) or 0
|
||||
end
|
||||
|
||||
function HMGT:EnsureLocalSpellStateRevision(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then
|
||||
return 0
|
||||
end
|
||||
self:EnsureTrackerStateTables()
|
||||
local current = tonumber(self.localSpellStateRevisions[sid]) or 0
|
||||
if current <= 0 then
|
||||
current = 1
|
||||
self.localSpellStateRevisions[sid] = current
|
||||
end
|
||||
return current
|
||||
end
|
||||
|
||||
function HMGT:NextLocalSpellStateRevision(spellId)
|
||||
local sid = tonumber(spellId)
|
||||
if not sid or sid <= 0 then
|
||||
return 0
|
||||
end
|
||||
self:EnsureTrackerStateTables()
|
||||
local nextRevision = (tonumber(self.localSpellStateRevisions[sid]) or 0) + 1
|
||||
self.localSpellStateRevisions[sid] = nextRevision
|
||||
return nextRevision
|
||||
end
|
||||
|
||||
function HMGT:GetRemoteSpellStateRevision(playerName, spellId)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
local sid = tonumber(spellId)
|
||||
local bySpell = normalizedName and self.remoteSpellStateRevisions[normalizedName]
|
||||
return tonumber(bySpell and bySpell[sid]) or 0
|
||||
end
|
||||
|
||||
function HMGT:SetRemoteSpellStateRevision(playerName, spellId, revision)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
local sid = tonumber(spellId)
|
||||
local rev = tonumber(revision) or 0
|
||||
if not normalizedName or not sid or sid <= 0 or rev <= 0 then
|
||||
return
|
||||
end
|
||||
self:EnsureTrackerStateTables()
|
||||
self.remoteSpellStateRevisions[normalizedName] = self.remoteSpellStateRevisions[normalizedName] or {}
|
||||
self.remoteSpellStateRevisions[normalizedName][sid] = rev
|
||||
end
|
||||
|
||||
function HMGT:ClearRemoteSpellStateRevisions(playerName)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
if not normalizedName then
|
||||
return false
|
||||
end
|
||||
if self.remoteSpellStateRevisions and self.remoteSpellStateRevisions[normalizedName] then
|
||||
self.remoteSpellStateRevisions[normalizedName] = nil
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function HMGT:ClearTrackerStateForPlayer(playerName)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
if not normalizedName then
|
||||
return false
|
||||
end
|
||||
|
||||
local changed = false
|
||||
if self.activeCDs and self.activeCDs[normalizedName] then
|
||||
self.activeCDs[normalizedName] = nil
|
||||
changed = true
|
||||
end
|
||||
if self.availabilityStates and self.availabilityStates[normalizedName] then
|
||||
self.availabilityStates[normalizedName] = nil
|
||||
changed = true
|
||||
end
|
||||
if self.remoteSpellStateRevisions and self.remoteSpellStateRevisions[normalizedName] then
|
||||
self.remoteSpellStateRevisions[normalizedName] = nil
|
||||
changed = true
|
||||
end
|
||||
|
||||
return changed
|
||||
end
|
||||
|
||||
function HMGT:StoreKnownChargeInfo(spellId, maxCharges, chargeDuration)
|
||||
local sid = tonumber(spellId)
|
||||
local maxCount = tonumber(maxCharges)
|
||||
if not sid or sid <= 0 or not maxCount or maxCount <= 1 then
|
||||
return
|
||||
end
|
||||
|
||||
self:EnsureTrackerStateTables()
|
||||
self.knownChargeInfo[sid] = {
|
||||
maxCharges = math.max(1, math.floor(maxCount + 0.5)),
|
||||
chargeDuration = math.max(0, tonumber(chargeDuration) or 0),
|
||||
updatedAt = GetTime(),
|
||||
}
|
||||
end
|
||||
|
||||
function HMGT:GetKnownChargeInfo(spellEntry, talents, spellId, fallbackChargeDuration)
|
||||
local sid = tonumber(spellId or (spellEntry and spellEntry.spellId))
|
||||
if not sid or sid <= 0 then
|
||||
return 0, 0
|
||||
end
|
||||
|
||||
local cached = self.knownChargeInfo and self.knownChargeInfo[sid]
|
||||
local cachedMax = tonumber(cached and cached.maxCharges) or 0
|
||||
local cachedDuration = tonumber(cached and cached.chargeDuration) or 0
|
||||
|
||||
local inferredMax, inferredDuration = HMGT_SpellData.GetEffectiveChargeInfo(
|
||||
spellEntry,
|
||||
talents or {},
|
||||
(cachedMax > 0) and cachedMax or nil,
|
||||
(cachedDuration > 0) and cachedDuration or fallbackChargeDuration
|
||||
)
|
||||
|
||||
local maxCharges = math.max(cachedMax, tonumber(inferredMax) or 0)
|
||||
local chargeDuration = math.max(
|
||||
tonumber(inferredDuration) or 0,
|
||||
cachedDuration,
|
||||
tonumber(fallbackChargeDuration) or 0
|
||||
)
|
||||
|
||||
if maxCharges > 1 then
|
||||
self:StoreKnownChargeInfo(sid, maxCharges, chargeDuration)
|
||||
end
|
||||
|
||||
return maxCharges, chargeDuration
|
||||
end
|
||||
|
||||
function HMGT:PruneAvailabilityStates(playerName, knownSpells)
|
||||
local normalizedName = self:NormalizePlayerName(playerName)
|
||||
local states = normalizedName and self.availabilityStates[normalizedName]
|
||||
if not states or type(knownSpells) ~= "table" then
|
||||
return false
|
||||
end
|
||||
|
||||
local changed = false
|
||||
for sid in pairs(states) do
|
||||
if not knownSpells[tonumber(sid)] then
|
||||
states[sid] = nil
|
||||
changed = true
|
||||
end
|
||||
end
|
||||
|
||||
if not next(states) then
|
||||
self.availabilityStates[normalizedName] = nil
|
||||
end
|
||||
return changed
|
||||
end
|
||||
|
||||
function HMGT:ResolveChargeState(cdData, now)
|
||||
if type(cdData) ~= "table" then
|
||||
return 0, 0, 0, 0
|
||||
end
|
||||
|
||||
now = tonumber(now) or GetTime()
|
||||
local maxCharges = math.max(0, tonumber(cdData.maxCharges) or 0)
|
||||
local currentCharges = math.max(0, tonumber(cdData.currentCharges) or 0)
|
||||
local chargeDuration = math.max(0, tonumber(cdData.chargeDuration) or 0)
|
||||
local chargeStart = tonumber(cdData.chargeStart)
|
||||
|
||||
if maxCharges <= 0 then
|
||||
return 0, chargeDuration, currentCharges, maxCharges
|
||||
end
|
||||
if currentCharges >= maxCharges or chargeDuration <= 0 or not chargeStart then
|
||||
return 0, chargeDuration, math.min(currentCharges, maxCharges), maxCharges
|
||||
end
|
||||
|
||||
local elapsed = math.max(0, now - chargeStart)
|
||||
local gainedCharges = math.floor(elapsed / chargeDuration)
|
||||
local remaining = chargeDuration - (elapsed % chargeDuration)
|
||||
|
||||
if gainedCharges > 0 then
|
||||
currentCharges = math.min(maxCharges, currentCharges + gainedCharges)
|
||||
if currentCharges >= maxCharges then
|
||||
currentCharges = maxCharges
|
||||
chargeStart = nil
|
||||
remaining = 0
|
||||
else
|
||||
chargeStart = now - (elapsed % chargeDuration)
|
||||
end
|
||||
|
||||
cdData.currentCharges = currentCharges
|
||||
cdData.chargeStart = chargeStart
|
||||
if currentCharges >= maxCharges then
|
||||
cdData.startTime = now
|
||||
cdData.duration = 0
|
||||
else
|
||||
local missing = maxCharges - currentCharges
|
||||
cdData.startTime = chargeStart
|
||||
cdData.duration = missing * chargeDuration
|
||||
end
|
||||
end
|
||||
|
||||
if currentCharges >= maxCharges then
|
||||
return 0, chargeDuration, currentCharges, maxCharges
|
||||
end
|
||||
return math.max(0, remaining), chargeDuration, currentCharges, maxCharges
|
||||
end
|
||||
|
||||
function HMGT:RefreshCooldownExpiryTimer(playerName, spellId, cdData)
|
||||
if not cdData then return 0 end
|
||||
local now = GetTime()
|
||||
local duration = tonumber(cdData.duration) or 0
|
||||
local startTime = tonumber(cdData.startTime) or now
|
||||
local expiresIn = math.max(0, duration - (now - startTime))
|
||||
|
||||
self._cdNonce = (self._cdNonce or 0) + 1
|
||||
local nonce = self._cdNonce
|
||||
cdData._nonce = nonce
|
||||
|
||||
if expiresIn > 0 then
|
||||
self:ScheduleTimer(function()
|
||||
local current = self:GetActiveCooldown(playerName, spellId)
|
||||
if current and current._nonce == nonce then
|
||||
self:ClearActiveCooldown(playerName, spellId)
|
||||
if playerName == self:NormalizePlayerName(UnitName("player")) then
|
||||
self:PublishOwnSpellState(spellId)
|
||||
end
|
||||
self:TriggerTrackerUpdate()
|
||||
end
|
||||
end, expiresIn)
|
||||
end
|
||||
return expiresIn
|
||||
end
|
||||
|
||||
function HMGT:CleanupStaleCooldowns()
|
||||
local now = GetTime()
|
||||
local ownName = self:NormalizePlayerName(UnitName("player"))
|
||||
local removed = 0
|
||||
for playerName, spells in pairs(self.activeCDs) do
|
||||
for spellId, cdInfo in pairs(spells) do
|
||||
local duration = tonumber(cdInfo.duration) or 0
|
||||
local startTime = tonumber(cdInfo.startTime) or now
|
||||
local rem = duration - (now - startTime)
|
||||
local hasCharges = (tonumber(cdInfo.maxCharges) or 0) > 0
|
||||
local currentCharges = tonumber(cdInfo.currentCharges) or 0
|
||||
local maxCharges = tonumber(cdInfo.maxCharges) or 0
|
||||
if hasCharges then
|
||||
local _, _, cur, max = self:ResolveChargeState(cdInfo, now)
|
||||
currentCharges = cur
|
||||
maxCharges = max
|
||||
end
|
||||
local shouldDrop = false
|
||||
if hasCharges then
|
||||
if currentCharges >= maxCharges then
|
||||
shouldDrop = true
|
||||
elseif (tonumber(cdInfo.chargeDuration) or 0) <= 0 and rem <= -2 then
|
||||
shouldDrop = true
|
||||
end
|
||||
elseif rem <= -2 then
|
||||
shouldDrop = true
|
||||
end
|
||||
if shouldDrop then
|
||||
spells[spellId] = nil
|
||||
if playerName == ownName then
|
||||
self:PublishOwnSpellState(spellId)
|
||||
end
|
||||
removed = removed + 1
|
||||
end
|
||||
end
|
||||
if not next(spells) then
|
||||
self.activeCDs[playerName] = nil
|
||||
end
|
||||
end
|
||||
if removed > 0 then
|
||||
self:Debug("verbose", "CleanupStaleCooldowns removed=%d", removed)
|
||||
end
|
||||
end
|
||||
1041
Modules/Tracker/TrackerSync.lua
Normal file
1041
Modules/Tracker/TrackerSync.lua
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user