Compare commits

12 Commits

Author SHA1 Message Date
Torsten Brendgen
f97b7556cd Refactor code structure for improved readability and maintainability 2026-04-28 23:09:04 +02:00
Torsten Brendgen
cf78405148 nightly commit 2026-04-25 22:49:22 +02:00
feaa62309c Merge pull request 'dev-v.2.1.0' (#10) from dev-v.2.1.0 into dev
Reviewed-on: #10
2026-04-25 16:21:06 +00:00
Torsten Brendgen
02e062d66b delted old debug window, added new version notice window, added new features to tracker module, updated locales, and updated main addon files. 2026-04-25 17:33:32 +02:00
Torsten Brendgen
f1d2a761e4 initial commit v.2.1.0 2026-04-24 23:43:55 +02:00
Torsten Brendgen
258cadeba5 Dev Build 2.0.1 2026-04-22 16:20:47 +02:00
Torsten Brendgen
8c37da2d38 Adding Events for Hail Mary Bridge Addon 2026-04-21 18:26:12 +02:00
Torsten Brendgen
6151b434b1 Enhance version management features and localization for HMGT 2026-04-16 16:34:01 +02:00
Torsten Brendgen
50ff7c93b4 dev 2026-04-16 12:21:06 +02:00
Torsten Brendgen
7ab82e7655 dev 2026-04-16 12:19:02 +02:00
Torsten Brendgen
e5d39d88ea Workflow Fix 2026-04-16 12:09:25 +02:00
Torsten Brendgen
c3326dde88 updated Readme.md 2026-04-16 12:02:06 +02:00
39 changed files with 6273 additions and 4632 deletions

View File

@@ -19,38 +19,41 @@ jobs:
- name: Variablen setzen - name: Variablen setzen
run: | run: |
set -e set -e
echo "REPO=${{ gitea.repository }}" >> $GITHUB_ENV echo "REPO=${{ gitea.repository }}" >> "$GITHUB_ENV"
echo "TAG=${{ gitea.ref_name }}" >> $GITHUB_ENV echo "TAG=${{ gitea.ref_name }}" >> "$GITHUB_ENV"
echo "SERVER_URL=${{ gitea.server_url }}" >> $GITHUB_ENV echo "SERVER_URL=https://git.local.unique-studios.de" >> "$GITHUB_ENV"
echo "API_BASE=${{ gitea.server_url }}/api/v1" >> $GITHUB_ENV echo "API_BASE=https://git.local.unique-studios.de/api/v1" >> "$GITHUB_ENV"
- name: Repo klonen - name: Repo klonen
run: | run: |
set -e set -e
rm -rf /tmp/repo /tmp/build /tmp/release.json /tmp/assets.json rm -rf /tmp/repo /tmp/build /tmp/release.json /tmp/assets.json
CLONE_URL="${SERVER_URL#https://}" git clone "https://oauth2:${{ secrets.PAT_TOKEN }}@git.local.unique-studios.de/${REPO}.git" /tmp/repo
git clone "https://oauth2:${{ secrets.PAT_TOKEN }}@${CLONE_URL}/${REPO}.git" /tmp/repo
- name: ZIP mit Addon-Ordner bauen - name: ZIP mit Addon-Ordner bauen
run: | run: |
set -e set -e
mkdir -p /tmp/build/HailMaryGuildTools
REPO_NAME="${REPO##*/}"
mkdir -p "/tmp/build/${REPO_NAME}"
rsync -a \ rsync -a \
--exclude='.git' \ --exclude='.git' \
--exclude='.gitea' \ --exclude='.gitea' \
/tmp/repo/ /tmp/build/HailMaryGuildTools/ /tmp/repo/ "/tmp/build/${REPO_NAME}/"
cd /tmp/build cd /tmp/build
zip -r "/tmp/HailMaryGuildTools-${TAG}.zip" HailMaryGuildTools zip -r "/tmp/${REPO_NAME}-${TAG}.zip" "${REPO_NAME}"
ls -lh "/tmp/HailMaryGuildTools-${TAG}.zip" ls -lh "/tmp/${REPO_NAME}-${TAG}.zip"
- name: Release anlegen oder laden - name: Release anlegen oder laden
run: | run: |
set -e set -e
API="${API_BASE}/repos/${REPO}" API="${API_BASE}/repos/${REPO}"
REPO_NAME="${REPO##*/}"
echo "Server: ${SERVER_URL}" echo "Server: ${SERVER_URL}"
echo "Repo: ${REPO}" echo "Repo: ${REPO}"
@@ -72,7 +75,7 @@ jobs:
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{ -d "{
\"tag_name\": \"${TAG}\", \"tag_name\": \"${TAG}\",
\"name\": \"${REPO##*/} ${TAG}\", \"name\": \"${REPO_NAME} ${TAG}\",
\"draft\": false, \"draft\": false,
\"prerelease\": false \"prerelease\": false
}" \ }" \
@@ -91,7 +94,8 @@ jobs:
set -e set -e
RELEASE_ID=$(jq -r '.id' /tmp/release.json) RELEASE_ID=$(jq -r '.id' /tmp/release.json)
FILE_NAME="HailMaryGuildTools-${TAG}.zip" REPO_NAME="${REPO##*/}"
FILE_NAME="${REPO_NAME}-${TAG}.zip"
ASSET_API="${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets" ASSET_API="${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets"
curl --fail -s \ curl --fail -s \
@@ -117,7 +121,8 @@ jobs:
set -e set -e
RELEASE_ID=$(jq -r '.id' /tmp/release.json) RELEASE_ID=$(jq -r '.id' /tmp/release.json)
FILE="/tmp/HailMaryGuildTools-${TAG}.zip" REPO_NAME="${REPO##*/}"
FILE="/tmp/${REPO_NAME}-${TAG}.zip"
FILE_NAME="$(basename "$FILE")" FILE_NAME="$(basename "$FILE")"
UPLOAD_URL="${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${FILE_NAME}" UPLOAD_URL="${API_BASE}/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${FILE_NAME}"

View File

@@ -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

View File

@@ -5,7 +5,7 @@ if not HMGT then return end
local L = HMGT.L or LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME) local L = HMGT.L or LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
HMGT.devToolsBuffer = HMGT.devToolsBuffer or {} HMGT.devToolsBuffer = HMGT.devToolsBuffer or {}
HMGT.devToolsBufferMax = HMGT.devToolsBufferMax or 300 HMGT.devToolsBufferMax = HMGT.devToolsBufferMax or 500
local DEVTOOLS_SCOPE_ALL = "ALL" local DEVTOOLS_SCOPE_ALL = "ALL"
local DEVTOOLS_SCOPE_LABELS = { local DEVTOOLS_SCOPE_LABELS = {
@@ -20,7 +20,8 @@ local DEVTOOLS_SCOPE_LABELS = {
local DEVTOOLS_LEVELS = { local DEVTOOLS_LEVELS = {
error = 1, error = 1,
trace = 2, info = 2,
verbose = 3,
} }
local function TrimText(value) local function TrimText(value)
@@ -76,8 +77,10 @@ function HMGT:GetDevToolsSettings()
profile.devTools = type(profile.devTools) == "table" and profile.devTools or {} profile.devTools = type(profile.devTools) == "table" and profile.devTools or {}
local settings = profile.devTools local settings = profile.devTools
settings.enabled = settings.enabled == true settings.enabled = settings.enabled == true
if settings.level ~= "error" and settings.level ~= "trace" then if settings.level == "trace" then
settings.level = "error" settings.level = "verbose"
elseif settings.level ~= "error" and settings.level ~= "info" and settings.level ~= "verbose" then
settings.level = "info"
end end
if type(settings.scope) ~= "string" or settings.scope == "" then if type(settings.scope) ~= "string" or settings.scope == "" then
settings.scope = DEVTOOLS_SCOPE_ALL settings.scope = DEVTOOLS_SCOPE_ALL
@@ -94,24 +97,25 @@ function HMGT:IsDevToolsEnabled()
end end
function HMGT:GetDevToolsLevelOptions() function HMGT:GetDevToolsLevelOptions()
return { return self:GetDebugLevelOptions()
error = L["OPT_DEVTOOLS_LEVEL_ERROR"] or "Errors",
trace = L["OPT_DEVTOOLS_LEVEL_TRACE"] or "Trace",
}
end end
function HMGT:GetConfiguredDevToolsLevel() function HMGT:GetConfiguredDevToolsLevel()
return self:GetDevToolsSettings().level or "error" return self:GetConfiguredDebugLevel()
end end
function HMGT:ShouldIncludeDevToolsLevel(level) function HMGT:ShouldIncludeDevToolsLevel(level)
local configured = self:GetConfiguredDevToolsLevel() local configured = self:GetConfiguredDevToolsLevel()
return (DEVTOOLS_LEVELS[tostring(level or "error")] or DEVTOOLS_LEVELS.error) local normalizedLevel = tostring(level or "info")
<= (DEVTOOLS_LEVELS[configured] or DEVTOOLS_LEVELS.error) if normalizedLevel == "trace" then
normalizedLevel = "verbose"
end
return (DEVTOOLS_LEVELS[normalizedLevel] or DEVTOOLS_LEVELS.info)
<= (DEVTOOLS_LEVELS[configured] or DEVTOOLS_LEVELS.info)
end end
function HMGT:GetDevToolsScopeOptions() function HMGT:GetDevToolsScopeOptions()
local values = { local values = self:GetDebugScopeOptions() or {
[DEVTOOLS_SCOPE_ALL] = L["OPT_DEVTOOLS_SCOPE_ALL"] or "All scopes", [DEVTOOLS_SCOPE_ALL] = L["OPT_DEVTOOLS_SCOPE_ALL"] or "All scopes",
} }
for scope, label in pairs(DEVTOOLS_SCOPE_LABELS) do for scope, label in pairs(DEVTOOLS_SCOPE_LABELS) do
@@ -128,8 +132,11 @@ end
function HMGT:FormatDevToolsEntry(entry) function HMGT:FormatDevToolsEntry(entry)
local stamp = tostring(entry and entry.stamp or date("%H:%M:%S")) local stamp = tostring(entry and entry.stamp or date("%H:%M:%S"))
local level = string.upper(tostring(entry and entry.level or "error")) local level = string.upper(tostring(entry and entry.level or "info"))
local scope = tostring(entry and entry.scope or "System") local scope = tostring(entry and entry.scope or "System")
if entry and entry.kind == "debug" then
return string.format("%s [%s][%s] %s", stamp, level, scope, tostring(entry.message or ""))
end
local eventName = tostring(entry and entry.event or "") local eventName = tostring(entry and entry.event or "")
local payload = TrimText(entry and entry.payload or "") local payload = TrimText(entry and entry.payload or "")
if payload ~= "" then if payload ~= "" then
@@ -164,8 +171,10 @@ function HMGT:RecordDevEvent(level, scope, eventName, payload)
end end
local normalizedLevel = tostring(level or "error") local normalizedLevel = tostring(level or "error")
if normalizedLevel ~= "error" and normalizedLevel ~= "trace" then if normalizedLevel == "trace" then
normalizedLevel = "trace" normalizedLevel = "verbose"
elseif normalizedLevel ~= "error" and normalizedLevel ~= "info" and normalizedLevel ~= "verbose" then
normalizedLevel = "verbose"
end end
if not self:ShouldIncludeDevToolsLevel(normalizedLevel) then if not self:ShouldIncludeDevToolsLevel(normalizedLevel) then
return return
@@ -182,6 +191,7 @@ function HMGT:RecordDevEvent(level, scope, eventName, payload)
scope = normalizedScope, scope = normalizedScope,
event = TrimText(eventName or "event"), event = TrimText(eventName or "event"),
payload = EncodePayloadValue(payload, 0), payload = EncodePayloadValue(payload, 0),
kind = "event",
} }
table.insert(self.devToolsBuffer, entry) table.insert(self.devToolsBuffer, entry)
@@ -194,6 +204,40 @@ function HMGT:RecordDevEvent(level, scope, eventName, payload)
end end
end end
function HMGT:RecordDebugEntry(level, scope, message)
if not self:IsDevToolsEnabled() then
return
end
local normalizedLevel = tostring(level or "info")
if normalizedLevel == "trace" then
normalizedLevel = "verbose"
elseif normalizedLevel ~= "error" and normalizedLevel ~= "info" and normalizedLevel ~= "verbose" then
normalizedLevel = "info"
end
local normalizedScope = TrimText(scope or "General")
if normalizedScope == "" then
normalizedScope = "General"
end
self.devToolsBuffer = self.devToolsBuffer or {}
self.devToolsBuffer[#self.devToolsBuffer + 1] = {
stamp = date("%H:%M:%S"),
level = normalizedLevel,
scope = normalizedScope,
message = TrimText(message or ""),
kind = "debug",
}
while #self.devToolsBuffer > (tonumber(self.devToolsBufferMax) or 500) do
table.remove(self.devToolsBuffer, 1)
end
if self.devToolsWindow and self.devToolsWindow:IsShown() and self.RefreshDevToolsWindow then
self:RefreshDevToolsWindow()
end
end
function HMGT:DevError(scope, eventName, payload) function HMGT:DevError(scope, eventName, payload)
self:RecordDevEvent("error", scope, eventName, payload) self:RecordDevEvent("error", scope, eventName, payload)
end end

View File

@@ -7,7 +7,7 @@ local AceGUI = LibStub("AceGUI-3.0", true)
if not AceGUI then return end if not AceGUI then return end
local function GetOrderedLevels() local function GetOrderedLevels()
return { "error", "trace" } return { "error", "info", "verbose" }
end end
local function GetOrderedScopes() local function GetOrderedScopes()
@@ -78,8 +78,8 @@ function HMGT:EnsureDevToolsWindow()
local settings = self:GetDevToolsSettings() local settings = self:GetDevToolsSettings()
local window = self:CreateAceWindow("devTools", { local window = self:CreateAceWindow("devTools", {
title = L["DEVTOOLS_WINDOW_TITLE"] or "HMGT Developer Tools", title = L["DEVTOOLS_WINDOW_TITLE"] or "HMGT Debug Console",
statusText = L["DEVTOOLS_WINDOW_HINT"] or "Structured developer events for the current session", statusText = L["DEVTOOLS_WINDOW_HINT"] or "Debug and developer events for the current session",
statusTable = settings.window, statusTable = settings.window,
width = settings.window.width or 920, width = settings.window.width or 920,
height = settings.window.height or 420, height = settings.window.height or 420,
@@ -93,7 +93,7 @@ function HMGT:EnsureDevToolsWindow()
local content = window:GetContent() local content = window:GetContent()
local clearButton = AceGUI:Create("Button") local clearButton = AceGUI:Create("Button")
clearButton:SetText(L["OPT_DEVTOOLS_CLEAR"] or "Clear developer log") clearButton:SetText(L["OPT_DEVTOOLS_CLEAR"] or L["OPT_DEBUG_CLEAR"] or "Clear log")
clearButton:SetWidth(140) clearButton:SetWidth(140)
clearButton:SetCallback("OnClick", function() clearButton:SetCallback("OnClick", function()
HMGT:ClearDevToolsLog() HMGT:ClearDevToolsLog()
@@ -176,11 +176,11 @@ function HMGT:RefreshDevToolsWindow()
end end
local levelOptions = self:GetDevToolsLevelOptions() local levelOptions = self:GetDevToolsLevelOptions()
SetFilterButtonText(window.levelFilter, L["OPT_DEVTOOLS_LEVEL"] or "Capture level", levelOptions[self:GetConfiguredDevToolsLevel()]) SetFilterButtonText(window.levelFilter, L["OPT_DEBUG_LEVEL"] or L["OPT_DEVTOOLS_LEVEL"] or "Level", levelOptions[self:GetConfiguredDevToolsLevel()])
local scopeValues = self:GetDevToolsScopeOptions() local scopeValues = self:GetDevToolsScopeOptions()
local currentScope = self:GetDevToolsSettings().scope or "ALL" local currentScope = self:GetDevToolsSettings().scope or "ALL"
SetFilterButtonText(window.scopeFilter, L["OPT_DEVTOOLS_SCOPE"] or "Scope", scopeValues[currentScope] or currentScope) SetFilterButtonText(window.scopeFilter, L["OPT_DEBUG_SCOPE"] or L["OPT_DEVTOOLS_SCOPE"] or "Module", scopeValues[currentScope] or currentScope)
local text = table.concat(self:GetFilteredDevToolsLines(), "\n") local text = table.concat(self:GetFilteredDevToolsLines(), "\n")
window.logWidget:SetText(text) window.logWidget:SetText(text)

View File

@@ -3,6 +3,218 @@ local HMGT = _G[ADDON_NAME]
if not HMGT then return end if not HMGT then return end
local L = HMGT.L or LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME) local L = HMGT.L or LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
local AceGUI = LibStub("AceGUI-3.0", true)
local CLASS_ICON_TCOORDS = CLASS_ICON_TCOORDS or {}
local function NormalizeName(name)
if HMGT.NormalizePlayerName then
return HMGT:NormalizePlayerName(name)
end
return tostring(name or "")
end
local function GetRosterRows()
local rows = {}
local seen = {}
local function addUnit(unitId)
if not unitId or not UnitExists(unitId) then
return
end
local name = NormalizeName(UnitName(unitId))
if not name or name == "" or seen[name] then
return
end
seen[name] = true
rows[#rows + 1] = {
name = name,
class = select(2, UnitClass(unitId)),
isLeader = UnitIsGroupLeader and UnitIsGroupLeader(unitId) or false,
isAssistant = UnitIsGroupAssistant and UnitIsGroupAssistant(unitId) or false,
connected = UnitIsConnected and UnitIsConnected(unitId) ~= false or true,
isPlayer = UnitIsUnit and UnitIsUnit(unitId, "player") or unitId == "player",
}
end
if IsInRaid() then
for i = 1, GetNumGroupMembers() do
addUnit("raid" .. i)
end
elseif IsInGroup() then
addUnit("player")
for i = 1, GetNumSubgroupMembers() do
addUnit("party" .. i)
end
else
addUnit("player")
end
table.sort(rows, function(a, b)
if a.isLeader ~= b.isLeader then
return a.isLeader
end
if a.isPlayer ~= b.isPlayer then
return a.isPlayer
end
return tostring(a.name or "") < tostring(b.name or "")
end)
return rows
end
local function GetPlayerVersionText(name)
local normalized = NormalizeName(name)
if normalized == NormalizeName(UnitName("player")) then
return tostring(HMGT.ADDON_VERSION or "dev"), tonumber(HMGT.PROTOCOL_VERSION) or 0, true
end
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
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)
if not texture then
return
end
local coords = classTag and CLASS_ICON_TCOORDS[classTag]
if coords then
texture:SetTexture("Interface\\GLUES\\CHARACTERCREATE\\UI-CHARACTERCREATE-CLASSES")
texture:SetTexCoord(coords[1], coords[2], coords[3], coords[4])
texture:Show()
else
texture:SetTexture(nil)
texture:Hide()
end
end
local function AcquireVersionRow(window, index)
window.versionRows = window.versionRows or {}
local row = window.versionRows[index]
if row then
return row
end
local parent = window.scrollChild
row = CreateFrame("Frame", nil, parent)
row:SetHeight(22)
row.background = row:CreateTexture(nil, "BACKGROUND")
row.background:SetAllPoints(row)
row.background:SetColorTexture(1, 1, 1, 0.03)
row.classIcon = row:CreateTexture(nil, "ARTWORK")
row.classIcon:SetSize(16, 16)
row.classIcon:SetPoint("LEFT", row, "LEFT", 4, 0)
row.nameText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
row.nameText:SetPoint("LEFT", row.classIcon, "RIGHT", 6, 0)
row.nameText:SetJustifyH("LEFT")
row.versionText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
row.versionText:SetPoint("LEFT", row, "LEFT", 250, 0)
row.versionText:SetWidth(150)
row.versionText:SetJustifyH("LEFT")
row.protocolText = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
row.protocolText:SetPoint("LEFT", row, "LEFT", 410, 0)
row.protocolText:SetWidth(100)
row.protocolText:SetJustifyH("LEFT")
window.versionRows[index] = row
return row
end
function HMGT:RefreshVersionNoticeWindow()
local window = self.versionNoticeWindow
if not window then
return
end
local roster = GetRosterRows()
local localName = NormalizeName(UnitName("player"))
for index, info in ipairs(roster) do
local row = AcquireVersionRow(window, index)
row:ClearAllPoints()
if index == 1 then
row:SetPoint("TOPLEFT", window.scrollChild, "TOPLEFT", 0, 0)
row:SetPoint("TOPRIGHT", window.scrollChild, "TOPRIGHT", 0, 0)
else
row:SetPoint("TOPLEFT", window.versionRows[index - 1], "BOTTOMLEFT", 0, -2)
row:SetPoint("TOPRIGHT", window.versionRows[index - 1], "BOTTOMRIGHT", 0, -2)
end
ApplyClassIcon(row.classIcon, info.class)
local nameLabel = tostring(info.name or UNKNOWN)
if info.isLeader then
nameLabel = string.format("%s %s", nameLabel, L["VERSION_WINDOW_LEADER_TAG"] or "(Leader)")
elseif info.isAssistant then
nameLabel = string.format("%s %s", nameLabel, L["VERSION_WINDOW_ASSISTANT_TAG"] or "(Assist)")
end
if info.isPlayer or info.name == localName then
nameLabel = string.format("%s %s", nameLabel, L["VERSION_WINDOW_SELF_TAG"] or "(You)")
end
row.nameText:SetText(nameLabel)
row.nameText:SetTextColor(1, 0.82, 0.1, 1)
local versionText, protocol, hasAddon = GetPlayerVersionText(info.name)
if hasAddon then
row.versionText:SetText(versionText or "?")
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
row.versionText:SetText(L["VERSION_WINDOW_MISSING_ADDON"] or "Addon not installed")
row.versionText:SetTextColor(1, 0.25, 0.25, 1)
row.protocolText:SetText("-")
row.protocolText:SetTextColor(1, 0.25, 0.25, 1)
end
row:Show()
end
if window.versionRows then
for index = #roster + 1, #window.versionRows do
window.versionRows[index]:Hide()
end
end
local contentHeight = math.max(1, (#roster * 24))
window.scrollChild:SetHeight(contentHeight)
local known = 0
for _, info in ipairs(roster) do
local _, _, hasAddon = GetPlayerVersionText(info.name)
if hasAddon then
known = known + 1
end
end
window.messageText:SetText(L["VERSION_WINDOW_MESSAGE"] or "Hail Mary Guild Tools versions in your current group")
window.detailText:SetText(string.format(
L["VERSION_WINDOW_CURRENT"] or "Current version: %s | Protocol: %s",
tostring(HMGT.ADDON_VERSION or "dev"),
tostring(HMGT.PROTOCOL_VERSION or "?")
))
window:SetStatusText(string.format(
L["VERSION_WINDOW_STATUS"] or "Detected HMGT on %d/%d players",
tonumber(known) or 0,
tonumber(#roster) or 0
))
end
function HMGT:EnsureVersionNoticeWindow() function HMGT:EnsureVersionNoticeWindow()
if self.versionNoticeWindow then if self.versionNoticeWindow then
@@ -10,21 +222,21 @@ function HMGT:EnsureVersionNoticeWindow()
end end
self.versionNoticeWindowStatus = self.versionNoticeWindowStatus or { self.versionNoticeWindowStatus = self.versionNoticeWindowStatus or {
width = 560, width = 640,
height = 240, height = 420,
} }
local window = self:CreateAceWindow("versionNotice", { local window = self:CreateAceWindow("versionNotice", {
title = L["VERSION_WINDOW_TITLE"] or "HMGT Version Check", title = L["VERSION_WINDOW_TITLE"] or "HMGT Version Check",
statusText = "", statusText = "",
statusTable = self.versionNoticeWindowStatus, statusTable = self.versionNoticeWindowStatus,
width = self.versionNoticeWindowStatus.width or 560, width = self.versionNoticeWindowStatus.width or 640,
height = self.versionNoticeWindowStatus.height or 240, height = self.versionNoticeWindowStatus.height or 420,
backgroundTexture = "Interface\\AddOns\\HailMaryGuildTools\\Media\\HailMaryLogo.png", backgroundTexture = "Interface\\AddOns\\HailMaryGuildTools\\Media\\HailMaryLogo.png",
backgroundWidth = 220, backgroundWidth = 220,
backgroundHeight = 120, backgroundHeight = 120,
backgroundOffsetY = -8, backgroundOffsetY = -8,
backgroundAlpha = 0.12, backgroundAlpha = 0.08,
strata = "FULLSCREEN_DIALOG", strata = "FULLSCREEN_DIALOG",
}) })
if not window then if not window then
@@ -32,26 +244,64 @@ function HMGT:EnsureVersionNoticeWindow()
end end
local content = window:GetContent() local content = window:GetContent()
local messageText = content:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge") local messageText = content:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
messageText:SetPoint("TOPLEFT", content, "TOPLEFT", 28, -28) messageText:SetPoint("TOPLEFT", content, "TOPLEFT", 24, -22)
messageText:SetPoint("TOPRIGHT", content, "TOPRIGHT", -28, -28) messageText:SetPoint("TOPRIGHT", content, "TOPRIGHT", -24, -22)
messageText:SetJustifyH("CENTER") messageText:SetJustifyH("LEFT")
messageText:SetJustifyV("MIDDLE")
messageText:SetTextColor(1, 0.82, 0.1, 1) messageText:SetTextColor(1, 0.82, 0.1, 1)
messageText:SetText(L["VERSION_WINDOW_MESSAGE"] or "A new version of Hail Mary Guild Tools is available.")
window.messageText = messageText window.messageText = messageText
local detailText = content:CreateFontString(nil, "OVERLAY", "GameFontHighlight") local detailText = content:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
detailText:SetPoint("TOPLEFT", messageText, "BOTTOMLEFT", 0, -18) detailText:SetPoint("TOPLEFT", messageText, "BOTTOMLEFT", 0, -8)
detailText:SetPoint("TOPRIGHT", messageText, "BOTTOMRIGHT", 0, -18) detailText:SetPoint("TOPRIGHT", messageText, "BOTTOMRIGHT", 0, -8)
detailText:SetJustifyH("CENTER") detailText:SetJustifyH("LEFT")
detailText:SetJustifyV("TOP")
if detailText.SetSpacing then
detailText:SetSpacing(2)
end
detailText:SetTextColor(0.9, 0.9, 0.9, 1) detailText:SetTextColor(0.9, 0.9, 0.9, 1)
window.detailText = detailText window.detailText = detailText
local refreshButton = AceGUI and AceGUI:Create("Button") or nil
if refreshButton then
refreshButton:SetText(L["VERSION_WINDOW_REFRESH"] or "Refresh")
refreshButton:SetWidth(120)
refreshButton:SetCallback("OnClick", function()
HMGT:RequestSync("VersionWindow")
HMGT:RefreshVersionNoticeWindow()
end)
refreshButton.frame:SetParent(window.frame)
refreshButton.frame:ClearAllPoints()
refreshButton.frame:SetPoint("TOPRIGHT", content, "TOPRIGHT", -24, -68)
refreshButton.frame:Show()
window.refreshButton = refreshButton
end
local header = CreateFrame("Frame", nil, content)
header:SetPoint("TOPLEFT", detailText, "BOTTOMLEFT", 0, -14)
header:SetPoint("TOPRIGHT", content, "TOPRIGHT", -24, -96)
header:SetHeight(18)
window.header = header
local nameHeader = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
nameHeader:SetPoint("LEFT", header, "LEFT", 24, 0)
nameHeader:SetText(L["VERSION_WINDOW_COLUMN_PLAYER"] or "Player")
local versionHeader = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
versionHeader:SetPoint("LEFT", header, "LEFT", 250, 0)
versionHeader:SetText(L["VERSION_WINDOW_COLUMN_VERSION"] or "Version")
local protocolHeader = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
protocolHeader:SetPoint("LEFT", header, "LEFT", 410, 0)
protocolHeader:SetText(L["VERSION_WINDOW_COLUMN_PROTOCOL"] or "Protocol")
local scrollFrame = CreateFrame("ScrollFrame", nil, content, "UIPanelScrollFrameTemplate")
scrollFrame:SetPoint("TOPLEFT", header, "BOTTOMLEFT", 0, -6)
scrollFrame:SetPoint("BOTTOMRIGHT", content, "BOTTOMRIGHT", -28, 24)
window.scrollFrame = scrollFrame
local scrollChild = CreateFrame("Frame", nil, scrollFrame)
scrollChild:SetSize(1, 1)
scrollFrame:SetScrollChild(scrollChild)
window.scrollChild = scrollChild
self.versionNoticeWindow = window self.versionNoticeWindow = window
return window return window
end end
@@ -67,36 +317,16 @@ function HMGT:ShowVersionMismatchPopup(playerName, detail, sourceTag, opts)
} }
end end
local info = self.latestVersionMismatch or {}
local window = self:EnsureVersionNoticeWindow() local window = self:EnsureVersionNoticeWindow()
if not window then if not window then
return return
end end
local hasMismatch = info.playerName or info.detail
window:SetTitle(L["VERSION_WINDOW_TITLE"] or "HMGT Version Check") self:RefreshVersionNoticeWindow()
self:DevTrace("Version", "window_show", {
if hasMismatch then player = playerName,
window.messageText:SetText(L["VERSION_WINDOW_MESSAGE"] or "A new version of Hail Mary Guild Tools is available.") source = sourceTag,
window.detailText:SetText(string.format( detail = detail,
L["VERSION_WINDOW_DETAIL"] or "Detected via %s from %s.\n%s",
tostring(info.sourceTag or "?"),
tostring(info.playerName or UNKNOWN),
tostring(info.detail or "")
))
else
window.messageText:SetText(L["VERSION_WINDOW_NO_MISMATCH"] or "No newer HMGT version has been detected in your current group.")
window.detailText:SetText(string.format(
L["VERSION_WINDOW_CURRENT"] or "Current version: %s | Protocol: %s",
tostring(HMGT.ADDON_VERSION or "dev"),
tostring(HMGT.PROTOCOL_VERSION or "?")
))
end
self:DevTrace("Version", hasMismatch and "window_show_mismatch" or "window_show_current", {
player = info.playerName,
source = info.sourceTag,
detail = info.detail,
}) })
window:Show() window:Show()
window:Raise() window:Raise()

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
## Interface: 120000,120001 ## Interface: 120000,120001,120005
## IconTexture: Interface\Addons\HailMaryGuildTools\Media\HailMaryIcon.png ## IconTexture: Interface\Addons\HailMaryGuildTools\Media\HailMaryIcon.png
## Author: Torsten Brendgen ## Author: Torsten Brendgen
## Title: Hail Mary Guild Tools ## Title: Hail Mary Guild Tools
@@ -31,10 +31,20 @@ HailMaryGuildToolsOptions.lua
# ────── Tracker ────────────────────────────────────────────────────── # ────── Tracker ──────────────────────────────────────────────────────
Modules\Tracker\Frame.lua Modules\Tracker\Frame.lua
Modules\Tracker\SpellDatabase.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\InterruptTracker\InterruptSpellDatabase.lua
Modules\Tracker\RaidcooldownTracker\RaidCooldownSpellDatabase.lua Modules\Tracker\RaidCooldownTracker\RaidCooldownSpellDatabase.lua
Modules\Tracker\GroupCooldownTracker\GroupCooldownSpellDatabase.lua Modules\Tracker\GroupCooldownTracker\GroupCooldownSpellDatabase.lua
Modules\Tracker\TrackerManager.lua Modules\Tracker\TrackerManager.lua
Modules\Tracker\NormalTrackerFrames.lua Modules\Tracker\NormalTrackerFrames.lua
@@ -55,5 +65,9 @@ Modules\MapOverlay\MapOverlay.xml
Modules\RaidTimeline\RaidTimelineBossAbilityData.lua Modules\RaidTimeline\RaidTimelineBossAbilityData.lua
Modules\RaidTimeline\RaidTimeline.lua Modules\RaidTimeline\RaidTimeline.lua
Modules\RaidTimeline\RaidTimelineBigWigs.lua Modules\RaidTimeline\RaidTimelineBigWigs.lua
Modules\RaidTimeline\RaidTimelineDBM.lua
Modules\RaidTimeline\RaidTimelineOptions.lua Modules\RaidTimeline\RaidTimelineOptions.lua
# EncounterAlerts
Modules\EncounterAlerts\EncounterAlerts.lua
Modules\EncounterAlerts\LuraRunes.lua
Modules\EncounterAlerts\EncounterAlertsOptions.lua

View File

@@ -17,6 +17,9 @@ function HMGT_Config:RegisterOptionsProvider(id, provider)
if type(id) ~= "string" or id == "" then return false end if type(id) ~= "string" or id == "" then return false end
if type(provider) ~= "function" then return false end if type(provider) ~= "function" then return false end
self._optionProviders[id] = provider self._optionProviders[id] = provider
if type(self.RebuildRootOptions) == "function" then
self:RebuildRootOptions()
end
return true return true
end end
@@ -1835,6 +1838,88 @@ function HMGT_Config:Initialize()
end end
end, end,
}, },
devToolsEnabled = {
type = "toggle",
order = 2,
width = "full",
name = L["OPT_DEVTOOLS_MODE"] or L["OPT_DEBUG_MODE"] or "Debug console",
desc = L["OPT_DEVTOOLS_MODE_DESC"] or L["OPT_DEBUG_MODE_DESC"] or "Enable the debug console.",
get = function()
return HMGT.GetDevToolsSettings and HMGT:GetDevToolsSettings().enabled == true
end,
set = function(_, value)
if not HMGT.GetDevToolsSettings then
return
end
HMGT:GetDevToolsSettings().enabled = value == true
if HMGT.UpdateDevToolsWindowVisibility then
HMGT:UpdateDevToolsWindowVisibility()
end
end,
},
debugLevel = {
type = "select",
order = 3,
width = "full",
name = L["OPT_DEBUG_LEVEL"] or "Debug level",
values = function()
return HMGT.GetDebugLevelOptions and HMGT:GetDebugLevelOptions() or {}
end,
get = function()
return HMGT.GetConfiguredDebugLevel and HMGT:GetConfiguredDebugLevel() or "info"
end,
set = function(_, value)
if HMGT.GetDevToolsSettings then
HMGT:GetDevToolsSettings().level = value or "info"
end
if HMGT.RefreshDevToolsWindow then
HMGT:RefreshDevToolsWindow()
end
end,
},
debugScope = {
type = "select",
order = 4,
width = "full",
name = L["OPT_DEBUG_SCOPE"] or "Module filter",
values = function()
return HMGT.GetDebugScopeOptions and HMGT:GetDebugScopeOptions() or {}
end,
get = function()
local settings = HMGT.GetDevToolsSettings and HMGT:GetDevToolsSettings() or {}
return settings.scope or "ALL"
end,
set = function(_, value)
if HMGT.GetDevToolsSettings then
HMGT:GetDevToolsSettings().scope = value or "ALL"
end
if HMGT.RefreshDevToolsWindow then
HMGT:RefreshDevToolsWindow()
end
end,
},
openDebug = {
type = "execute",
order = 5,
width = "half",
name = L["OPT_DEVTOOLS_OPEN"] or L["OPT_DEBUG_OPEN"] or "Open debug console",
func = function()
if HMGT.OpenDevToolsWindow then
HMGT:OpenDevToolsWindow()
end
end,
},
clearDebug = {
type = "execute",
order = 6,
width = "half",
name = L["OPT_DEVTOOLS_CLEAR"] or L["OPT_DEBUG_CLEAR"] or "Clear debug log",
func = function()
if HMGT.ClearDevToolsLog then
HMGT:ClearDevToolsLog()
end
end,
},
}, },
}, },
commands = { commands = {
@@ -1850,6 +1935,8 @@ function HMGT_Config:Initialize()
name = table.concat({ name = table.concat({
"|cffffd100/hmgt|r", "|cffffd100/hmgt|r",
"|cffffd100/hmgt debug|r", "|cffffd100/hmgt debug|r",
"|cffffd100/hmgt status|r",
"|cffffd100/hmgt lura|r",
"|cffffd100/hmgt version|r", "|cffffd100/hmgt version|r",
}, "\n"), }, "\n"),
}, },
@@ -2000,6 +2087,15 @@ function HMGT_Config:Initialize()
modulesGroup.args.raidTimeline = raidTimelineGroup modulesGroup.args.raidTimeline = raidTimelineGroup
end end
local encounterAlertsGroup = BuildNamedModuleGroup(
"encounterAlerts",
L["OPT_MODULE_ENCOUNTER_ALERTS"] or "Encounter Alerts",
50
)
if encounterAlertsGroup then
modulesGroup.args.encounterAlerts = encounterAlertsGroup
end
if next(modulesGroup.args) == nil then if next(modulesGroup.args) == nil then
return nil return nil
end end
@@ -2037,12 +2133,20 @@ function HMGT_Config:Initialize()
}, },
} }
function HMGT_Config:RebuildRootOptions()
local modulesGroup = BuildModulesGroup() local modulesGroup = BuildModulesGroup()
if modulesGroup then if modulesGroup then
rootOptions.args.modules = modulesGroup rootOptions.args.modules = modulesGroup
else
rootOptions.args.modules = nil
end
NormalizeExecuteButtonWidths(rootOptions)
if AceConfigRegistry and type(AceConfigRegistry.NotifyChange) == "function" then
AceConfigRegistry:NotifyChange(ADDON_NAME)
end
end end
NormalizeExecuteButtonWidths(rootOptions) HMGT_Config:RebuildRootOptions()
local aceConfig = LibStub("AceConfig-3.0") local aceConfig = LibStub("AceConfig-3.0")
local aceConfigDialog = LibStub("AceConfigDialog-3.0") local aceConfigDialog = LibStub("AceConfigDialog-3.0")

View File

@@ -18,12 +18,23 @@ L["SLASH_HINT"] = "/hmgt Optionen | /hmgt lock/unlock | /hmgt dem
L["VERSION_MISMATCH_CHAT"] = "Versionskonflikt mit %s: %s" L["VERSION_MISMATCH_CHAT"] = "Versionskonflikt mit %s: %s"
L["VERSION_MISMATCH_POPUP"] = "HailMaryGuildTools Konflikt mit %s.\n%s\nQuelle: %s" L["VERSION_MISMATCH_POPUP"] = "HailMaryGuildTools Konflikt mit %s.\n%s\nQuelle: %s"
L["VERSION_WINDOW_TITLE"] = "HMGT Versionscheck" L["VERSION_WINDOW_TITLE"] = "HMGT Versionscheck"
L["VERSION_WINDOW_MESSAGE"] = "Es gibt eine neue Version von Hail Mary Guild Tools." L["VERSION_WINDOW_MESSAGE"] = "Hail Mary Guild Tools Versionen in deiner aktuellen Gruppe"
L["VERSION_WINDOW_DETAIL"] = "Erkannt ueber %s von %s.\n%s" 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_NO_MISMATCH"] = "In deiner aktuellen Gruppe wurde keine neuere HMGT-Version erkannt."
L["VERSION_WINDOW_CURRENT"] = "Aktuelle Version: %s | Protokoll: %s" L["VERSION_WINDOW_CURRENT"] = "Aktuelle Version: %s | Protokoll: %s"
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)"
L["VERSION_WINDOW_SELF_TAG"] = "(Du)"
L["VERSION_OUTDATED_WHISPER"] = "Deine Hail Mary Guild Tools Version ist veraltet. Du hast %s, der Gruppenleiter hat %s."
L["VERSION_WINDOW_DEBUG_ONLY"] = "HMGT: /hmgt version ist nur bei aktiviertem Debugmodus verfuegbar." L["VERSION_WINDOW_DEBUG_ONLY"] = "HMGT: /hmgt version ist nur bei aktiviertem Debugmodus verfuegbar."
L["VERSION_WINDOW_DEVTOOLS_ONLY"] = "HMGT: /hmgt version ist nur bei aktivierten Entwicklerwerkzeugen verfuegbar." L["VERSION_WINDOW_DEVTOOLS_ONLY"] = "HMGT: /hmgt version ist nur bei aktivierter Debug-Konsole verfuegbar."
-- ── Options: general ───────────────────────────────────────── -- ── Options: general ─────────────────────────────────────────
L["OPT_GENERAL"] = "Allgemein" L["OPT_GENERAL"] = "Allgemein"
@@ -53,17 +64,47 @@ L["OPT_DEBUG_CLEAR"] = "Debug-Log leeren"
L["OPT_DEBUG_SELECT_ALL"] = "Alles markieren" L["OPT_DEBUG_SELECT_ALL"] = "Alles markieren"
L["DEBUG_WINDOW_TITLE"] = "HMGT Debug-Konsole" L["DEBUG_WINDOW_TITLE"] = "HMGT Debug-Konsole"
L["DEBUG_WINDOW_HINT"] = "Mit dem Mausrad scrollen, Strg+A markiert alles, Strg+C kopiert markierten Text" L["DEBUG_WINDOW_HINT"] = "Mit dem Mausrad scrollen, Strg+A markiert alles, Strg+C kopiert markierten Text"
L["OPT_DEVTOOLS_MODE"] = "Entwicklerwerkzeuge" L["OPT_DEVTOOLS_MODE"] = "Debug-Konsole"
L["OPT_DEVTOOLS_MODE_DESC"] = "Aktiviert die strukturierte Entwickler-Konsole." L["OPT_DEVTOOLS_MODE_DESC"] = "Aktiviert das gemeinsame Debug- und Entwickler-Log."
L["OPT_DEVTOOLS_LEVEL"] = "Erfassungsstufe" L["OPT_DEVTOOLS_LEVEL"] = "Debug-Stufe"
L["OPT_DEVTOOLS_LEVEL_ERROR"] = "Fehler" L["OPT_DEVTOOLS_LEVEL_ERROR"] = "Fehler"
L["OPT_DEVTOOLS_LEVEL_TRACE"] = "Trace" L["OPT_DEVTOOLS_LEVEL_TRACE"] = "Ausfuehrlich"
L["OPT_DEVTOOLS_SCOPE"] = "Scope-Filter" L["OPT_DEVTOOLS_SCOPE"] = "Modulfilter"
L["OPT_DEVTOOLS_SCOPE_ALL"] = "Alle Scopes" L["OPT_DEVTOOLS_SCOPE_ALL"] = "Alle Module"
L["OPT_DEVTOOLS_OPEN"] = "Entwickler-Konsole oeffnen" L["OPT_DEVTOOLS_OPEN"] = "Debug-Konsole oeffnen"
L["OPT_DEVTOOLS_CLEAR"] = "Entwickler-Log leeren" L["OPT_DEVTOOLS_CLEAR"] = "Debug-Log leeren"
L["OPT_DEVTOOLS_SELECT_ALL"] = "Alles markieren" L["OPT_DEVTOOLS_SELECT_ALL"] = "Alles markieren"
L["OPT_DEVTOOLS_DISABLED"] = "HMGT: Entwicklerwerkzeuge sind nicht aktiviert." L["OPT_DEVTOOLS_DISABLED"] = "HMGT: Entwicklerwerkzeuge sind nicht aktiviert."
L["OPT_MODULE_ENCOUNTER_ALERTS"] = "Encounter Alerts"
L["OPT_ENCOUNTER_ALERTS_PLACEHOLDER"] = "Encounter-spezifische Helper-Frames und Warnungen."
L["OPT_EA_LURA_TITLE"] = "L'ura Runen"
L["OPT_EA_LURA_RUNE_WINDOW"] = "Runen-Fenster"
L["OPT_EA_LURA_ENABLED"] = "L'ura Runen aktivieren"
L["OPT_EA_LURA_UNLOCK"] = "Runen-Frame entsperren"
L["OPT_EA_LURA_HINT"] = "Erste Version: nur Normal/Heroisch Layout. Tank steht unten mittig zwischen Slot 1 und 5."
L["OPT_EA_LURA_SHOW"] = "Anzeigen"
L["OPT_EA_LURA_TEST"] = "Testmuster"
L["OPT_EA_LURA_CLEAR"] = "Leeren"
L["OPT_EA_LURA_BROADCAST"] = "Sequenz senden"
L["OPT_EA_LURA_ACTIONBAR"] = "Runen-Actionbar"
L["OPT_EA_LURA_ACTIONBAR_SHOW"] = "Leiste anzeigen"
L["OPT_EA_LURA_ACTIONBAR_UNLOCK"] = "Leiste entsperren"
L["OPT_EA_LURA_ACTIONBAR_AUTO_SHOW"] = "Automatisch im Bossraum anzeigen"
L["OPT_EA_LURA_ACTIONBAR_ORIENTATION"] = "Ausrichtung"
L["OPT_EA_LURA_ACTIONBAR_HORIZONTAL"] = "Horizontal"
L["OPT_EA_LURA_ACTIONBAR_VERTICAL"] = "Vertikal"
L["OPT_EA_LURA_ACTIONBAR_HINT"] = "Klicke die Runen in beobachteter Reihenfolge. Slot 5 sendet die Sequenz automatisch. Der rote Button leert die lokale Sequenz."
L["OPT_EA_LURA_ICON_SIZE"] = "Icongroesse"
L["OPT_EA_LURA_BACKGROUND_ALPHA"] = "Hintergrund-Alpha"
L["OPT_EA_LURA_ICON_SPACING"] = "Icon-Abstand"
L["OPT_EA_LURA_BORDER_ENABLED"] = "Rahmen anzeigen"
L["OPT_EA_LURA_BORDER_WIDTH"] = "Rahmenbreite"
L["OPT_EA_LURA_BORDER_COLOR"] = "Rahmenfarbe"
L["OPT_EA_LURA_SHOW_LABELS"] = "Labels anzeigen"
L["OPT_EA_LURA_RUNE_EMPTY"] = "Leer"
L["OPT_EA_LURA_DRAG_HINT"] = "Ziehen zum Verschieben"
L["OPT_EA_LURA_BOSS"] = "Boss"
L["OPT_EA_LURA_TANK"] = "Tank"
L["DEVTOOLS_WINDOW_TITLE"] = "HMGT Entwicklerwerkzeuge" L["DEVTOOLS_WINDOW_TITLE"] = "HMGT Entwicklerwerkzeuge"
L["DEVTOOLS_WINDOW_HINT"] = "Strukturierte Entwickler-Ereignisse fuer die aktuelle Sitzung" L["DEVTOOLS_WINDOW_HINT"] = "Strukturierte Entwickler-Ereignisse fuer die aktuelle Sitzung"
L["OPT_SYNC_REMOTE_CHARGES"] = "Remote-Aufladungen synchronisieren" L["OPT_SYNC_REMOTE_CHARGES"] = "Remote-Aufladungen synchronisieren"

View File

@@ -18,12 +18,23 @@ L["SLASH_HINT"] = "/hmgt options | /hmgt lock/unlock | /hmgt demo
L["VERSION_MISMATCH_CHAT"] = "Version mismatch with %s: %s" L["VERSION_MISMATCH_CHAT"] = "Version mismatch with %s: %s"
L["VERSION_MISMATCH_POPUP"] = "HailMaryGuildTools mismatch with %s.\n%s\nSource: %s" L["VERSION_MISMATCH_POPUP"] = "HailMaryGuildTools mismatch with %s.\n%s\nSource: %s"
L["VERSION_WINDOW_TITLE"] = "HMGT Version Check" L["VERSION_WINDOW_TITLE"] = "HMGT Version Check"
L["VERSION_WINDOW_MESSAGE"] = "A new version of Hail Mary Guild Tools is available." L["VERSION_WINDOW_MESSAGE"] = "Hail Mary Guild Tools versions in your current group"
L["VERSION_WINDOW_DETAIL"] = "Detected via %s from %s.\n%s" 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_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_CURRENT"] = "Current version: %s | Protocol: %s"
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)"
L["VERSION_WINDOW_SELF_TAG"] = "(You)"
L["VERSION_OUTDATED_WHISPER"] = "Your Hail Mary Guild Tools version is outdated. You have %s, the group leader has %s."
L["VERSION_WINDOW_DEBUG_ONLY"] = "HMGT: /hmgt version is only available while debug mode is enabled." L["VERSION_WINDOW_DEBUG_ONLY"] = "HMGT: /hmgt version is only available while debug mode is enabled."
L["VERSION_WINDOW_DEVTOOLS_ONLY"] = "HMGT: /hmgt version is only available while developer tools are enabled." L["VERSION_WINDOW_DEVTOOLS_ONLY"] = "HMGT: /hmgt version is only available while the debug console is enabled."
-- ── Options: general ───────────────────────────────────────── -- ── Options: general ─────────────────────────────────────────
L["OPT_GENERAL"] = "General" L["OPT_GENERAL"] = "General"
@@ -53,17 +64,47 @@ L["OPT_DEBUG_CLEAR"] = "Clear debug log"
L["OPT_DEBUG_SELECT_ALL"] = "Select all" L["OPT_DEBUG_SELECT_ALL"] = "Select all"
L["DEBUG_WINDOW_TITLE"] = "HMGT Debug Console" L["DEBUG_WINDOW_TITLE"] = "HMGT Debug Console"
L["DEBUG_WINDOW_HINT"] = "Mouse wheel scrolls, Ctrl+A selects all, Ctrl+C copies selected text" L["DEBUG_WINDOW_HINT"] = "Mouse wheel scrolls, Ctrl+A selects all, Ctrl+C copies selected text"
L["OPT_DEVTOOLS_MODE"] = "Developer tools" L["OPT_DEVTOOLS_MODE"] = "Debug console"
L["OPT_DEVTOOLS_MODE_DESC"] = "Enable the structured developer event console." L["OPT_DEVTOOLS_MODE_DESC"] = "Enable the shared debug and developer log."
L["OPT_DEVTOOLS_LEVEL"] = "Capture level" L["OPT_DEVTOOLS_LEVEL"] = "Debug level"
L["OPT_DEVTOOLS_LEVEL_ERROR"] = "Errors" L["OPT_DEVTOOLS_LEVEL_ERROR"] = "Errors"
L["OPT_DEVTOOLS_LEVEL_TRACE"] = "Trace" L["OPT_DEVTOOLS_LEVEL_TRACE"] = "Verbose"
L["OPT_DEVTOOLS_SCOPE"] = "Scope filter" L["OPT_DEVTOOLS_SCOPE"] = "Module filter"
L["OPT_DEVTOOLS_SCOPE_ALL"] = "All scopes" L["OPT_DEVTOOLS_SCOPE_ALL"] = "All modules"
L["OPT_DEVTOOLS_OPEN"] = "Open developer console" L["OPT_DEVTOOLS_OPEN"] = "Open debug console"
L["OPT_DEVTOOLS_CLEAR"] = "Clear developer log" L["OPT_DEVTOOLS_CLEAR"] = "Clear debug log"
L["OPT_DEVTOOLS_SELECT_ALL"] = "Select all" L["OPT_DEVTOOLS_SELECT_ALL"] = "Select all"
L["OPT_DEVTOOLS_DISABLED"] = "HMGT: developer tools are not enabled." L["OPT_DEVTOOLS_DISABLED"] = "HMGT: developer tools are not enabled."
L["OPT_MODULE_ENCOUNTER_ALERTS"] = "Encounter Alerts"
L["OPT_ENCOUNTER_ALERTS_PLACEHOLDER"] = "Encounter-specific helper frames and alerts."
L["OPT_EA_LURA_TITLE"] = "L'ura Runes"
L["OPT_EA_LURA_RUNE_WINDOW"] = "Rune window"
L["OPT_EA_LURA_ENABLED"] = "Enable L'ura runes"
L["OPT_EA_LURA_UNLOCK"] = "Unlock rune frame"
L["OPT_EA_LURA_HINT"] = "First version: normal/heroic layout only. Tank reference is placed bottom-center between slot 1 and 5."
L["OPT_EA_LURA_SHOW"] = "Show"
L["OPT_EA_LURA_TEST"] = "Test pattern"
L["OPT_EA_LURA_CLEAR"] = "Clear"
L["OPT_EA_LURA_BROADCAST"] = "Send sequence"
L["OPT_EA_LURA_ACTIONBAR"] = "Rune action bar"
L["OPT_EA_LURA_ACTIONBAR_SHOW"] = "Show bar"
L["OPT_EA_LURA_ACTIONBAR_UNLOCK"] = "Unlock bar"
L["OPT_EA_LURA_ACTIONBAR_AUTO_SHOW"] = "Auto show in boss room"
L["OPT_EA_LURA_ACTIONBAR_ORIENTATION"] = "Orientation"
L["OPT_EA_LURA_ACTIONBAR_HORIZONTAL"] = "Horizontal"
L["OPT_EA_LURA_ACTIONBAR_VERTICAL"] = "Vertical"
L["OPT_EA_LURA_ACTIONBAR_HINT"] = "Click rune buttons in the observed order. Slot 5 sends the sequence automatically. The red button clears the local sequence."
L["OPT_EA_LURA_ICON_SIZE"] = "Icon size"
L["OPT_EA_LURA_BACKGROUND_ALPHA"] = "Background alpha"
L["OPT_EA_LURA_ICON_SPACING"] = "Icon spacing"
L["OPT_EA_LURA_BORDER_ENABLED"] = "Show border"
L["OPT_EA_LURA_BORDER_WIDTH"] = "Border width"
L["OPT_EA_LURA_BORDER_COLOR"] = "Border color"
L["OPT_EA_LURA_SHOW_LABELS"] = "Show labels"
L["OPT_EA_LURA_RUNE_EMPTY"] = "Empty"
L["OPT_EA_LURA_DRAG_HINT"] = "Drag to move"
L["OPT_EA_LURA_BOSS"] = "Boss"
L["OPT_EA_LURA_TANK"] = "Tank"
L["DEVTOOLS_WINDOW_TITLE"] = "HMGT Developer Tools" L["DEVTOOLS_WINDOW_TITLE"] = "HMGT Developer Tools"
L["DEVTOOLS_WINDOW_HINT"] = "Structured developer events for the current session" L["DEVTOOLS_WINDOW_HINT"] = "Structured developer events for the current session"
L["OPT_SYNC_REMOTE_CHARGES"] = "Sync remote charges" L["OPT_SYNC_REMOTE_CHARGES"] = "Sync remote charges"

View File

@@ -0,0 +1,102 @@
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, true) or {}
local EA = HMGT:NewModule("EncounterAlerts", "AceEvent-3.0")
HMGT.EncounterAlerts = EA
EA.runtimeEnabled = false
function EA:GetSettings()
local profile = HMGT.db and HMGT.db.profile
profile = profile or {}
profile.encounterAlerts = type(profile.encounterAlerts) == "table" and profile.encounterAlerts or {}
return profile.encounterAlerts
end
function EA:GetLuraRunesSettings()
local settings = self:GetSettings()
settings.luraRunes = type(settings.luraRunes) == "table" and settings.luraRunes or {}
return settings.luraRunes
end
function EA:OnEnable()
local settings = self:GetSettings()
self.runtimeEnabled = settings.enabled == true
self:RegisterEvent("PLAYER_ENTERING_WORLD", "RefreshLuraRunesContext")
self:RegisterEvent("ZONE_CHANGED", "RefreshLuraRunesContext")
self:RegisterEvent("ZONE_CHANGED_INDOORS", "RefreshLuraRunesContext")
self:RegisterEvent("ZONE_CHANGED_NEW_AREA", "RefreshLuraRunesContext")
self:RegisterEvent("GROUP_ROSTER_UPDATE", "RefreshLuraRunesContext")
self:RegisterEvent("INSTANCE_ENCOUNTER_ENGAGE_UNIT", "RefreshLuraRunesContext")
self:RegisterEvent("PLAYER_TARGET_CHANGED", "RefreshLuraRunesContext")
self:RegisterEvent("ENCOUNTER_START", "HandleLuraEncounterStart")
self:RegisterEvent("ENCOUNTER_END", "HandleLuraEncounterEnd")
self:RegisterEvent("CHAT_MSG_RAID", "HandleLuraRaidChat")
self:RegisterEvent("CHAT_MSG_RAID_LEADER", "HandleLuraRaidChat")
if self.LuraRunes and self.LuraRunes.Refresh then
self.LuraRunes:Refresh()
end
end
function EA:Enable()
local settings = self:GetSettings()
settings.enabled = true
self.runtimeEnabled = true
if self.LuraRunes and self.LuraRunes.Refresh then
self.LuraRunes:Refresh()
end
end
function EA:Disable()
local settings = self:GetSettings()
settings.enabled = false
self.runtimeEnabled = false
if self.LuraRunes and self.LuraRunes.Hide then
self.LuraRunes:Hide()
end
end
function EA:GetDisplayName()
return L["OPT_MODULE_ENCOUNTER_ALERTS"] or "Encounter Alerts"
end
function EA:RefreshLuraRunesContext(event)
if self.LuraRunes and self.LuraRunes.RefreshContext then
self.LuraRunes:RefreshContext(event)
end
end
function EA:HandleLuraEncounterStart(_, encounterId, encounterName)
if self.LuraRunes and self.LuraRunes.OnEncounterStart then
self.LuraRunes:OnEncounterStart(encounterId, encounterName)
end
end
function EA:HandleLuraEncounterEnd(_, encounterId)
if self.LuraRunes and self.LuraRunes.OnEncounterEnd then
self.LuraRunes:OnEncounterEnd(encounterId)
end
end
function EA:HandleLuraRunesComm(senderName, payload)
if self.LuraRunes and self.LuraRunes.HandleComm then
self.LuraRunes:HandleComm(senderName, payload)
end
end
function EA:HandleLuraRaidChat(event, message, senderName)
if self.LuraRunes and self.LuraRunes.HandleRaidChatMessage then
self.LuraRunes:HandleRaidChatMessage(message, senderName, event)
end
end
function EA:HandleSlashCommand(input)
if self.LuraRunes and self.LuraRunes.HandleSlashCommand then
self.LuraRunes:HandleSlashCommand(input)
else
HMGT:OpenConfig()
end
end

View File

@@ -0,0 +1,414 @@
local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
if not HMGT then return end
if not HMGT_Config or not HMGT_Config.RegisterOptionsProvider then return end
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME, true) or {}
local AceConfigRegistry = LibStub("AceConfigRegistry-3.0", true)
local function NotifyOptionsChanged()
if AceConfigRegistry and type(AceConfigRegistry.NotifyChange) == "function" then
AceConfigRegistry:NotifyChange(ADDON_NAME)
end
end
local function GetSettings()
local profile = HMGT.db and HMGT.db.profile
if not profile then
return {}
end
profile.encounterAlerts = type(profile.encounterAlerts) == "table" and profile.encounterAlerts or {}
return profile.encounterAlerts
end
local function GetLuraSettings()
local settings = GetSettings()
settings.luraRunes = type(settings.luraRunes) == "table" and settings.luraRunes or {}
settings.luraRunes.slots = type(settings.luraRunes.slots) == "table" and settings.luraRunes.slots or {}
settings.luraRunes.actionBar = type(settings.luraRunes.actionBar) == "table" and settings.luraRunes.actionBar or {}
return settings.luraRunes
end
local function GetLuraActionBarSettings()
local settings = GetLuraSettings()
settings.actionBar.shown = settings.actionBar.shown == true
settings.actionBar.autoShow = settings.actionBar.autoShow ~= false
settings.actionBar.unlocked = settings.actionBar.unlocked == true
settings.actionBar.iconSize = tonumber(settings.actionBar.iconSize) or 42
settings.actionBar.iconSpacing = tonumber(settings.actionBar.iconSpacing) or 8
settings.actionBar.orientation = tostring(settings.actionBar.orientation or "horizontal")
if settings.actionBar.orientation ~= "vertical" then
settings.actionBar.orientation = "horizontal"
end
settings.actionBar.border = type(settings.actionBar.border) == "table" and settings.actionBar.border or {}
return settings.actionBar
end
local function GetLuraBorderSettings()
local actionBar = GetLuraActionBarSettings()
actionBar.border.enabled = actionBar.border.enabled == true
actionBar.border.width = tonumber(actionBar.border.width) or 2
actionBar.border.color = type(actionBar.border.color) == "table" and actionBar.border.color or {}
actionBar.border.color.r = tonumber(actionBar.border.color.r) or 1
actionBar.border.color.g = tonumber(actionBar.border.color.g) or 0.82
actionBar.border.color.b = tonumber(actionBar.border.color.b) or 0.1
actionBar.border.color.a = tonumber(actionBar.border.color.a) or 0.9
return actionBar.border
end
local function GetActionBarOrientationValues()
return {
horizontal = L["OPT_EA_LURA_ACTIONBAR_HORIZONTAL"] or "Horizontal",
vertical = L["OPT_EA_LURA_ACTIONBAR_VERTICAL"] or "Vertical",
}
end
local function GetLuraRunes()
return HMGT.EncounterAlerts and HMGT.EncounterAlerts.LuraRunes or nil
end
local function RefreshEncounterAlerts()
if HMGT.EncounterAlerts then
local settings = GetSettings()
HMGT.EncounterAlerts.runtimeEnabled = settings.enabled == true
if HMGT.EncounterAlerts.LuraRunes and HMGT.EncounterAlerts.LuraRunes.Refresh then
HMGT.EncounterAlerts.LuraRunes:Refresh()
end
end
NotifyOptionsChanged()
end
local function BuildRuneWindowOptions()
return {
type = "group",
inline = true,
order = 2,
name = L["OPT_EA_LURA_RUNE_WINDOW"] or "Rune window",
args = {
unlocked = {
type = "toggle",
order = 1,
width = "double",
name = L["OPT_EA_LURA_UNLOCK"] or "Unlock rune frame",
get = function()
return GetLuraSettings().unlocked == true
end,
set = function(_, value)
GetSettings().enabled = true
local settings = GetLuraSettings()
settings.enabled = true
settings.unlocked = value == true
RefreshEncounterAlerts()
end,
},
hint = {
type = "description",
order = 2,
width = "full",
name = L["OPT_EA_LURA_HINT"] or "First version: normal/heroic layout only. Tank reference is placed bottom-center between slot 1 and 5.",
},
show = {
type = "execute",
order = 3,
width = 0.8,
name = L["OPT_EA_LURA_SHOW"] or "Show",
func = function()
local lura = GetLuraRunes()
if lura and lura.Show then
lura:Show()
end
end,
},
test = {
type = "execute",
order = 4,
width = 0.9,
name = L["OPT_EA_LURA_TEST"] or "Test pattern",
func = function()
local lura = GetLuraRunes()
if lura and lura.Show and lura.ApplyTestPattern then
lura:Show()
lura:ApplyTestPattern()
end
end,
},
clear = {
type = "execute",
order = 5,
width = 0.8,
name = L["OPT_EA_LURA_CLEAR"] or "Clear",
func = function()
local lura = GetLuraRunes()
if lura and lura.ClearAssignments then
lura:ClearAssignments(false)
end
end,
},
broadcast = {
type = "execute",
order = 6,
width = 1.2,
name = L["OPT_EA_LURA_BROADCAST"] or "Broadcast",
disabled = function()
local lura = GetLuraRunes()
return lura and lura.CanBroadcastSequence and not lura:CanBroadcastSequence() or false
end,
func = function()
local lura = GetLuraRunes()
if lura and lura.BroadcastAssignments then
lura:BroadcastAssignments()
end
end,
},
iconSize = {
type = "range",
order = 7,
width = 1.1,
min = 28,
max = 80,
step = 1,
name = L["OPT_EA_LURA_ICON_SIZE"] or "Icon size",
get = function()
return tonumber(GetLuraSettings().iconSize) or 44
end,
set = function(_, value)
GetLuraSettings().iconSize = tonumber(value) or 44
RefreshEncounterAlerts()
end,
},
backgroundAlpha = {
type = "range",
order = 8,
width = 1.1,
min = 0,
max = 0.8,
step = 0.01,
name = L["OPT_EA_LURA_BACKGROUND_ALPHA"] or "Background alpha",
get = function()
return tonumber(GetLuraSettings().backgroundAlpha) or 0.14
end,
set = function(_, value)
GetLuraSettings().backgroundAlpha = tonumber(value) or 0.14
RefreshEncounterAlerts()
end,
},
},
}
end
local function BuildRuneActionBarOptions()
return {
type = "group",
inline = true,
order = 3,
name = L["OPT_EA_LURA_ACTIONBAR"] or "Rune action bar",
args = {
shown = {
type = "toggle",
order = 1,
width = 1.1,
name = L["OPT_EA_LURA_ACTIONBAR_SHOW"] or "Show bar",
get = function()
return GetLuraActionBarSettings().shown == true
end,
set = function(_, value)
GetSettings().enabled = true
local settings = GetLuraSettings()
settings.enabled = true
GetLuraActionBarSettings().shown = value == true
RefreshEncounterAlerts()
end,
},
unlocked = {
type = "toggle",
order = 2,
width = 1.2,
name = L["OPT_EA_LURA_ACTIONBAR_UNLOCK"] or "Unlock bar",
get = function()
return GetLuraActionBarSettings().unlocked == true
end,
set = function(_, value)
GetSettings().enabled = true
local settings = GetLuraSettings()
settings.enabled = true
local actionBar = GetLuraActionBarSettings()
actionBar.shown = true
actionBar.unlocked = value == true
RefreshEncounterAlerts()
end,
},
autoShow = {
type = "toggle",
order = 2.5,
width = 1.4,
name = L["OPT_EA_LURA_ACTIONBAR_AUTO_SHOW"] or "Auto show in boss room",
get = function()
return GetLuraActionBarSettings().autoShow == true
end,
set = function(_, value)
GetLuraActionBarSettings().autoShow = value == true
RefreshEncounterAlerts()
end,
},
orientation = {
type = "select",
order = 3,
width = 1.2,
name = L["OPT_EA_LURA_ACTIONBAR_ORIENTATION"] or "Orientation",
values = GetActionBarOrientationValues,
get = function()
return GetLuraActionBarSettings().orientation
end,
set = function(_, value)
local actionBar = GetLuraActionBarSettings()
actionBar.orientation = tostring(value or "horizontal")
RefreshEncounterAlerts()
end,
},
iconSize = {
type = "range",
order = 5,
width = 1.1,
min = 28,
max = 80,
step = 1,
name = L["OPT_EA_LURA_ICON_SIZE"] or "Icon size",
get = function()
return tonumber(GetLuraActionBarSettings().iconSize) or 42
end,
set = function(_, value)
GetLuraActionBarSettings().iconSize = tonumber(value) or 42
RefreshEncounterAlerts()
end,
},
iconSpacing = {
type = "range",
order = 6,
width = 1.1,
min = 0,
max = 80,
step = 1,
name = L["OPT_EA_LURA_ICON_SPACING"] or "Icon spacing",
get = function()
return tonumber(GetLuraActionBarSettings().iconSpacing) or 8
end,
set = function(_, value)
GetLuraActionBarSettings().iconSpacing = tonumber(value) or 8
RefreshEncounterAlerts()
end,
},
borderEnabled = {
type = "toggle",
order = 7,
width = 1.1,
name = L["OPT_EA_LURA_BORDER_ENABLED"] or "Show border",
get = function()
return GetLuraBorderSettings().enabled == true
end,
set = function(_, value)
GetLuraBorderSettings().enabled = value == true
RefreshEncounterAlerts()
end,
},
borderWidth = {
type = "range",
order = 8,
width = 1.1,
min = 1,
max = 12,
step = 1,
name = L["OPT_EA_LURA_BORDER_WIDTH"] or "Border width",
disabled = function()
return GetLuraBorderSettings().enabled ~= true
end,
get = function()
return tonumber(GetLuraBorderSettings().width) or 2
end,
set = function(_, value)
GetLuraBorderSettings().width = tonumber(value) or 2
RefreshEncounterAlerts()
end,
},
borderColor = {
type = "color",
order = 9,
width = 1.1,
hasAlpha = true,
name = L["OPT_EA_LURA_BORDER_COLOR"] or "Border color",
disabled = function()
return GetLuraBorderSettings().enabled ~= true
end,
get = function()
local color = GetLuraBorderSettings().color
return color.r or 1, color.g or 0.82, color.b or 0.1, color.a or 0.9
end,
set = function(_, r, g, b, a)
local color = GetLuraBorderSettings().color
color.r = tonumber(r) or 1
color.g = tonumber(g) or 0.82
color.b = tonumber(b) or 0.1
color.a = tonumber(a) or 0.9
RefreshEncounterAlerts()
end,
},
hint = {
type = "description",
order = 10,
width = "full",
name = L["OPT_EA_LURA_ACTIONBAR_HINT"] or "Click rune buttons in the observed order. Slot 5 sends the sequence automatically.",
},
},
}
end
HMGT_Config:RegisterOptionsProvider("encounterAlerts", function()
return {
path = "encounterAlerts",
order = 50,
group = {
type = "group",
name = L["OPT_MODULE_ENCOUNTER_ALERTS"] or "Encounter Alerts",
order = 50,
childGroups = "tab",
args = {
general = {
type = "group",
name = L["OPT_GENERAL"] or "General",
order = 1,
args = {
description = {
type = "description",
order = 1,
width = "full",
name = L["OPT_ENCOUNTER_ALERTS_PLACEHOLDER"] or "Encounter-specific helper frames and alerts.",
},
},
},
luraRunes = {
type = "group",
name = L["OPT_EA_LURA_TITLE"] or "L'ura Runes",
order = 2,
args = {
enabled = {
type = "toggle",
order = 1,
width = "double",
name = L["OPT_EA_LURA_ENABLED"] or "Enable L'ura runes",
get = function()
return GetLuraSettings().enabled == true
end,
set = function(_, value)
local enabled = value == true
GetSettings().enabled = enabled
GetLuraSettings().enabled = enabled
RefreshEncounterAlerts()
end,
},
runeWindow = BuildRuneWindowOptions(),
actionBar = BuildRuneActionBarOptions(),
},
},
},
},
}
end)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1 @@

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@@ -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.

View File

@@ -1,692 +1,69 @@
-- Modules/GroupCooldownTracker.lua
-- Group-Cooldown-Tracker Modul (ein Frame pro Spieler in der Gruppe)
local ADDON_NAME = "HailMaryGuildTools" local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME) local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
if not HMGT then return end if not HMGT then return end
local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME) local L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
local GCT = HMGT:NewModule("GroupCooldownTracker") local module = HMGT:NewModule("GroupCooldownTracker")
HMGT.GroupCooldownTracker = GCT HMGT.GroupCooldownTracker = module
GCT.frame = nil module.definition = {
GCT.frames = {} moduleName = "GroupCooldownTracker",
dbKey = "groupCooldownTracker",
local function SanitizeFrameToken(name) trackerType = "group",
if not name or name == "" then return "Unknown" end trackerKey = "groupCooldownTracker",
return name:gsub("[^%w_]", "_") title = function()
end return L["GCD_TITLE"]
end,
local function ShortName(name) categories = { "tank", "defensive", "healing", "cc", "utility", "offensive", "lust", "interrupt" },
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",
} }
local PARTY_FRAME_PATTERNS = { function module:GetDefinition()
"PartyMemberFrame%d", -- Blizzard alt return self.definition
"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 end
local remaining = tonumber(entry.remaining) or 0 function module:GetSettings()
if remaining > 0 then for _, tracker in ipairs(HMGT:GetTrackerConfigs()) do
return true if tracker.trackerKey == self.definition.trackerKey then
end return tracker
local maxCharges = tonumber(entry.maxCharges) or 0
local currentCharges = tonumber(entry.currentCharges)
if maxCharges > 0 and currentCharges ~= nil and currentCharges < maxCharges then
return true
end
return false
end
local function BuildAnchorLayoutSignature(settings, ordered, unitByPlayer)
local parts = {
settings.attachToPartyFrame == true and "attach" or "stack",
tostring(settings.partyAttachSide or "RIGHT"),
tostring(tonumber(settings.partyAttachOffsetX) or 8),
tostring(tonumber(settings.partyAttachOffsetY) or 0),
tostring(settings.showBar and "bar" or "icon"),
tostring(settings.growDirection or "DOWN"),
tostring(settings.width or 250),
tostring(settings.barHeight or 20),
tostring(settings.iconSize or 32),
tostring(settings.iconCols or 6),
tostring(settings.barSpacing or 2),
tostring(settings.locked),
tostring(settings.anchorTo or "UIParent"),
tostring(settings.anchorPoint or "TOPLEFT"),
tostring(settings.anchorRelPoint or "TOPLEFT"),
tostring(settings.anchorX or settings.posX or 0),
tostring(settings.anchorY or settings.posY or 0),
}
for _, playerName in ipairs(ordered or {}) do
parts[#parts + 1] = tostring(playerName)
parts[#parts + 1] = tostring(unitByPlayer and unitByPlayer[playerName] or "")
end
return table.concat(parts, "|")
end
local function ResolveNamedUnitFrame(unitId)
if unitId == "player" then
for _, frameName in ipairs(PLAYER_FRAME_CANDIDATES) do
local frame = _G[frameName]
if FrameMatchesUnit(frame, unitId) or (frameName == "PlayerFrame" and IsUsableAnchorFrame(frame)) then
return frame
end end
end end
return nil return nil
end end
local idx = type(unitId) == "string" and unitId:match("^party(%d+)$") function module:Enable()
if not idx then if HMGT.TrackerManager and HMGT.TrackerManager.Enable then
return nil HMGT.TrackerManager:Enable()
end
idx = tonumber(idx)
for _, pattern in ipairs(PARTY_FRAME_PATTERNS) do
local frame = _G[pattern:format(idx)]
if FrameMatchesUnit(frame, unitId) then
return frame
end
end
return nil
end
local function ScanUnitFrame(unitId)
local frame = EnumerateFrames()
local scanned = 0
while frame and scanned < 8000 do
if IsUsableAnchorFrame(frame) then
local unit = GetFrameUnit(frame)
if unit == unitId then
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach scan unit=%s scanned=%d found=true", tostring(unitId), scanned)
return frame
end
end
scanned = scanned + 1
frame = EnumerateFrames(frame)
end
HMGT:DebugScoped("verbose", HMGT:GetTrackerDebugScope("Group Cooldowns"), "GroupAttach scan unit=%s scanned=%d found=false", tostring(unitId), scanned)
return nil
end
local function ResolveUnitAnchorFrame(unitId)
if not unitId then return nil end
local now = GetTime()
local cached = unitFrameCache[unitId]
if cached and now < (cached.expires or 0) then
if cached.frame and cached.frame:IsShown() then
return cached.frame
end
return nil
end
local frame = ResolveNamedUnitFrame(unitId)
if not frame then
frame = ScanUnitFrame(unitId)
end
local expiresIn = 1.0
if frame and frame:IsShown() then
expiresIn = 10.0
end
unitFrameCache[unitId] = {
frame = frame,
expires = now + expiresIn,
}
if frame and frame:IsShown() then
return frame
end
return nil
end
function GCT:GetFrameIdForPlayer(playerName)
return "GroupCooldownTracker_" .. SanitizeFrameToken(playerName)
end
function GCT:GetPlayerFrame(playerName)
if not playerName then return nil end
return self.frames[playerName]
end
function GCT:GetAnchorableFrames()
return self.frames
end
function GCT:EnsurePlayerFrame(playerName)
local frame = self.frames[playerName]
local s = HMGT.db.profile.groupCooldownTracker
if frame then
return frame
end
frame = HMGT.TrackerFrame:CreateTrackerFrame(self:GetFrameIdForPlayer(playerName), s)
frame._hmgtPlayerName = playerName
self.frames[playerName] = frame
return frame
end
function GCT:HideAllFrames()
for _, frame in pairs(self.frames) do
frame:Hide()
end
self.activeOrder = nil
self.unitByPlayer = nil
self.frame = nil
self._lastAnchorLayoutSignature = nil
self._nextAnchorRetryAt = nil
end
function GCT:SetLockedAll(locked)
for _, frame in pairs(self.frames) do
HMGT.TrackerFrame:SetLocked(frame, locked)
end end
end end
function GCT:EnsureUpdateTicker() function module:Disable()
if self.updateTicker then if HMGT.TrackerManager and HMGT.TrackerManager.UpdateDisplay then
return HMGT.TrackerManager:UpdateDisplay()
end
self.updateTicker = C_Timer.NewTicker(0.1, function()
self:UpdateDisplay()
end)
end
function GCT:StopUpdateTicker()
if self.updateTicker then
self.updateTicker:Cancel()
self.updateTicker = nil
end end
end end
function GCT:SetUpdateTickerEnabled(enabled) function module:SetLockedAll(locked)
if enabled then if HMGT.TrackerManager and HMGT.TrackerManager.SetAllLocked then
self:EnsureUpdateTicker() HMGT.TrackerManager:SetAllLocked(locked)
else
self:StopUpdateTicker()
end end
end end
function GCT:InvalidateAnchorLayout() function module:RefreshAnchors(force)
self._lastAnchorLayoutSignature = nil if HMGT.TrackerManager and HMGT.TrackerManager.RefreshAnchors then
self._nextAnchorRetryAt = nil HMGT.TrackerManager:RefreshAnchors(force)
end
function GCT:RefreshAnchors(force)
local s = HMGT.db.profile.groupCooldownTracker
if not s then return end
local ordered = {}
for _, playerName in ipairs(self.activeOrder or {}) do
local frame = self.frames[playerName]
if frame and frame:IsShown() then
table.insert(ordered, playerName)
end end
end end
if #ordered == 0 then function module:InvalidateAnchorLayout()
self.frame = nil if HMGT.TrackerManager and HMGT.TrackerManager.InvalidateAnchorLayout then
self._lastAnchorLayoutSignature = nil HMGT.TrackerManager:InvalidateAnchorLayout()
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
end end
-- Do not force anchor updates while user is dragging a tracker frame. function module:GetAnchorableFrames()
for _, playerName in ipairs(ordered) do if HMGT.TrackerManager and HMGT.TrackerManager.GetAnchorableFrames then
local frame = self.frames[playerName] return HMGT.TrackerManager:GetAnchorableFrames()
if frame and frame._hmgtDragging then
return
end end
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()
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

View File

@@ -42,54 +42,13 @@ function Manager:HidePlayerFrames(frameKey)
end end
function Manager:BuildEntriesByPlayerForTracker(tracker) function Manager:BuildEntriesByPlayerForTracker(tracker)
local frameKey = S.GetTrackerFrameKey(tracker.id) return HMGT:BuildEntriesByPlayerForTracker(
local ownName = HMGT:NormalizePlayerName(UnitName("player")) or "Player" tracker,
if tracker.testMode then self:GetTrackerFrameKey(tracker),
local entries = self:CollectTestEntries(tracker) function(unitId)
if S.IsGroupTracker(tracker) and tracker.attachToPartyFrame == true then return S.ResolveUnitAnchorFrame(unitId)
return S.BuildPartyPreviewEntries(entries)
end 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 end
function Manager:RefreshPerGroupAnchors(tracker, force) function Manager:RefreshPerGroupAnchors(tracker, force)
@@ -206,11 +165,10 @@ function Manager:UpdatePerGroupMemberTracker(tracker)
for _, playerName in ipairs(order) do for _, playerName in ipairs(order) do
local frame = self:EnsurePlayerFrame(tracker, playerName) local frame = self:EnsurePlayerFrame(tracker, playerName)
local entries = byPlayer[playerName] or {} local entries = byPlayer[playerName] or {}
if HMGT.FilterDisplayEntries then local tickThis = false
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
end if tickThis then
if HMGT.SortDisplayEntries then shouldTick = true
HMGT:SortDisplayEntries(entries)
end end
if #entries > 0 then if #entries > 0 then
HMGT.TrackerFrame:UpdateFrame(frame, entries, true) HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
@@ -219,12 +177,6 @@ function Manager:UpdatePerGroupMemberTracker(tracker)
shownOrder[#shownOrder + 1] = playerName shownOrder[#shownOrder + 1] = playerName
shownByPlayer[playerName] = entries shownByPlayer[playerName] = entries
entryCount = entryCount + #entries entryCount = entryCount + #entries
for _, entry in ipairs(entries) do
if S.EntryNeedsVisualTicker(entry) then
shouldTick = true
break
end
end
else else
frame:Hide() frame:Hide()
end end

View File

@@ -97,14 +97,23 @@ HMGT_SpellData.Interrupts = {
-- WARLOCK -- WARLOCK
Spell(19647, "Spell Lock", { Spell(19647, "Spell Lock", {
classes = {"WARLOCK"}, classes = {"WARLOCK"},
specs = {2}, specs = {1, 3},
category = "interrupt", category = "interrupt",
state = { kind = "cooldown", cooldown = 24 }, state = { kind = "cooldown", cooldown = 24 },
}), }),
Spell(119914, "Axe Toss", {
classes = {"WARLOCK"},
specs = {2},
category = "interrupt",
aliases = { 89766 },
petSpellId = 89766,
state = { kind = "cooldown", cooldown = 30 },
}),
Spell(132409, "Spell Lock (Grimoire)", { Spell(132409, "Spell Lock (Grimoire)", {
classes = {"WARLOCK"}, classes = {"WARLOCK"},
specs = {1, 3}, specs = {1, 3},
category = "interrupt", category = "interrupt",
aliases = { 1276467 },
state = { kind = "cooldown", cooldown = 24 }, state = { kind = "cooldown", cooldown = 24 },
}), }),

View File

@@ -1,21 +1,44 @@
-- Modules/InterruptTracker.lua
-- Interrupt tracker based on the shared single-frame tracker base.
local ADDON_NAME = "HailMaryGuildTools" local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME) 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 L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
local Base = HMGT.SingleFrameTrackerBase local module = HMGT:NewModule("InterruptTracker")
if not Base then return end HMGT.InterruptTracker = module
Base:CreateModule("InterruptTracker", { module.definition = {
profileKey = "interruptTracker", moduleName = "InterruptTracker",
frameName = "InterruptTracker", dbKey = "interruptTracker",
trackerType = "normal",
trackerKey = "interruptTracker",
title = function() title = function()
return L["IT_TITLE"] return L["IT_TITLE"]
end, end,
demoKey = "interruptTracker", categories = { "interrupt" },
database = function() }
return HMGT_SpellData.Interrupts
end, function module:GetDefinition()
}) return self.definition
end
function module:GetSettings()
for _, tracker in ipairs(HMGT:GetTrackerConfigs()) do
if tracker.trackerKey == self.definition.trackerKey then
return tracker
end
end
return 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

View File

@@ -3,66 +3,15 @@ local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME)
if not HMGT or not HMGT.TrackerManager then return end if not HMGT or not HMGT.TrackerManager then return end
local Manager = HMGT.TrackerManager local Manager = HMGT.TrackerManager
local S = Manager._shared or {}
function Manager:CollectEntries(tracker) function Manager:CollectEntries(tracker)
local entries = {} return HMGT:CollectTrackerEntries(tracker)
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
end end
function Manager:CollectTestEntries(tracker) function Manager:CollectTestEntries(tracker)
local playerName = HMGT:NormalizePlayerName(UnitName("player")) or "Player" return HMGT:CollectTrackerTestEntries(tracker)
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
end end
function Manager:BuildEntriesForTracker(tracker) function Manager:BuildEntriesForTracker(tracker)
if tracker.testMode then return HMGT:BuildEntriesForTracker(tracker, self:GetTrackerFrameKey(tracker))
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
end end

View File

@@ -1,21 +1,44 @@
-- Modules/RaidCooldownTracker.lua
-- Raid cooldown tracker based on the shared single-frame tracker base.
local ADDON_NAME = "HailMaryGuildTools" local ADDON_NAME = "HailMaryGuildTools"
local HMGT = LibStub("AceAddon-3.0"):GetAddon(ADDON_NAME) 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 L = LibStub("AceLocale-3.0"):GetLocale(ADDON_NAME)
local Base = HMGT.SingleFrameTrackerBase local module = HMGT:NewModule("RaidCooldownTracker")
if not Base then return end HMGT.RaidCooldownTracker = module
Base:CreateModule("RaidCooldownTracker", { module.definition = {
profileKey = "raidCooldownTracker", moduleName = "RaidCooldownTracker",
frameName = "RaidCooldownTracker", dbKey = "raidCooldownTracker",
trackerType = "normal",
trackerKey = "raidCooldownTracker",
title = function() title = function()
return L["RCD_TITLE"] return L["RCD_TITLE"]
end, end,
demoKey = "raidCooldownTracker", categories = { "lust", "defensive", "healing", "tank", "utility", "offensive", "cc", "interrupt" },
database = function() }
return HMGT_SpellData.RaidCooldowns
end, function module:GetDefinition()
}) return self.definition
end
function module:GetSettings()
for _, tracker in ipairs(HMGT:GetTrackerConfigs()) do
if tracker.trackerKey == self.definition.trackerKey then
return tracker
end
end
return 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

View File

@@ -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

View File

@@ -1077,6 +1077,18 @@ function HMGT_SpellData.RebuildLookups()
for _, entry in ipairs(HMGT_SpellData.Interrupts or {}) do for _, entry in ipairs(HMGT_SpellData.Interrupts or {}) do
entry._hmgtDataset = "Interrupts" entry._hmgtDataset = "Interrupts"
HMGT_SpellData.InterruptLookup[entry.spellId] = entry HMGT_SpellData.InterruptLookup[entry.spellId] = entry
if type(entry.aliases) == "table" then
for _, aliasId in ipairs(entry.aliases) do
local sid = tonumber(aliasId)
if sid and sid > 0 then
HMGT_SpellData.InterruptLookup[sid] = entry
end
end
end
local petSpellId = tonumber(entry.petSpellId)
if petSpellId and petSpellId > 0 then
HMGT_SpellData.InterruptLookup[petSpellId] = entry
end
end end
HMGT_SpellData.CooldownLookup = {} HMGT_SpellData.CooldownLookup = {}

View 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

View 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

View 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

View 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

View 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

View File

@@ -94,25 +94,6 @@ local PARTY_FRAME_PATTERNS = {
local unitFrameCache = {} 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 function BuildAnchorLayoutSignature(settings, ordered, unitByPlayer)
local parts = { local parts = {
settings.attachToPartyFrame == true and "attach" or "stack", settings.attachToPartyFrame == true and "attach" or "stack",
@@ -222,55 +203,6 @@ local function ResolveUnitAnchorFrame(unitId)
return nil return nil
end 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) local function GetTrackerLabel(tracker)
if type(tracker) ~= "table" then if type(tracker) ~= "table" then
return "Tracker" return "Tracker"
@@ -287,136 +219,6 @@ local function GetTrackerLabel(tracker)
return "Tracker" return "Tracker"
end 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) local function SortTrackers(trackers)
table.sort(trackers, function(a, b) table.sort(trackers, function(a, b)
local aId = tonumber(a and a.id) or 0 local aId = tonumber(a and a.id) or 0
@@ -467,13 +269,7 @@ Manager._shared.ShortName = ShortName
Manager._shared.BuildAnchorLayoutSignature = BuildAnchorLayoutSignature Manager._shared.BuildAnchorLayoutSignature = BuildAnchorLayoutSignature
Manager._shared.IsGroupTracker = IsGroupTracker Manager._shared.IsGroupTracker = IsGroupTracker
Manager._shared.ResolveUnitAnchorFrame = ResolveUnitAnchorFrame Manager._shared.ResolveUnitAnchorFrame = ResolveUnitAnchorFrame
Manager._shared.GetGroupPlayers = GetGroupPlayers
Manager._shared.GetTrackerLabel = GetTrackerLabel 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 Manager._shared.BuildGroupDisplaySignature = BuildGroupDisplaySignature
function Manager:GetTrackers() function Manager:GetTrackers()
@@ -492,6 +288,13 @@ function Manager:GetTrackers()
return self._trackerCache return self._trackerCache
end end
function Manager:GetTrackerFrameKey(tracker)
if type(tracker) == "table" then
return GetTrackerFrameKey(tracker.id)
end
return GetTrackerFrameKey(tracker)
end
function Manager:MarkTrackersDirty() function Manager:MarkTrackersDirty()
self._trackerCache = nil self._trackerCache = nil
self._trackerCacheSignature = nil self._trackerCacheSignature = nil
@@ -648,12 +451,8 @@ function Manager:RefreshVisibleVisuals()
break break
end end
local entries = byPlayer[playerName] or {} local entries = byPlayer[playerName] or {}
if HMGT.FilterDisplayEntries then local tickThis = false
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
end
if HMGT.SortDisplayEntries then
HMGT:SortDisplayEntries(entries)
end
if #entries == 0 then if #entries == 0 then
needsFullRefresh = true needsFullRefresh = true
break break
@@ -666,11 +465,8 @@ function Manager:RefreshVisibleVisuals()
HMGT.TrackerFrame:UpdateFrame(frame, entries, true) HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
totalEntries = totalEntries + #entries totalEntries = totalEntries + #entries
byPlayerFiltered[playerName] = entries byPlayerFiltered[playerName] = entries
for _, entry in ipairs(entries) do if tickThis then
if EntryNeedsVisualTicker(entry) then
shouldTick = true shouldTick = true
break
end
end end
end end
local newSignature = BuildGroupDisplaySignature(currentOrder, byPlayerFiltered) local newSignature = BuildGroupDisplaySignature(currentOrder, byPlayerFiltered)
@@ -686,12 +482,8 @@ function Manager:RefreshVisibleVisuals()
if not shouldShow then if not shouldShow then
needsFullRefresh = true needsFullRefresh = true
else else
if HMGT.FilterDisplayEntries then local tickThis = false
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
end
if HMGT.SortDisplayEntries then
HMGT:SortDisplayEntries(entries)
end
if #entries == 0 then if #entries == 0 then
needsFullRefresh = true needsFullRefresh = true
else else
@@ -701,11 +493,8 @@ function Manager:RefreshVisibleVisuals()
if self._displaySignatures[frameKey] ~= newSignature then if self._displaySignatures[frameKey] ~= newSignature then
needsFullRefresh = true needsFullRefresh = true
end end
for _, entry in ipairs(entries) do if tickThis then
if EntryNeedsVisualTicker(entry) then
shouldTick = true shouldTick = true
break
end
end end
end end
end end
@@ -751,12 +540,8 @@ function Manager:UpdateDisplay()
local entries, shouldShow = self:BuildEntriesForTracker(tracker) local entries, shouldShow = self:BuildEntriesForTracker(tracker)
if shouldShow then if shouldShow then
if HMGT.FilterDisplayEntries then local tickThis = false
entries = HMGT:FilterDisplayEntries(tracker, entries) or entries entries, tickThis = HMGT:FinalizeTrackerEntries(tracker, entries, tracker.trackerKey or nil)
end
if HMGT.SortDisplayEntries then
HMGT:SortDisplayEntries(entries)
end
HMGT.TrackerFrame:UpdateFrame(frame, entries, true) HMGT.TrackerFrame:UpdateFrame(frame, entries, true)
frame:Show() frame:Show()
@@ -769,11 +554,8 @@ function Manager:UpdateDisplay()
layoutDirty = true layoutDirty = true
end end
for _, entry in ipairs(entries) do if tickThis then
if EntryNeedsVisualTicker(entry) then
shouldTick = true shouldTick = true
break
end
end end
else else
frame:Hide() frame:Hide()

View File

@@ -142,13 +142,26 @@ local function IsPartyAttachMode(tracker)
end end
local function IsGroupTracker(tracker) 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 end
local TRACKER_TYPE_VALUES = { local function GetTrackerTypeValues()
return HMGT.GetTrackerTypeOptions and HMGT:GetTrackerTypeOptions() or {
normal = L["OPT_TRACKER_TYPE_NORMAL"] or "Normal tracker", normal = L["OPT_TRACKER_TYPE_NORMAL"] or "Normal tracker",
group = L["OPT_TRACKER_TYPE_GROUP"] or "Group-based 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 function GetTrackerVisibilitySummary(tracker)
local parts = {} 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") local display = tracker.showBar and (L["OPT_DISPLAY_BAR"] or "Progress bars") or (L["OPT_DISPLAY_ICON"] or "Icons")
return table.concat({ 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_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_MODE"] or "Mode", modeLabel),
string.format("|cffffd100%s|r: %s", L["OPT_STATUS_DISPLAY"] or "Display", display), string.format("|cffffd100%s|r: %s", L["OPT_STATUS_DISPLAY"] or "Display", display),
@@ -814,7 +827,7 @@ local function BuildGlobalSpellBrowserArgs()
end end
local function BuildTrackerOverviewArgs() local function BuildTrackerOverviewArgs()
return { local args = {
description = { description = {
type = "description", type = "description",
order = 1, order = 1,
@@ -833,24 +846,38 @@ local function BuildTrackerOverviewArgs()
return string.format("%s\n\n%s (%d): %s", body, L["OPT_TRACKERS"] or "Tracker Bars", #trackers, table.concat(names, ", ")) return string.format("%s\n\n%s (%d): %s", body, L["OPT_TRACKERS"] or "Tracker Bars", #trackers, table.concat(names, ", "))
end, 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", type = "execute",
order = 2, order = 2 + index,
width = "full", 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() func = function()
local nextId = HMGT:GetNextTrackerId() local nextId = HMGT:GetNextTrackerId()
local tracker = HMGT:CreateTrackerConfig(nextId, { local tracker = HMGT:BuildTrackerConfigFromPreset(presetKey, nextId)
name = string.format("%s %d", L["OPT_TRACKER"] or "Tracker", nextId),
})
HMGT.db.profile.trackers = HMGT.db.profile.trackers or {} HMGT.db.profile.trackers = HMGT.db.profile.trackers or {}
HMGT.db.profile.trackers[#HMGT.db.profile.trackers + 1] = tracker HMGT.db.profile.trackers[#HMGT.db.profile.trackers + 1] = tracker
TriggerTrackerUpdate(true) TriggerTrackerUpdate(true)
end, end,
},
} }
end end
return args
end
local function BuildTrackerGroup(trackerId, order) local function BuildTrackerGroup(trackerId, order)
local function s() local function s()
return GetTrackerSettings(trackerId) return GetTrackerSettings(trackerId)
@@ -1008,7 +1035,7 @@ local function BuildTrackerGroup(trackerId, order)
width = "full", width = "full",
name = L["OPT_TRACKER_TYPE"] or "Tracker type", 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.", 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() get = function()
local tracker = s() local tracker = s()
return (tracker and tracker.trackerType) or "normal" return (tracker and tracker.trackerType) or "normal"

View 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

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -5,8 +5,8 @@ It combines cooldown tracking, encounter reminders, notes, and map utilities in
## Status ## Status
- Stable version: `1.3` - Stable version: `2.0.0`
- Current build: `2.0-beta` - Current build: `2.1.0-beta`
- SavedVariables: `HailMaryGuildToolsDB` - SavedVariables: `HailMaryGuildToolsDB`
## Main Features ## Main Features
@@ -16,7 +16,6 @@ It combines cooldown tracking, encounter reminders, notes, and map utilities in
- Per-tracker bar and icon layouts - Per-tracker bar and icon layouts
- Aura Expiry for selected buffs and channels - Aura Expiry for selected buffs and channels
- Raid Timeline for encounter-based text reminders and raid cooldown assignments - Raid Timeline for encounter-based text reminders and raid cooldown assignments
- Notes window for raid or personal note management
- Map Overlay with custom world map POIs - Map Overlay with custom world map POIs
- Version mismatch detection inside groups and raids - Version mismatch detection inside groups and raids
- Blizzard AddOn options integration with Ace3-based module configuration - Blizzard AddOn options integration with Ace3-based module configuration
@@ -59,12 +58,24 @@ Provides a dedicated notes window for raid notes, personal notes, and drafts.
Toggles tracker test mode Toggles tracker test mode
- `/hmgt notes` - `/hmgt notes`
Opens the notes window Opens the notes window
- `/hmgt lura`
Opens the L'ura rune helper
- `/hmgt lura circle|x|diamond|t|triangle`
Adds one rune to the next L'ura sequence slot; slot 5 sends the sequence for raid leader/assist
- `Encounter Alerts > L'ura Runes > Rune action bar`
Shows five clickable rune buttons plus a local clear button for building the sequence
- `/hmgt lura reset`
Clears the local L'ura sequence builder
- `/hmgt lura bar`
Toggles the L'ura rune action bar
- `/hmgt debug` - `/hmgt debug`
Opens the developer tools window Opens the debug console
- `/hmgt dev` - `/hmgt dev`
Alias for the developer tools window Alias for the debug console
- `/hmgt status`
Prints a compact addon health check
- `/hmgt version` - `/hmgt version`
Opens the version window when developer tools are enabled Opens the version window when the debug console is enabled
## Installation ## Installation