dev-v.2.1.0 #10
@@ -1,486 +0,0 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = _G[ADDON_NAME]
|
||||
if not HMGT then return end
|
||||
|
||||
local L = HMGT.L or LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||
local AceGUI = LibStub("AceGUI-3.0", true)
|
||||
|
||||
local function GetOrderedDebugLevels()
|
||||
return { "error", "info", "verbose" }
|
||||
end
|
||||
|
||||
local function GetOrderedDebugScopes()
|
||||
local values = HMGT:GetDebugScopeOptions() or {}
|
||||
local names = { "ALL" }
|
||||
for scope in pairs(values) do
|
||||
if scope ~= "ALL" then
|
||||
names[#names + 1] = scope
|
||||
end
|
||||
end
|
||||
table.sort(names, function(a, b)
|
||||
if a == "ALL" then return true end
|
||||
if b == "ALL" then return false end
|
||||
return tostring(values[a] or a) < tostring(values[b] or b)
|
||||
end)
|
||||
return names, values
|
||||
end
|
||||
|
||||
local function SetFilterButtonText(buttonWidget, prefix, valueLabel)
|
||||
if not buttonWidget then
|
||||
return
|
||||
end
|
||||
buttonWidget:SetText(string.format("%s: %s", tostring(prefix or ""), tostring(valueLabel or "")))
|
||||
end
|
||||
|
||||
local function AdvanceDebugLevel(step)
|
||||
local levels = GetOrderedDebugLevels()
|
||||
local current = HMGT:GetConfiguredDebugLevel()
|
||||
local nextIndex = 1
|
||||
for index, value in ipairs(levels) do
|
||||
if value == current then
|
||||
nextIndex = index + (step or 1)
|
||||
break
|
||||
end
|
||||
end
|
||||
if nextIndex < 1 then
|
||||
nextIndex = #levels
|
||||
elseif nextIndex > #levels then
|
||||
nextIndex = 1
|
||||
end
|
||||
HMGT.db.profile.debugLevel = levels[nextIndex]
|
||||
HMGT:RefreshDebugWindow()
|
||||
end
|
||||
|
||||
local function AdvanceDebugScope(step)
|
||||
local scopes, labels = GetOrderedDebugScopes()
|
||||
local current = (HMGT.db and HMGT.db.profile and HMGT.db.profile.debugScope) or "ALL"
|
||||
local nextIndex = 1
|
||||
for index, value in ipairs(scopes) do
|
||||
if value == current then
|
||||
nextIndex = index + (step or 1)
|
||||
break
|
||||
end
|
||||
end
|
||||
if nextIndex < 1 then
|
||||
nextIndex = #scopes
|
||||
elseif nextIndex > #scopes then
|
||||
nextIndex = 1
|
||||
end
|
||||
HMGT.db.profile.debugScope = scopes[nextIndex]
|
||||
HMGT:RefreshDebugWindow()
|
||||
end
|
||||
|
||||
function HMGT:SetDebugWindowMinimized(minimized)
|
||||
local frame = self.debugWindow
|
||||
if not frame then
|
||||
return
|
||||
end
|
||||
|
||||
minimized = minimized and true or false
|
||||
self.debugWindowStatus = self.debugWindowStatus or {
|
||||
width = 860,
|
||||
height = 340,
|
||||
}
|
||||
self.debugWindowStatus.minimized = minimized
|
||||
|
||||
local collapsedHeight = 64
|
||||
if minimized then
|
||||
self.debugWindowStatus.restoreHeight = self.debugWindowStatus.height or frame:GetHeight() or 340
|
||||
end
|
||||
|
||||
local targetHeight = minimized
|
||||
and collapsedHeight
|
||||
or (self.debugWindowStatus.restoreHeight or self.debugWindowStatus.height or 340)
|
||||
|
||||
if frame.aceWidget then
|
||||
frame.aceWidget:EnableResize(not minimized)
|
||||
frame.aceWidget:SetHeight(targetHeight)
|
||||
else
|
||||
frame:SetHeight(targetHeight)
|
||||
end
|
||||
|
||||
if frame.minimizeButton then
|
||||
frame.minimizeButton:SetText(minimized and "+" or "-")
|
||||
end
|
||||
if frame.clearButton then
|
||||
local buttonFrame = frame.clearButton.frame or frame.clearButton
|
||||
buttonFrame:SetShown(not minimized)
|
||||
end
|
||||
if frame.selectButton then
|
||||
local buttonFrame = frame.selectButton.frame or frame.selectButton
|
||||
buttonFrame:SetShown(not minimized)
|
||||
end
|
||||
if frame.levelFilter then
|
||||
local filterFrame = frame.levelFilter.frame or frame.levelFilter
|
||||
filterFrame:SetShown(not minimized)
|
||||
end
|
||||
if frame.scopeFilter then
|
||||
local filterFrame = frame.scopeFilter.frame or frame.scopeFilter
|
||||
filterFrame:SetShown(not minimized)
|
||||
end
|
||||
if frame.logWidget then
|
||||
frame.logWidget.frame:SetShown(not minimized)
|
||||
end
|
||||
if frame.scrollBG then
|
||||
frame.scrollBG:SetShown(not minimized)
|
||||
end
|
||||
|
||||
if not minimized then
|
||||
self:RefreshDebugWindow()
|
||||
end
|
||||
end
|
||||
|
||||
function HMGT:ToggleDebugWindowMinimized()
|
||||
self:SetDebugWindowMinimized(not (self.debugWindowStatus and self.debugWindowStatus.minimized))
|
||||
end
|
||||
|
||||
function HMGT:EnsureDebugWindow()
|
||||
if self.debugWindow then
|
||||
return self.debugWindow
|
||||
end
|
||||
|
||||
local frameWidget
|
||||
if AceGUI then
|
||||
frameWidget = AceGUI:Create("Frame")
|
||||
self.debugWindowStatus = self.debugWindowStatus or {
|
||||
width = 860,
|
||||
height = 340,
|
||||
}
|
||||
frameWidget:SetTitle(L["DEBUG_WINDOW_TITLE"] or "HMGT Debug Console")
|
||||
frameWidget:SetStatusText(L["DEBUG_WINDOW_HINT"] or "Mouse wheel scrolls, Ctrl+A selects all, Ctrl+C copies selected text")
|
||||
frameWidget:SetStatusTable(self.debugWindowStatus)
|
||||
frameWidget:SetWidth(self.debugWindowStatus.width or 860)
|
||||
frameWidget:SetHeight(self.debugWindowStatus.height or 340)
|
||||
frameWidget:EnableResize(true)
|
||||
frameWidget.frame:SetClampedToScreen(true)
|
||||
frameWidget.frame:SetToplevel(true)
|
||||
frameWidget.frame:SetFrameStrata("FULLSCREEN_DIALOG")
|
||||
frameWidget:Hide()
|
||||
end
|
||||
|
||||
local frame = frameWidget and frameWidget.frame or CreateFrame("Frame", "HMGT_DebugWindow", UIParent, "BackdropTemplate")
|
||||
if not frameWidget then
|
||||
frame:SetSize(860, 340)
|
||||
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||||
frame:SetFrameStrata("DIALOG")
|
||||
frame:SetClampedToScreen(true)
|
||||
frame:SetMovable(true)
|
||||
frame:EnableMouse(true)
|
||||
frame:RegisterForDrag("LeftButton")
|
||||
frame:SetScript("OnDragStart", function(selfFrame) selfFrame:StartMoving() end)
|
||||
frame:SetScript("OnDragStop", function(selfFrame) selfFrame:StopMovingOrSizing() end)
|
||||
frame:SetBackdrop({
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
edgeSize = 12,
|
||||
insets = { left = 3, right = 3, top = 3, bottom = 3 },
|
||||
})
|
||||
frame:SetBackdropColor(0.05, 0.05, 0.06, 0.95)
|
||||
frame:SetBackdropBorderColor(0.35, 0.55, 0.85, 1)
|
||||
frame:Hide()
|
||||
|
||||
local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
|
||||
title:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -12)
|
||||
title:SetText(L["DEBUG_WINDOW_TITLE"] or "HMGT Debug Console")
|
||||
frame.title = title
|
||||
|
||||
local closeButton = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
|
||||
closeButton:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -5, -5)
|
||||
frame.closeButton = closeButton
|
||||
|
||||
local minimizeButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
|
||||
minimizeButton:SetSize(22, 20)
|
||||
minimizeButton:SetPoint("TOPRIGHT", closeButton, "TOPLEFT", -2, 0)
|
||||
minimizeButton:SetText((self.debugWindowStatus and self.debugWindowStatus.minimized) and "+" or "-")
|
||||
minimizeButton:SetScript("OnClick", function()
|
||||
HMGT:ToggleDebugWindowMinimized()
|
||||
end)
|
||||
frame.minimizeButton = minimizeButton
|
||||
end
|
||||
|
||||
frame.aceWidget = frameWidget
|
||||
|
||||
if frameWidget and AceGUI then
|
||||
local content = frameWidget.content
|
||||
|
||||
local minimizeButton = AceGUI:Create("Button")
|
||||
minimizeButton:SetText((self.debugWindowStatus and self.debugWindowStatus.minimized) and "+" or "-")
|
||||
minimizeButton:SetWidth(24)
|
||||
minimizeButton:SetCallback("OnClick", function()
|
||||
HMGT:ToggleDebugWindowMinimized()
|
||||
end)
|
||||
minimizeButton.frame:SetParent(frame)
|
||||
minimizeButton.frame:ClearAllPoints()
|
||||
minimizeButton.frame:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -34, -4)
|
||||
minimizeButton.frame:SetHeight(20)
|
||||
minimizeButton.frame:Show()
|
||||
frame.minimizeButton = minimizeButton
|
||||
|
||||
local clearButton = AceGUI:Create("Button")
|
||||
clearButton:SetText(L["OPT_DEBUG_CLEAR"] or "Clear log")
|
||||
clearButton:SetWidth(120)
|
||||
clearButton:SetCallback("OnClick", function()
|
||||
HMGT:ClearDebugLog()
|
||||
end)
|
||||
clearButton.frame:SetParent(content)
|
||||
clearButton.frame:ClearAllPoints()
|
||||
clearButton.frame:SetPoint("TOPRIGHT", content, "TOPRIGHT", 0, -2)
|
||||
clearButton.frame:Show()
|
||||
frame.clearButton = clearButton
|
||||
|
||||
local selectButton = AceGUI:Create("Button")
|
||||
selectButton:SetText(L["OPT_DEBUG_SELECT_ALL"] or "Select all")
|
||||
selectButton:SetWidth(120)
|
||||
selectButton:SetCallback("OnClick", function()
|
||||
if frame.editBox then
|
||||
frame.editBox:SetFocus()
|
||||
frame.editBox:HighlightText(0)
|
||||
end
|
||||
end)
|
||||
selectButton.frame:SetParent(content)
|
||||
selectButton.frame:ClearAllPoints()
|
||||
selectButton.frame:SetPoint("TOPRIGHT", clearButton.frame, "TOPLEFT", -6, 0)
|
||||
selectButton.frame:Show()
|
||||
frame.selectButton = selectButton
|
||||
|
||||
local levelFilter = AceGUI:Create("Button")
|
||||
levelFilter:SetWidth(150)
|
||||
levelFilter:SetCallback("OnClick", function()
|
||||
AdvanceDebugLevel(1)
|
||||
end)
|
||||
levelFilter.frame:SetParent(content)
|
||||
levelFilter.frame:ClearAllPoints()
|
||||
levelFilter.frame:SetPoint("TOPLEFT", content, "TOPLEFT", 0, 0)
|
||||
levelFilter.frame:Show()
|
||||
frame.levelFilter = levelFilter
|
||||
|
||||
local scopeFilter = AceGUI:Create("Button")
|
||||
scopeFilter:SetWidth(180)
|
||||
scopeFilter:SetCallback("OnClick", function()
|
||||
AdvanceDebugScope(1)
|
||||
end)
|
||||
scopeFilter.frame:SetParent(content)
|
||||
scopeFilter.frame:ClearAllPoints()
|
||||
scopeFilter.frame:SetPoint("TOPLEFT", levelFilter.frame, "TOPRIGHT", 8, 0)
|
||||
scopeFilter.frame:Show()
|
||||
frame.scopeFilter = scopeFilter
|
||||
|
||||
local logWidget = AceGUI:Create("MultiLineEditBox")
|
||||
logWidget:SetLabel("")
|
||||
logWidget:DisableButton(true)
|
||||
logWidget:SetNumLines(18)
|
||||
logWidget:SetText("")
|
||||
logWidget.frame:SetParent(content)
|
||||
logWidget.frame:ClearAllPoints()
|
||||
logWidget.frame:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -54)
|
||||
logWidget.frame:SetPoint("BOTTOMRIGHT", content, "BOTTOMRIGHT", 0, 0)
|
||||
logWidget.frame:Show()
|
||||
logWidget:SetCallback("OnTextChanged", function()
|
||||
HMGT:RefreshDebugWindow()
|
||||
end)
|
||||
logWidget.editBox:SetScript("OnKeyDown", function(selfBox, key)
|
||||
if IsControlKeyDown() and (key == "A" or key == "a") then
|
||||
selfBox:HighlightText(0)
|
||||
end
|
||||
end)
|
||||
frame.logWidget = logWidget
|
||||
frame.editBox = logWidget.editBox
|
||||
frame.scrollFrame = logWidget.scrollFrame
|
||||
|
||||
self.debugWindow = frame
|
||||
self:SetDebugWindowMinimized(self.debugWindowStatus and self.debugWindowStatus.minimized)
|
||||
return frame
|
||||
end
|
||||
|
||||
local clearButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
|
||||
clearButton:SetSize(90, 22)
|
||||
clearButton:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -30, -6)
|
||||
clearButton:SetText(L["OPT_DEBUG_CLEAR"] or "Clear log")
|
||||
clearButton:SetScript("OnClick", function()
|
||||
HMGT:ClearDebugLog()
|
||||
end)
|
||||
frame.clearButton = clearButton
|
||||
|
||||
local selectButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
|
||||
selectButton:SetSize(90, 22)
|
||||
selectButton:SetPoint("TOPRIGHT", clearButton, "TOPLEFT", -6, 0)
|
||||
selectButton:SetText(L["OPT_DEBUG_SELECT_ALL"] or "Select all")
|
||||
selectButton:SetScript("OnClick", function()
|
||||
if frame.editBox then
|
||||
frame.editBox:SetFocus()
|
||||
frame.editBox:HighlightText(0)
|
||||
end
|
||||
end)
|
||||
frame.selectButton = selectButton
|
||||
|
||||
local scopeFilter = CreateFrame("Frame", nil, frame)
|
||||
scopeFilter:SetSize(170, 22)
|
||||
scopeFilter:SetPoint("TOPLEFT", frame, "TOPLEFT", 16, -8)
|
||||
frame.scopeFilter = scopeFilter
|
||||
|
||||
local scrollBG = CreateFrame("Frame", nil, frame, "BackdropTemplate")
|
||||
scrollBG:SetBackdrop({
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
edgeSize = 16,
|
||||
insets = { left = 4, right = 3, top = 4, bottom = 3 },
|
||||
})
|
||||
scrollBG:SetBackdropColor(0, 0, 0, 0.95)
|
||||
scrollBG:SetBackdropBorderColor(0.4, 0.4, 0.4, 1)
|
||||
scrollBG:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -36)
|
||||
scrollBG:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -30, 14)
|
||||
frame.scrollBG = scrollBG
|
||||
|
||||
local scrollFrame = CreateFrame("ScrollFrame", nil, scrollBG, "UIPanelScrollFrameTemplate")
|
||||
scrollFrame:SetPoint("TOPLEFT", scrollBG, "TOPLEFT", 6, -6)
|
||||
scrollFrame:SetPoint("BOTTOMRIGHT", scrollBG, "BOTTOMRIGHT", -27, 4)
|
||||
scrollFrame:EnableMouseWheel(true)
|
||||
scrollFrame:SetScript("OnMouseWheel", function(selfMsg, delta)
|
||||
if delta > 0 then
|
||||
selfMsg:SetVerticalScroll(math.max(0, selfMsg:GetVerticalScroll() - 42))
|
||||
else
|
||||
selfMsg:SetVerticalScroll(selfMsg:GetVerticalScroll() + 42)
|
||||
end
|
||||
end)
|
||||
frame.scrollFrame = scrollFrame
|
||||
|
||||
local editBox = CreateFrame("EditBox", nil, scrollFrame)
|
||||
editBox:SetMultiLine(true)
|
||||
editBox:SetAutoFocus(false)
|
||||
editBox:SetFontObject(ChatFontNormal)
|
||||
editBox:SetWidth(780)
|
||||
editBox:SetTextInsets(6, 6, 6, 6)
|
||||
editBox:EnableMouse(true)
|
||||
editBox:SetScript("OnEscapePressed", function(selfBox)
|
||||
selfBox:ClearFocus()
|
||||
end)
|
||||
editBox:SetScript("OnKeyDown", function(selfBox, key)
|
||||
if IsControlKeyDown() and (key == "A" or key == "a") then
|
||||
selfBox:HighlightText(0)
|
||||
end
|
||||
end)
|
||||
editBox:SetScript("OnTextChanged", function(selfBox, userInput)
|
||||
if userInput then
|
||||
HMGT:RefreshDebugWindow()
|
||||
else
|
||||
selfBox:SetCursorPosition(selfBox:GetNumLetters())
|
||||
selfBox:SetHeight(math.max(scrollFrame:GetHeight(), HMGT:GetDebugWindowTextHeight(frame, selfBox:GetText()) + 16))
|
||||
scrollFrame:UpdateScrollChildRect()
|
||||
end
|
||||
end)
|
||||
editBox:SetScript("OnMouseUp", function(selfBox)
|
||||
selfBox:SetFocus()
|
||||
end)
|
||||
scrollFrame:SetScrollChild(editBox)
|
||||
frame.editBox = editBox
|
||||
|
||||
local measureText = frame:CreateFontString(nil, "ARTWORK", "ChatFontNormal")
|
||||
measureText:SetJustifyH("LEFT")
|
||||
measureText:SetJustifyV("TOP")
|
||||
if measureText.SetSpacing then
|
||||
measureText:SetSpacing(2)
|
||||
end
|
||||
measureText:SetWidth(768)
|
||||
frame.measureText = measureText
|
||||
|
||||
self.debugWindow = frame
|
||||
self:SetDebugWindowMinimized(self.debugWindowStatus and self.debugWindowStatus.minimized)
|
||||
return frame
|
||||
end
|
||||
|
||||
function HMGT:GetDebugWindowTextHeight(frame, text)
|
||||
if not frame or not frame.measureText then
|
||||
return 0
|
||||
end
|
||||
|
||||
local width = 768
|
||||
if frame.editBox then
|
||||
width = math.max(1, (frame.editBox:GetWidth() or width) - 12)
|
||||
end
|
||||
frame.measureText:SetWidth(width)
|
||||
frame.measureText:SetText(text or "")
|
||||
return frame.measureText:GetStringHeight()
|
||||
end
|
||||
|
||||
function HMGT:RefreshDebugWindow()
|
||||
local frame = self:EnsureDebugWindow()
|
||||
if not frame then
|
||||
return
|
||||
end
|
||||
|
||||
local filtered = self:GetFilteredDebugBuffer() or self.debugBuffer or {}
|
||||
local text = table.concat(filtered, "\n")
|
||||
if frame.logWidget and frame.editBox then
|
||||
if frame.levelFilter then
|
||||
local levelOptions = self:GetDebugLevelOptions()
|
||||
SetFilterButtonText(frame.levelFilter, L["OPT_DEBUG_LEVEL"] or "Level", levelOptions[self:GetConfiguredDebugLevel()])
|
||||
end
|
||||
if frame.scopeFilter then
|
||||
local scopeOptions = self:GetDebugScopeOptions()
|
||||
local currentScope = (self.db and self.db.profile and self.db.profile.debugScope) or "ALL"
|
||||
SetFilterButtonText(frame.scopeFilter, L["OPT_DEBUG_SCOPE"] or "Module", scopeOptions[currentScope] or currentScope)
|
||||
end
|
||||
frame.logWidget:SetText(text)
|
||||
frame.editBox:SetCursorPosition(frame.editBox:GetNumLetters())
|
||||
return
|
||||
end
|
||||
|
||||
if not frame.editBox then
|
||||
return
|
||||
end
|
||||
|
||||
frame.editBox:SetText(text)
|
||||
frame.editBox:SetCursorPosition(#text)
|
||||
frame.editBox:SetHeight(math.max(frame.scrollFrame:GetHeight(), self:GetDebugWindowTextHeight(frame, text) + 16))
|
||||
frame.scrollFrame:SetVerticalScroll(math.max(0, frame.editBox:GetHeight() - frame.scrollFrame:GetHeight()))
|
||||
end
|
||||
|
||||
function HMGT:UpdateDebugWindowVisibility()
|
||||
if self.db and self.db.profile then
|
||||
self.db.profile.debug = false
|
||||
end
|
||||
local frame = self.debugWindow
|
||||
if not frame then
|
||||
return
|
||||
end
|
||||
local widget = frame.aceWidget
|
||||
if widget then
|
||||
widget:Hide()
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
function HMGT:ClearDebugLog()
|
||||
wipe(self.debugBuffer)
|
||||
if self.debugWindow and self.debugWindow.logWidget then
|
||||
self.debugWindow.logWidget:SetText("")
|
||||
self.debugWindow.editBox:SetCursorPosition(0)
|
||||
self.debugWindow.scrollFrame:SetVerticalScroll(0)
|
||||
return
|
||||
end
|
||||
if self.debugWindow and self.debugWindow.editBox then
|
||||
self.debugWindow.editBox:SetText("")
|
||||
self.debugWindow.scrollFrame:SetVerticalScroll(0)
|
||||
end
|
||||
end
|
||||
|
||||
function HMGT:ToggleDebugWindowShortcut()
|
||||
if self.db and self.db.profile then
|
||||
self.db.profile.debug = false
|
||||
end
|
||||
local frame = self.debugWindow
|
||||
if not frame then
|
||||
return
|
||||
end
|
||||
local widget = frame.aceWidget
|
||||
if widget then
|
||||
widget:Hide()
|
||||
else
|
||||
frame:Hide()
|
||||
end
|
||||
end
|
||||
|
||||
function HMGT:DumpDebugLog(maxLines)
|
||||
return
|
||||
end
|
||||
@@ -68,12 +68,14 @@ local function GetPlayerVersionText(name)
|
||||
return tostring(HMGT.ADDON_VERSION or "dev"), tonumber(HMGT.PROTOCOL_VERSION) or 0, true
|
||||
end
|
||||
|
||||
local version = HMGT.peerVersions and HMGT.peerVersions[normalized] or nil
|
||||
local protocol = HMGT.GetPeerProtocolVersion and HMGT:GetPeerProtocolVersion(normalized) or 0
|
||||
if version and version ~= "" then
|
||||
return tostring(version), tonumber(protocol) or 0, true
|
||||
local addonStatus = HMGT.GetPlayerAddonStatus and HMGT:GetPlayerAddonStatus(normalized) or nil
|
||||
if addonStatus and addonStatus.mode == "hmgt" and addonStatus.version and addonStatus.version ~= "" then
|
||||
return tostring(addonStatus.version), tonumber(addonStatus.protocol) or 0, true
|
||||
end
|
||||
return nil, tonumber(protocol) or 0, false
|
||||
if addonStatus and addonStatus.mode == "bridge" then
|
||||
return L["VERSION_WINDOW_BRIDGE_MODE"] or "Bridge Mode", 0, true
|
||||
end
|
||||
return nil, tonumber(addonStatus and addonStatus.protocol) or 0, false
|
||||
end
|
||||
|
||||
local function ApplyClassIcon(texture, classTag)
|
||||
@@ -167,7 +169,11 @@ function HMGT:RefreshVersionNoticeWindow()
|
||||
local versionText, protocol, hasAddon = GetPlayerVersionText(info.name)
|
||||
if hasAddon then
|
||||
row.versionText:SetText(versionText or "?")
|
||||
row.versionText:SetTextColor(0.9, 0.9, 0.9, 1)
|
||||
if versionText == (L["VERSION_WINDOW_BRIDGE_MODE"] or "Bridge Mode") then
|
||||
row.versionText:SetTextColor(0.55, 0.82, 1, 1)
|
||||
else
|
||||
row.versionText:SetTextColor(0.9, 0.9, 0.9, 1)
|
||||
end
|
||||
row.protocolText:SetText(protocol > 0 and tostring(protocol) or "-")
|
||||
row.protocolText:SetTextColor(0.75, 0.75, 0.75, 1)
|
||||
else
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,10 +31,20 @@ 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
|
||||
Modules\Tracker\RaidCooldownTracker\RaidCooldownSpellDatabase.lua
|
||||
Modules\Tracker\GroupCooldownTracker\GroupCooldownSpellDatabase.lua
|
||||
Modules\Tracker\TrackerManager.lua
|
||||
Modules\Tracker\NormalTrackerFrames.lua
|
||||
@@ -55,5 +65,4 @@ Modules\MapOverlay\MapOverlay.xml
|
||||
Modules\RaidTimeline\RaidTimelineBossAbilityData.lua
|
||||
Modules\RaidTimeline\RaidTimeline.lua
|
||||
Modules\RaidTimeline\RaidTimelineBigWigs.lua
|
||||
Modules\RaidTimeline\RaidTimelineDBM.lua
|
||||
Modules\RaidTimeline\RaidTimelineOptions.lua
|
||||
@@ -22,11 +22,12 @@ L["VERSION_WINDOW_MESSAGE"] = "Hail Mary Guild Tools Versionen in deiner aktuell
|
||||
L["VERSION_WINDOW_DETAIL"] = "Erkannt ueber %s von %s.\n%s"
|
||||
L["VERSION_WINDOW_NO_MISMATCH"] = "In deiner aktuellen Gruppe wurde keine neuere HMGT-Version erkannt."
|
||||
L["VERSION_WINDOW_CURRENT"] = "Aktuelle Version: %s | Protokoll: %s"
|
||||
L["VERSION_WINDOW_STATUS"] = "HMGT bei %d/%d Spielern erkannt"
|
||||
L["VERSION_WINDOW_STATUS"] = "Addon oder Bridge bei %d/%d Spielern erkannt"
|
||||
L["VERSION_WINDOW_REFRESH"] = "Aktualisieren"
|
||||
L["VERSION_WINDOW_COLUMN_PLAYER"] = "Spieler"
|
||||
L["VERSION_WINDOW_COLUMN_VERSION"] = "Version"
|
||||
L["VERSION_WINDOW_COLUMN_PROTOCOL"] = "Protokoll"
|
||||
L["VERSION_WINDOW_BRIDGE_MODE"] = "Bridge Mode"
|
||||
L["VERSION_WINDOW_MISSING_ADDON"] = "Addon nicht vorhanden"
|
||||
L["VERSION_WINDOW_LEADER_TAG"] = "(Leiter)"
|
||||
L["VERSION_WINDOW_ASSISTANT_TAG"] = "(Assist)"
|
||||
|
||||
@@ -22,11 +22,12 @@ L["VERSION_WINDOW_MESSAGE"] = "Hail Mary Guild Tools versions in your current gr
|
||||
L["VERSION_WINDOW_DETAIL"] = "Detected via %s from %s.\n%s"
|
||||
L["VERSION_WINDOW_NO_MISMATCH"] = "No newer HMGT version has been detected in your current group."
|
||||
L["VERSION_WINDOW_CURRENT"] = "Current version: %s | Protocol: %s"
|
||||
L["VERSION_WINDOW_STATUS"] = "Detected HMGT on %d/%d players"
|
||||
L["VERSION_WINDOW_STATUS"] = "Detected addon or bridge on %d/%d players"
|
||||
L["VERSION_WINDOW_REFRESH"] = "Refresh"
|
||||
L["VERSION_WINDOW_COLUMN_PLAYER"] = "Player"
|
||||
L["VERSION_WINDOW_COLUMN_VERSION"] = "Version"
|
||||
L["VERSION_WINDOW_COLUMN_PROTOCOL"] = "Protocol"
|
||||
L["VERSION_WINDOW_BRIDGE_MODE"] = "Bridge Mode"
|
||||
L["VERSION_WINDOW_MISSING_ADDON"] = "Addon not installed"
|
||||
L["VERSION_WINDOW_LEADER_TAG"] = "(Leader)"
|
||||
L["VERSION_WINDOW_ASSISTANT_TAG"] = "(Assist)"
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
local RT = HMGT.RaidTimeline
|
||||
if not RT then return end
|
||||
|
||||
-- Placeholder for later DBM-specific raid timeline integration.
|
||||
@@ -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 L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
|
||||
|
||||
GCT.frame = nil
|
||||
GCT.frames = {}
|
||||
local module = HMGT:NewModule("GroupCooldownTracker")
|
||||
HMGT.GroupCooldownTracker = module
|
||||
|
||||
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
|
||||
function module:GetDefinition()
|
||||
return self.definition
|
||||
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, "|")
|
||||
function module:GetSettings()
|
||||
local profile = HMGT.db and HMGT.db.profile
|
||||
return profile and profile[self.definition.dbKey] or nil
|
||||
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)
|
||||
function module:Enable()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.Enable then
|
||||
HMGT.TrackerManager:Enable()
|
||||
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
|
||||
function module:Disable()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.UpdateDisplay then
|
||||
HMGT.TrackerManager:UpdateDisplay()
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:SetUpdateTickerEnabled(enabled)
|
||||
if enabled then
|
||||
self:EnsureUpdateTicker()
|
||||
else
|
||||
self:StopUpdateTicker()
|
||||
function module:SetLockedAll(locked)
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.SetAllLocked then
|
||||
HMGT.TrackerManager:SetAllLocked(locked)
|
||||
end
|
||||
end
|
||||
|
||||
function GCT:InvalidateAnchorLayout()
|
||||
self._lastAnchorLayoutSignature = nil
|
||||
self._nextAnchorRetryAt = nil
|
||||
function module:RefreshAnchors(force)
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.RefreshAnchors then
|
||||
HMGT.TrackerManager:RefreshAnchors(force)
|
||||
end
|
||||
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
|
||||
function module:InvalidateAnchorLayout()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.InvalidateAnchorLayout then
|
||||
HMGT.TrackerManager:InvalidateAnchorLayout()
|
||||
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
|
||||
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()
|
||||
function module:GetAnchorableFrames()
|
||||
if HMGT.TrackerManager and HMGT.TrackerManager.GetAnchorableFrames then
|
||||
return HMGT.TrackerManager:GetAnchorableFrames()
|
||||
end
|
||||
return {}
|
||||
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()
|
||||
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 IsInRaid() or not IsInGroup() then
|
||||
self.lastEntryCount = 0
|
||||
self:StopUpdateTicker()
|
||||
self:HideAllFrames()
|
||||
return
|
||||
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()
|
||||
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)
|
||||
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
|
||||
end
|
||||
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
|
||||
191
Modules/Tracker/TrackerBridge.lua
Normal file
191
Modules/Tracker/TrackerBridge.lua
Normal file
@@ -0,0 +1,191 @@
|
||||
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,
|
||||
}
|
||||
self:SetPlayerBridgeStatus(normalizedName, source)
|
||||
self:DebugScoped("verbose", "TrackerBridge", "Bridge known spell source=%s player=%s spellId=%s", tostring(source), tostring(normalizedName), tostring(sid))
|
||||
|
||||
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:SetPlayerBridgeStatus(normalizedName, source)
|
||||
self:DebugScoped("info", "TrackerBridge", "Bridge spec sync source=%s player=%s class=%s spec=%s", tostring(source), tostring(normalizedName), tostring(classToken), tostring(spec))
|
||||
|
||||
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:DebugScoped("info", "TrackerBridge", "Bridge cooldown source=%s player=%s spellId=%s cooldown=%.1f", tostring(source), tostring(normalizedName), tostring(sid), 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
|
||||
576
Modules/Tracker/TrackerCore.lua
Normal file
576
Modules/Tracker/TrackerCore.lua
Normal file
@@ -0,0 +1,576 @@
|
||||
local ADDON_NAME = "HailMaryGuildTools"
|
||||
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
|
||||
if not HMGT then return end
|
||||
|
||||
HMGT.TrackerCore = HMGT.TrackerCore or {}
|
||||
|
||||
HMGT.TRACKER_PRESET_DEFINITIONS = HMGT.TRACKER_PRESET_DEFINITIONS or {
|
||||
interruptTracker = {
|
||||
moduleName = "InterruptTracker",
|
||||
dbKey = "interruptTracker",
|
||||
trackerType = "normal",
|
||||
trackerKey = "interruptTracker",
|
||||
categories = { "interrupt" },
|
||||
defaultName = function(L)
|
||||
return (L and L["IT_NAME"]) or "Interrupts"
|
||||
end,
|
||||
},
|
||||
raidCooldownTracker = {
|
||||
moduleName = "RaidCooldownTracker",
|
||||
dbKey = "raidCooldownTracker",
|
||||
trackerType = "normal",
|
||||
trackerKey = "raidCooldownTracker",
|
||||
categories = { "lust", "defensive", "healing", "tank", "utility", "offensive", "cc", "interrupt" },
|
||||
defaultName = function(L)
|
||||
return (L and L["RCD_NAME"]) or "Raid Cooldowns"
|
||||
end,
|
||||
},
|
||||
groupCooldownTracker = {
|
||||
moduleName = "GroupCooldownTracker",
|
||||
dbKey = "groupCooldownTracker",
|
||||
trackerType = "group",
|
||||
trackerKey = "groupCooldownTracker",
|
||||
categories = { "tank", "defensive", "healing", "cc", "utility", "offensive", "lust", "interrupt" },
|
||||
includeSelfFrame = false,
|
||||
showChargesOnIcon = true,
|
||||
defaultName = function(L)
|
||||
return (L and L["GCD_NAME"]) or "Cooldowns"
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
function HMGT:GetTrackerPresetDefinitions()
|
||||
return self.TRACKER_PRESET_DEFINITIONS or {}
|
||||
end
|
||||
|
||||
function HMGT:GetTrackerPresetDefinition(key)
|
||||
local definitions = self:GetTrackerPresetDefinitions()
|
||||
return definitions and definitions[tostring(key or "")]
|
||||
end
|
||||
|
||||
function HMGT:GetTrackerPresetDefinitionByModule(moduleName)
|
||||
local target = tostring(moduleName or "")
|
||||
for _, definition in pairs(self:GetTrackerPresetDefinitions()) do
|
||||
if tostring(definition.moduleName or "") == target then
|
||||
return definition
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function HMGT:GetTrackerTypeOptions()
|
||||
local L = self.L
|
||||
return {
|
||||
normal = (L and L["OPT_TRACKER_TYPE_NORMAL"]) or "Normal tracker",
|
||||
group = (L and L["OPT_TRACKER_TYPE_GROUP"]) or "Group-based tracker",
|
||||
}
|
||||
end
|
||||
|
||||
function HMGT:BuildTrackerConfigFromPreset(presetKey, trackerId, overrides)
|
||||
local definition = self:GetTrackerPresetDefinition(presetKey)
|
||||
local config = overrides or {}
|
||||
if not definition then
|
||||
return self:CreateTrackerConfig(trackerId, config)
|
||||
end
|
||||
|
||||
local base = {
|
||||
name = type(definition.defaultName) == "function" and definition.defaultName(self.L) or tostring(definition.defaultName or ""),
|
||||
trackerType = definition.trackerType,
|
||||
trackerKey = definition.trackerKey,
|
||||
categories = definition.categories,
|
||||
includeSelfFrame = definition.includeSelfFrame,
|
||||
showChargesOnIcon = definition.showChargesOnIcon,
|
||||
}
|
||||
|
||||
for key, value in pairs(config) do
|
||||
base[key] = value
|
||||
end
|
||||
|
||||
return self:CreateTrackerConfig(trackerId, base)
|
||||
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 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:GetDemoEntries(trackerKey, database, settings)
|
||||
local pool = {}
|
||||
local poolByClass = {}
|
||||
for _, entry in ipairs(database or {}) do
|
||||
if settings.enabledSpells[entry.spellId] ~= false then
|
||||
pool[#pool + 1] = entry
|
||||
for _, classToken in ipairs(entry.classes or {}) do
|
||||
poolByClass[classToken] = poolByClass[classToken] or {}
|
||||
poolByClass[classToken][#poolByClass[classToken] + 1] = entry
|
||||
end
|
||||
end
|
||||
end
|
||||
if #pool == 0 then
|
||||
return {}
|
||||
end
|
||||
|
||||
local classKeys = {}
|
||||
for classToken in pairs(poolByClass) do
|
||||
classKeys[#classKeys + 1] = classToken
|
||||
end
|
||||
if #classKeys == 0 then
|
||||
classKeys[1] = "WARRIOR"
|
||||
end
|
||||
|
||||
local count = settings.showBar and math.min(8, #pool) or math.min(12, #pool)
|
||||
local names = { "Alice", "Bob", "Clara", "Duke", "Elli", "Fynn", "Gina", "Hektor", "Ivo", "Jana", "Kira", "Lio" }
|
||||
|
||||
local spellIds = {}
|
||||
for _, entry in ipairs(pool) do
|
||||
spellIds[#spellIds + 1] = tostring(entry.spellId)
|
||||
end
|
||||
table.sort(spellIds)
|
||||
local signature = table.concat(spellIds, ",") .. "|" .. tostring(settings.showBar and 1 or 0) .. "|" .. tostring(count)
|
||||
|
||||
local now = GetTime()
|
||||
local cache = self.demoModeData[trackerKey]
|
||||
if (not cache) or cache.signature ~= signature or (not cache.entries) or #cache.entries ~= count then
|
||||
local cachedEntries = {}
|
||||
for index = 1, count do
|
||||
local classToken = classKeys[math.random(1, #classKeys)]
|
||||
local classPool = poolByClass[classToken]
|
||||
local spellEntry = (classPool and classPool[math.random(1, #classPool)]) or pool[math.random(1, #pool)]
|
||||
local duration = math.max(
|
||||
1,
|
||||
tonumber(HMGT_SpellData.GetBaseCooldown and HMGT_SpellData.GetBaseCooldown(spellEntry)) or tonumber(spellEntry.cooldown) or 60
|
||||
)
|
||||
local offset = math.random() * math.min(duration * 0.85, duration - 0.1)
|
||||
cachedEntries[#cachedEntries + 1] = {
|
||||
playerName = names[((index - 1) % #names) + 1],
|
||||
class = classToken or ((spellEntry.classes and spellEntry.classes[1]) or "WARRIOR"),
|
||||
spellEntry = spellEntry,
|
||||
total = duration,
|
||||
cycleStart = now - offset,
|
||||
currentCharges = nil,
|
||||
maxCharges = nil,
|
||||
}
|
||||
end
|
||||
cache = {
|
||||
signature = signature,
|
||||
entries = cachedEntries,
|
||||
}
|
||||
self.demoModeData[trackerKey] = cache
|
||||
end
|
||||
|
||||
local entries = {}
|
||||
for _, entry in ipairs(cache.entries) do
|
||||
local total = math.max(1, tonumber(entry.total) or 1)
|
||||
local elapsed = math.max(0, now - (entry.cycleStart or now))
|
||||
local phase = math.fmod(elapsed, total)
|
||||
local remaining = total - phase
|
||||
if elapsed > 0 and phase < 0.05 then
|
||||
remaining = 0
|
||||
end
|
||||
entries[#entries + 1] = {
|
||||
playerName = entry.playerName,
|
||||
class = entry.class,
|
||||
spellEntry = entry.spellEntry,
|
||||
remaining = remaining,
|
||||
total = total,
|
||||
currentCharges = entry.currentCharges,
|
||||
maxCharges = entry.maxCharges,
|
||||
}
|
||||
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:DebugScoped("verbose", "TrackerUI", "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",
|
||||
"TrackerState",
|
||||
"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
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
if tickThis then
|
||||
shouldTick = true
|
||||
end
|
||||
end
|
||||
local newSignature = BuildGroupDisplaySignature(currentOrder, byPlayerFiltered)
|
||||
@@ -680,36 +476,29 @@ function Manager:RefreshVisibleVisuals()
|
||||
end
|
||||
end
|
||||
else
|
||||
local frame = self.frames[frameKey]
|
||||
if frame and frame:IsShown() then
|
||||
local entries, shouldShow = self:BuildEntriesForTracker(tracker)
|
||||
if not shouldShow then
|
||||
needsFullRefresh = true
|
||||
else
|
||||
if HMGT.FilterDisplayEntries then
|
||||
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries
|
||||
end
|
||||
if HMGT.SortDisplayEntries then
|
||||
HMGT:SortDisplayEntries(entries)
|
||||
end
|
||||
if #entries == 0 then
|
||||
local frame = self.frames[frameKey]
|
||||
if frame and frame:IsShown() then
|
||||
local entries, shouldShow = self:BuildEntriesForTracker(tracker)
|
||||
if not shouldShow then
|
||||
needsFullRefresh = true
|
||||
else
|
||||
local tickThis = false
|
||||
entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
|
||||
if #entries == 0 then
|
||||
needsFullRefresh = true
|
||||
else
|
||||
HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
|
||||
totalEntries = totalEntries + #entries
|
||||
local newSignature = BuildNormalDisplaySignature(true, entries)
|
||||
if self._displaySignatures[frameKey] ~= newSignature then
|
||||
needsFullRefresh = true
|
||||
end
|
||||
for _, entry in ipairs(entries) do
|
||||
if EntryNeedsVisualTicker(entry) then
|
||||
if self._displaySignatures[frameKey] ~= newSignature then
|
||||
needsFullRefresh = true
|
||||
end
|
||||
if tickThis then
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
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
|
||||
shouldTick = true
|
||||
break
|
||||
end
|
||||
if tickThis then
|
||||
shouldTick = true
|
||||
end
|
||||
else
|
||||
frame:Hide()
|
||||
|
||||
@@ -142,13 +142,26 @@ local function IsPartyAttachMode(tracker)
|
||||
end
|
||||
|
||||
local function IsGroupTracker(tracker)
|
||||
return type(tracker) == "table" and tracker.trackerType == "group"
|
||||
return HMGT.IsGroupTrackerConfig and HMGT:IsGroupTrackerConfig(tracker) or (type(tracker) == "table" and tracker.trackerType == "group")
|
||||
end
|
||||
|
||||
local TRACKER_TYPE_VALUES = {
|
||||
normal = L["OPT_TRACKER_TYPE_NORMAL"] or "Normal tracker",
|
||||
group = L["OPT_TRACKER_TYPE_GROUP"] or "Group-based tracker",
|
||||
}
|
||||
local function GetTrackerTypeValues()
|
||||
return HMGT.GetTrackerTypeOptions and HMGT:GetTrackerTypeOptions() or {
|
||||
normal = L["OPT_TRACKER_TYPE_NORMAL"] or "Normal tracker",
|
||||
group = L["OPT_TRACKER_TYPE_GROUP"] or "Group-based tracker",
|
||||
}
|
||||
end
|
||||
|
||||
local function GetPresetLabel(presetKey)
|
||||
local definition = HMGT.GetTrackerPresetDefinition and HMGT:GetTrackerPresetDefinition(presetKey) or nil
|
||||
if not definition then
|
||||
return tostring(presetKey or (L["OPT_TRACKER"] or "Tracker"))
|
||||
end
|
||||
if type(definition.defaultName) == "function" then
|
||||
return tostring(definition.defaultName(L))
|
||||
end
|
||||
return tostring(definition.defaultName or definition.moduleName or presetKey)
|
||||
end
|
||||
|
||||
local function GetTrackerVisibilitySummary(tracker)
|
||||
local parts = {}
|
||||
@@ -180,7 +193,7 @@ local function GetTrackerSummaryText(tracker)
|
||||
local display = tracker.showBar and (L["OPT_DISPLAY_BAR"] or "Progress bars") or (L["OPT_DISPLAY_ICON"] or "Icons")
|
||||
|
||||
return table.concat({
|
||||
string.format("|cffffd100%s|r: %s", L["OPT_TRACKER_TYPE"] or "Tracker type", TRACKER_TYPE_VALUES[tracker.trackerType or "normal"] or (L["OPT_TRACKER_TYPE_NORMAL"] or "Normal tracker")),
|
||||
string.format("|cffffd100%s|r: %s", L["OPT_TRACKER_TYPE"] or "Tracker type", GetTrackerTypeValues()[tracker.trackerType or "normal"] or (L["OPT_TRACKER_TYPE_NORMAL"] or "Normal tracker")),
|
||||
string.format("|cffffd100%s|r: %s", L["OPT_TRACKER_CATEGORIES"] or "Categories", GetTrackerCategoriesSummary(tracker)),
|
||||
string.format("|cffffd100%s|r: %s", L["OPT_STATUS_MODE"] or "Mode", modeLabel),
|
||||
string.format("|cffffd100%s|r: %s", L["OPT_STATUS_DISPLAY"] or "Display", display),
|
||||
@@ -814,7 +827,7 @@ local function BuildGlobalSpellBrowserArgs()
|
||||
end
|
||||
|
||||
local function BuildTrackerOverviewArgs()
|
||||
return {
|
||||
local args = {
|
||||
description = {
|
||||
type = "description",
|
||||
order = 1,
|
||||
@@ -833,22 +846,36 @@ local function BuildTrackerOverviewArgs()
|
||||
return string.format("%s\n\n%s (%d): %s", body, L["OPT_TRACKERS"] or "Tracker Bars", #trackers, table.concat(names, ", "))
|
||||
end,
|
||||
},
|
||||
addTracker = {
|
||||
}
|
||||
|
||||
local definitions = HMGT.GetTrackerPresetDefinitions and HMGT:GetTrackerPresetDefinitions() or {}
|
||||
local presetKeys = {}
|
||||
for presetKey in pairs(definitions) do
|
||||
presetKeys[#presetKeys + 1] = presetKey
|
||||
end
|
||||
table.sort(presetKeys, function(a, b)
|
||||
return GetPresetLabel(a) < GetPresetLabel(b)
|
||||
end)
|
||||
|
||||
for index, presetKey in ipairs(presetKeys) do
|
||||
args["addPreset_" .. presetKey] = {
|
||||
type = "execute",
|
||||
order = 2,
|
||||
order = 2 + index,
|
||||
width = "full",
|
||||
name = L["OPT_ADD_TRACKER"] or "Add tracker",
|
||||
name = function()
|
||||
return string.format("%s: %s", L["OPT_ADD_TRACKER"] or "Add tracker", GetPresetLabel(presetKey))
|
||||
end,
|
||||
func = function()
|
||||
local nextId = HMGT:GetNextTrackerId()
|
||||
local tracker = HMGT:CreateTrackerConfig(nextId, {
|
||||
name = string.format("%s %d", L["OPT_TRACKER"] or "Tracker", nextId),
|
||||
})
|
||||
local tracker = HMGT:BuildTrackerConfigFromPreset(presetKey, nextId)
|
||||
HMGT.db.profile.trackers = HMGT.db.profile.trackers or {}
|
||||
HMGT.db.profile.trackers[#HMGT.db.profile.trackers + 1] = tracker
|
||||
TriggerTrackerUpdate(true)
|
||||
end,
|
||||
},
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
return args
|
||||
end
|
||||
|
||||
local function BuildTrackerGroup(trackerId, order)
|
||||
@@ -1008,7 +1035,7 @@ local function BuildTrackerGroup(trackerId, order)
|
||||
width = "full",
|
||||
name = L["OPT_TRACKER_TYPE"] or "Tracker type",
|
||||
desc = L["OPT_TRACKER_TYPE_DESC"] or "Choose whether this tracker uses one shared frame or separate frames per group member.",
|
||||
values = TRACKER_TYPE_VALUES,
|
||||
values = GetTrackerTypeValues,
|
||||
get = function()
|
||||
local tracker = s()
|
||||
return (tracker and tracker.trackerType) or "normal"
|
||||
|
||||
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