initial commit

This commit is contained in:
Torsten Brendgen
2026-04-10 21:30:31 +02:00
commit fc5a8aa361
108 changed files with 40568 additions and 0 deletions

176
Core/AceWindow.lua Normal file
View File

@@ -0,0 +1,176 @@
local ADDON_NAME = "HailMaryGuildTools"
local HMGT = _G[ADDON_NAME]
if not HMGT then return end
local AceGUI = LibStub("AceGUI-3.0", true)
if not AceGUI then return end
local WindowPrototype = {}
local function ResolveFrame(target)
if not target then
return nil
end
if type(target) == "table" and target.frame then
return target.frame
end
return target
end
function WindowPrototype:GetContent()
return self.content
end
function WindowPrototype:SetTitle(text)
if self.widget and self.widget.SetTitle then
self.widget:SetTitle(tostring(text or ""))
end
end
function WindowPrototype:SetStatusText(text)
if self.widget and self.widget.SetStatusText then
self.widget:SetStatusText(tostring(text or ""))
end
end
function WindowPrototype:Show()
if self.widget and self.widget.Show then
self.widget:Show()
elseif self.frame then
self.frame:Show()
end
end
function WindowPrototype:Hide()
if self.widget and self.widget.Hide then
self.widget:Hide()
elseif self.frame then
self.frame:Hide()
end
end
function WindowPrototype:Raise()
if self.frame and self.frame.Raise then
self.frame:Raise()
end
end
function WindowPrototype:IsShown()
return self.frame and self.frame:IsShown() or false
end
function WindowPrototype:RegisterMinimizeTarget(target)
self.minimizeTargets = self.minimizeTargets or {}
self.minimizeTargets[#self.minimizeTargets + 1] = target
end
function WindowPrototype:SetMinimized(minimized)
minimized = minimized and true or false
if not self.minimizable then
minimized = false
end
self.statusTable = self.statusTable or {}
self.statusTable.minimized = minimized
if minimized then
self.statusTable.restoreHeight = self.statusTable.height or (self.frame and self.frame:GetHeight()) or self.height or 360
end
local targetHeight = minimized
and (self.minimizedHeight or 64)
or (self.statusTable.restoreHeight or self.statusTable.height or self.height or 360)
if self.widget and self.widget.EnableResize then
self.widget:EnableResize(not minimized)
self.widget:SetHeight(targetHeight)
elseif self.frame then
self.frame:SetHeight(targetHeight)
end
if self.minimizeButton and self.minimizeButton.SetText then
self.minimizeButton:SetText(minimized and "+" or "-")
end
for _, target in ipairs(self.minimizeTargets or {}) do
local frame = ResolveFrame(target)
if frame and frame.SetShown then
frame:SetShown(not minimized)
end
end
end
function WindowPrototype:ToggleMinimized()
self:SetMinimized(not (self.statusTable and self.statusTable.minimized))
end
function HMGT:CreateAceWindow(key, options)
self._aceWindows = self._aceWindows or {}
if self._aceWindows[key] then
return self._aceWindows[key]
end
if not AceGUI then
return nil
end
options = options or {}
local statusTable = options.statusTable or {}
local widget = AceGUI:Create("Frame")
widget:SetTitle(tostring(options.title or ""))
widget:SetStatusText(tostring(options.statusText or ""))
widget:SetStatusTable(statusTable)
widget:SetWidth(tonumber(options.width) or 800)
widget:SetHeight(tonumber(options.height) or 360)
widget:EnableResize(options.resizable ~= false)
widget.frame:SetClampedToScreen(true)
widget.frame:SetToplevel(true)
widget.frame:SetFrameStrata(options.strata or "FULLSCREEN_DIALOG")
widget:Hide()
widget:SetCallback("OnClose", function()
widget:Hide()
end)
local window = setmetatable({
key = key,
widget = widget,
frame = widget.frame,
content = widget.content,
statusTable = statusTable,
width = tonumber(options.width) or 800,
height = tonumber(options.height) or 360,
minimizable = options.minimizable == true,
minimizedHeight = tonumber(options.minimizedHeight) or 64,
minimizeTargets = {},
}, { __index = WindowPrototype })
if options.backgroundTexture then
local texture = window.content:CreateTexture(nil, "BACKGROUND")
texture:SetPoint("CENTER", window.content, "CENTER", tonumber(options.backgroundOffsetX) or 0, tonumber(options.backgroundOffsetY) or 0)
texture:SetSize(tonumber(options.backgroundWidth) or 220, tonumber(options.backgroundHeight) or 120)
texture:SetTexture(tostring(options.backgroundTexture))
texture:SetAlpha(tonumber(options.backgroundAlpha) or 0.12)
window.backgroundTexture = texture
end
if window.minimizable then
local minimizeButton = AceGUI:Create("Button")
minimizeButton:SetText((statusTable and statusTable.minimized) and "+" or "-")
minimizeButton:SetWidth(24)
minimizeButton:SetCallback("OnClick", function()
window:ToggleMinimized()
end)
minimizeButton.frame:SetParent(window.frame)
minimizeButton.frame:ClearAllPoints()
minimizeButton.frame:SetPoint("TOPRIGHT", window.frame, "TOPRIGHT", -34, -4)
minimizeButton.frame:SetHeight(20)
minimizeButton.frame:Show()
window.minimizeButton = minimizeButton
end
self._aceWindows[key] = window
if options.onCreate then
options.onCreate(window, window.content, widget)
end
window:SetMinimized(statusTable and statusTable.minimized)
return window
end

486
Core/DebugWindow.lua Normal file
View File

@@ -0,0 +1,486 @@
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

227
Core/DevTools.lua Normal file
View File

@@ -0,0 +1,227 @@
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)
HMGT.devToolsBuffer = HMGT.devToolsBuffer or {}
HMGT.devToolsBufferMax = HMGT.devToolsBufferMax or 300
local DEVTOOLS_SCOPE_ALL = "ALL"
local DEVTOOLS_SCOPE_LABELS = {
System = "System",
Version = "Version",
Options = "Options",
Comm = "Communication",
Tracker = "Tracker",
RaidTimeline = "Raid Timeline",
Notes = "Notes",
}
local DEVTOOLS_LEVELS = {
error = 1,
trace = 2,
}
local function TrimText(value)
return tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "")
end
local function SortKeys(tbl)
local keys = {}
for key in pairs(tbl or {}) do
keys[#keys + 1] = key
end
table.sort(keys, function(a, b)
return tostring(a) < tostring(b)
end)
return keys
end
local function EncodePayloadValue(value, depth)
depth = tonumber(depth) or 0
local valueType = type(value)
if valueType == "nil" then
return "nil"
end
if valueType == "string" then
return value
end
if valueType == "number" or valueType == "boolean" then
return tostring(value)
end
if valueType == "table" then
if depth >= 1 then
return "{...}"
end
local parts = {}
for _, key in ipairs(SortKeys(value)) do
parts[#parts + 1] = string.format("%s=%s", tostring(key), EncodePayloadValue(value[key], depth + 1))
end
return table.concat(parts, ", ")
end
return tostring(value)
end
function HMGT:GetDevToolsSettings()
local profile = self.db and self.db.profile
if not profile then
return {
enabled = false,
level = "error",
scope = DEVTOOLS_SCOPE_ALL,
window = { width = 920, height = 420, minimized = false },
}
end
profile.devTools = type(profile.devTools) == "table" and profile.devTools or {}
local settings = profile.devTools
settings.enabled = settings.enabled == true
if settings.level ~= "error" and settings.level ~= "trace" then
settings.level = "error"
end
if type(settings.scope) ~= "string" or settings.scope == "" then
settings.scope = DEVTOOLS_SCOPE_ALL
end
settings.window = type(settings.window) == "table" and settings.window or {}
settings.window.width = math.max(720, tonumber(settings.window.width) or 920)
settings.window.height = math.max(260, tonumber(settings.window.height) or 420)
settings.window.minimized = settings.window.minimized == true
return settings
end
function HMGT:IsDevToolsEnabled()
return self:GetDevToolsSettings().enabled == true
end
function HMGT:GetDevToolsLevelOptions()
return {
error = L["OPT_DEVTOOLS_LEVEL_ERROR"] or "Errors",
trace = L["OPT_DEVTOOLS_LEVEL_TRACE"] or "Trace",
}
end
function HMGT:GetConfiguredDevToolsLevel()
return self:GetDevToolsSettings().level or "error"
end
function HMGT:ShouldIncludeDevToolsLevel(level)
local configured = self:GetConfiguredDevToolsLevel()
return (DEVTOOLS_LEVELS[tostring(level or "error")] or DEVTOOLS_LEVELS.error)
<= (DEVTOOLS_LEVELS[configured] or DEVTOOLS_LEVELS.error)
end
function HMGT:GetDevToolsScopeOptions()
local values = {
[DEVTOOLS_SCOPE_ALL] = L["OPT_DEVTOOLS_SCOPE_ALL"] or "All scopes",
}
for scope, label in pairs(DEVTOOLS_SCOPE_LABELS) do
values[scope] = label
end
for _, entry in ipairs(self.devToolsBuffer or {}) do
local scope = TrimText(entry and entry.scope or "")
if scope ~= "" and scope ~= DEVTOOLS_SCOPE_ALL then
values[scope] = values[scope] or scope
end
end
return values
end
function HMGT:FormatDevToolsEntry(entry)
local stamp = tostring(entry and entry.stamp or date("%H:%M:%S"))
local level = string.upper(tostring(entry and entry.level or "error"))
local scope = tostring(entry and entry.scope or "System")
local eventName = tostring(entry and entry.event or "")
local payload = TrimText(entry and entry.payload or "")
if payload ~= "" then
return string.format("%s [%s][%s] %s | %s", stamp, level, scope, eventName, payload)
end
return string.format("%s [%s][%s] %s", stamp, level, scope, eventName)
end
function HMGT:GetFilteredDevToolsEntries()
local filtered = {}
local settings = self:GetDevToolsSettings()
for _, entry in ipairs(self.devToolsBuffer or {}) do
local scopeMatches = settings.scope == DEVTOOLS_SCOPE_ALL or settings.scope == tostring(entry.scope or "")
if scopeMatches and self:ShouldIncludeDevToolsLevel(entry.level) then
filtered[#filtered + 1] = entry
end
end
return filtered
end
function HMGT:GetFilteredDevToolsLines()
local lines = {}
for _, entry in ipairs(self:GetFilteredDevToolsEntries()) do
lines[#lines + 1] = self:FormatDevToolsEntry(entry)
end
return lines
end
function HMGT:RecordDevEvent(level, scope, eventName, payload)
if not self:IsDevToolsEnabled() then
return
end
local normalizedLevel = tostring(level or "error")
if normalizedLevel ~= "error" and normalizedLevel ~= "trace" then
normalizedLevel = "trace"
end
if not self:ShouldIncludeDevToolsLevel(normalizedLevel) then
return
end
local normalizedScope = TrimText(scope or "System")
if normalizedScope == "" then
normalizedScope = "System"
end
local entry = {
stamp = date("%H:%M:%S"),
level = normalizedLevel,
scope = normalizedScope,
event = TrimText(eventName or "event"),
payload = EncodePayloadValue(payload, 0),
}
table.insert(self.devToolsBuffer, entry)
if #self.devToolsBuffer > (tonumber(self.devToolsBufferMax) or 300) then
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)
self:RecordDevEvent("error", scope, eventName, payload)
end
function HMGT:DevTrace(scope, eventName, payload)
self:RecordDevEvent("trace", scope, eventName, payload)
end
function HMGT:ClearDevToolsLog()
wipe(self.devToolsBuffer)
if self.devToolsWindow and self.devToolsWindow.editBox then
self.devToolsWindow.editBox:SetText("")
self.devToolsWindow.editBox:SetCursorPosition(0)
end
end
function HMGT:DumpDevToolsLog(maxLines)
if self:IsDevToolsEnabled() and self.OpenDevToolsWindow then
self:OpenDevToolsWindow()
return
end
local lines = tonumber(maxLines) or 40
if lines < 1 then
lines = 1
end
local startIndex = math.max(1, #self.devToolsBuffer - lines + 1)
for i = startIndex, #self.devToolsBuffer do
self:Print(self:FormatDevToolsEntry(self.devToolsBuffer[i]))
end
end

254
Core/DevToolsWindow.lua Normal file
View File

@@ -0,0 +1,254 @@
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)
if not AceGUI then return end
local function GetOrderedLevels()
return { "error", "trace" }
end
local function GetOrderedScopes()
local values = HMGT:GetDevToolsScopeOptions() or {}
local scopes = { "ALL" }
for scope in pairs(values) do
if scope ~= "ALL" then
scopes[#scopes + 1] = scope
end
end
table.sort(scopes, 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 scopes, 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 AdvanceLevel(step)
local levels = GetOrderedLevels()
local current = HMGT:GetConfiguredDevToolsLevel()
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:GetDevToolsSettings().level = levels[nextIndex]
HMGT:RefreshDevToolsWindow()
end
local function AdvanceScope(step)
local scopes = GetOrderedScopes()
local current = HMGT:GetDevToolsSettings().scope 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:GetDevToolsSettings().scope = scopes[nextIndex]
HMGT:RefreshDevToolsWindow()
end
function HMGT:EnsureDevToolsWindow()
if self.devToolsWindow then
return self.devToolsWindow
end
local settings = self:GetDevToolsSettings()
local window = self:CreateAceWindow("devTools", {
title = L["DEVTOOLS_WINDOW_TITLE"] or "HMGT Developer Tools",
statusText = L["DEVTOOLS_WINDOW_HINT"] or "Structured developer events for the current session",
statusTable = settings.window,
width = settings.window.width or 920,
height = settings.window.height or 420,
minimizable = true,
minimizedHeight = 64,
})
if not window then
return nil
end
local content = window:GetContent()
local clearButton = AceGUI:Create("Button")
clearButton:SetText(L["OPT_DEVTOOLS_CLEAR"] or "Clear developer log")
clearButton:SetWidth(140)
clearButton:SetCallback("OnClick", function()
HMGT:ClearDevToolsLog()
end)
clearButton.frame:SetParent(content)
clearButton.frame:ClearAllPoints()
clearButton.frame:SetPoint("TOPRIGHT", content, "TOPRIGHT", 0, -2)
clearButton.frame:Show()
window.clearButton = clearButton
window:RegisterMinimizeTarget(clearButton)
local selectButton = AceGUI:Create("Button")
selectButton:SetText(L["OPT_DEVTOOLS_SELECT_ALL"] or "Select all")
selectButton:SetWidth(120)
selectButton:SetCallback("OnClick", function()
if window.editBox then
window.editBox:SetFocus()
window.editBox:HighlightText(0)
end
end)
selectButton.frame:SetParent(content)
selectButton.frame:ClearAllPoints()
selectButton.frame:SetPoint("TOPRIGHT", clearButton.frame, "TOPLEFT", -6, 0)
selectButton.frame:Show()
window.selectButton = selectButton
window:RegisterMinimizeTarget(selectButton)
local levelFilter = AceGUI:Create("Button")
levelFilter:SetWidth(150)
levelFilter:SetCallback("OnClick", function()
AdvanceLevel(1)
end)
levelFilter.frame:SetParent(content)
levelFilter.frame:ClearAllPoints()
levelFilter.frame:SetPoint("TOPLEFT", content, "TOPLEFT", 0, 0)
levelFilter.frame:Show()
window.levelFilter = levelFilter
window:RegisterMinimizeTarget(levelFilter)
local scopeFilter = AceGUI:Create("Button")
scopeFilter:SetWidth(200)
scopeFilter:SetCallback("OnClick", function()
AdvanceScope(1)
end)
scopeFilter.frame:SetParent(content)
scopeFilter.frame:ClearAllPoints()
scopeFilter.frame:SetPoint("TOPLEFT", levelFilter.frame, "TOPRIGHT", 8, 0)
scopeFilter.frame:Show()
window.scopeFilter = scopeFilter
window:RegisterMinimizeTarget(scopeFilter)
local logWidget = AceGUI:Create("MultiLineEditBox")
logWidget:SetLabel("")
logWidget:DisableButton(true)
logWidget:SetNumLines(20)
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.editBox:SetScript("OnKeyDown", function(selfBox, key)
if IsControlKeyDown() and (key == "A" or key == "a") then
selfBox:HighlightText(0)
end
end)
window.logWidget = logWidget
window.editBox = logWidget.editBox
window:RegisterMinimizeTarget(logWidget)
self.devToolsWindow = window
window:SetMinimized(settings.window.minimized)
return window
end
function HMGT:RefreshDevToolsWindow()
local window = self:EnsureDevToolsWindow()
if not window or not window.editBox or not window.logWidget then
return
end
local levelOptions = self:GetDevToolsLevelOptions()
SetFilterButtonText(window.levelFilter, L["OPT_DEVTOOLS_LEVEL"] or "Capture level", levelOptions[self:GetConfiguredDevToolsLevel()])
local scopeValues = self:GetDevToolsScopeOptions()
local currentScope = self:GetDevToolsSettings().scope or "ALL"
SetFilterButtonText(window.scopeFilter, L["OPT_DEVTOOLS_SCOPE"] or "Scope", scopeValues[currentScope] or currentScope)
local text = table.concat(self:GetFilteredDevToolsLines(), "\n")
window.logWidget:SetText(text)
window.editBox:SetCursorPosition(window.editBox:GetNumLetters())
end
function HMGT:OpenDevToolsWindow()
if not self:IsDevToolsEnabled() then
self:Print(L["OPT_DEVTOOLS_DISABLED"] or "HMGT: developer tools are not enabled.")
return
end
local window = self:EnsureDevToolsWindow()
if not window then
return
end
window:Show()
window:Raise()
self:RefreshDevToolsWindow()
end
function HMGT:ToggleDevToolsWindow()
if not self:IsDevToolsEnabled() then
self:Print(L["OPT_DEVTOOLS_DISABLED"] or "HMGT: developer tools are not enabled.")
return
end
local window = self:EnsureDevToolsWindow()
if not window then
return
end
if window:IsShown() then
window:Hide()
return
end
window:Show()
window:Raise()
self:RefreshDevToolsWindow()
end
function HMGT:UpdateDevToolsWindowVisibility()
local window = self.devToolsWindow
if not window then
return
end
if not self:IsDevToolsEnabled() then
window:Hide()
return
end
if window:IsShown() then
self:RefreshDevToolsWindow()
end
end
function HMGT:RefreshDebugWindow()
self:RefreshDevToolsWindow()
end
function HMGT:UpdateDebugWindowVisibility()
self:UpdateDevToolsWindowVisibility()
end
function HMGT:ClearDebugLog()
self:ClearDevToolsLog()
end
function HMGT:ToggleDebugWindowShortcut()
self:ToggleDevToolsWindow()
end
function HMGT:DumpDebugLog(maxLines)
self:DumpDevToolsLog(maxLines)
end

View File

@@ -0,0 +1,103 @@
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)
function HMGT:EnsureVersionNoticeWindow()
if self.versionNoticeWindow then
return self.versionNoticeWindow
end
self.versionNoticeWindowStatus = self.versionNoticeWindowStatus or {
width = 560,
height = 240,
}
local window = self:CreateAceWindow("versionNotice", {
title = L["VERSION_WINDOW_TITLE"] or "HMGT Version Check",
statusText = "",
statusTable = self.versionNoticeWindowStatus,
width = self.versionNoticeWindowStatus.width or 560,
height = self.versionNoticeWindowStatus.height or 240,
backgroundTexture = "Interface\\AddOns\\HailMaryGuildTools\\Media\\HailMaryLogo.png",
backgroundWidth = 220,
backgroundHeight = 120,
backgroundOffsetY = -8,
backgroundAlpha = 0.12,
strata = "FULLSCREEN_DIALOG",
})
if not window then
return nil
end
local content = window:GetContent()
local messageText = content:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
messageText:SetPoint("TOPLEFT", content, "TOPLEFT", 28, -28)
messageText:SetPoint("TOPRIGHT", content, "TOPRIGHT", -28, -28)
messageText:SetJustifyH("CENTER")
messageText:SetJustifyV("MIDDLE")
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
local detailText = content:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
detailText:SetPoint("TOPLEFT", messageText, "BOTTOMLEFT", 0, -18)
detailText:SetPoint("TOPRIGHT", messageText, "BOTTOMRIGHT", 0, -18)
detailText:SetJustifyH("CENTER")
detailText:SetJustifyV("TOP")
if detailText.SetSpacing then
detailText:SetSpacing(2)
end
detailText:SetTextColor(0.9, 0.9, 0.9, 1)
window.detailText = detailText
self.versionNoticeWindow = window
return window
end
function HMGT:ShowVersionMismatchPopup(playerName, detail, sourceTag, opts)
opts = opts or {}
if playerName or detail or sourceTag then
self.latestVersionMismatch = {
playerName = playerName,
detail = detail,
sourceTag = sourceTag,
}
end
local info = self.latestVersionMismatch or {}
local window = self:EnsureVersionNoticeWindow()
if not window then
return
end
local hasMismatch = info.playerName or info.detail
window:SetTitle(L["VERSION_WINDOW_TITLE"] or "HMGT Version Check")
if hasMismatch then
window.messageText:SetText(L["VERSION_WINDOW_MESSAGE"] or "A new version of Hail Mary Guild Tools is available.")
window.detailText:SetText(string.format(
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:Raise()
end