From 6151b434b1422e86da32c8591a57cd4d0d4f8e19 Mon Sep 17 00:00:00 2001 From: Torsten Brendgen Date: Thu, 16 Apr 2026 16:34:01 +0200 Subject: [PATCH] Enhance version management features and localization for HMGT --- Core/VersionNoticeWindow.lua | 308 ++++++++++++++++++++++++++++++----- HailMaryGuildTools.lua | 86 ++++++++++ Locales/deDE.lua | 12 +- Locales/enUS.lua | 12 +- 4 files changed, 374 insertions(+), 44 deletions(-) diff --git a/Core/VersionNoticeWindow.lua b/Core/VersionNoticeWindow.lua index c6bbf2b..cb86ed4 100644 --- a/Core/VersionNoticeWindow.lua +++ b/Core/VersionNoticeWindow.lua @@ -3,6 +3,212 @@ 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 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 version = HMGT.peerVersions and HMGT.peerVersions[normalized] or nil + local protocol = HMGT.GetPeerProtocolVersion and HMGT:GetPeerProtocolVersion(normalized) or 0 + if version and version ~= "" then + return tostring(version), tonumber(protocol) or 0, true + end + return nil, tonumber(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 "?") + row.versionText:SetTextColor(0.9, 0.9, 0.9, 1) + 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() if self.versionNoticeWindow then @@ -10,21 +216,21 @@ function HMGT:EnsureVersionNoticeWindow() end self.versionNoticeWindowStatus = self.versionNoticeWindowStatus or { - width = 560, - height = 240, + width = 640, + height = 420, } 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, + width = self.versionNoticeWindowStatus.width or 640, + height = self.versionNoticeWindowStatus.height or 420, backgroundTexture = "Interface\\AddOns\\HailMaryGuildTools\\Media\\HailMaryLogo.png", backgroundWidth = 220, backgroundHeight = 120, backgroundOffsetY = -8, - backgroundAlpha = 0.12, + backgroundAlpha = 0.08, strata = "FULLSCREEN_DIALOG", }) if not window then @@ -32,26 +238,64 @@ function HMGT:EnsureVersionNoticeWindow() 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:SetPoint("TOPLEFT", content, "TOPLEFT", 24, -22) + messageText:SetPoint("TOPRIGHT", content, "TOPRIGHT", -24, -22) + messageText:SetJustifyH("LEFT") 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:SetPoint("TOPLEFT", messageText, "BOTTOMLEFT", 0, -8) + detailText:SetPoint("TOPRIGHT", messageText, "BOTTOMRIGHT", 0, -8) + detailText:SetJustifyH("LEFT") detailText:SetTextColor(0.9, 0.9, 0.9, 1) 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 return window end @@ -67,36 +311,16 @@ function HMGT:ShowVersionMismatchPopup(playerName, detail, sourceTag, opts) } 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, + self:RefreshVersionNoticeWindow() + self:DevTrace("Version", "window_show", { + player = playerName, + source = sourceTag, + detail = detail, }) window:Show() window:Raise() diff --git a/HailMaryGuildTools.lua b/HailMaryGuildTools.lua index 0ca96fe..282141d 100644 --- a/HailMaryGuildTools.lua +++ b/HailMaryGuildTools.lua @@ -280,6 +280,7 @@ HMGT.pendingSpellPowerCosts = {} HMGT.demoModeData = {} HMGT.peerVersions = {} HMGT.versionWarnings = {} +HMGT.versionWhisperWarnings = {} HMGT.debugBuffer = {} HMGT.debugBufferMax = 500 HMGT.enabledDebugScopes = { @@ -484,6 +485,79 @@ function HMGT:RememberPeerProtocolVersion(playerName, protocol) self.peerProtocols[normalizedName] = numeric end +local function ParseVersionTokens(version) + local tokens = {} + local text = tostring(version or "") + for number in string.gmatch(text, "(%d+)") do + tokens[#tokens + 1] = tonumber(number) or 0 + end + return tokens +end + +function HMGT:CompareAddonVersions(leftVersion, rightVersion) + local left = ParseVersionTokens(leftVersion) + local right = ParseVersionTokens(rightVersion) + local count = math.max(#left, #right) + for i = 1, count do + local a = tonumber(left[i]) or 0 + local b = tonumber(right[i]) or 0 + if a ~= b then + return (a < b) and -1 or 1 + end + end + + local leftText = tostring(leftVersion or "") + local rightText = tostring(rightVersion or "") + if leftText == rightText then + return 0 + end + if leftText < rightText then + return -1 + end + return 1 +end + +function HMGT:IsPlayerGroupLeader() + if not IsInGroup() and not IsInRaid() then + return true + end + return UnitIsGroupLeader and UnitIsGroupLeader("player") or false +end + +function HMGT:SendOutdatedVersionWhisper(playerName, remoteVersion) + local target = self:NormalizePlayerName(playerName) + local localVersion = tostring(self.ADDON_VERSION or "dev") + local remoteText = tostring(remoteVersion or "?") + if not target or target == "" or not self:IsPlayerGroupLeader() then + return false + end + if self:CompareAddonVersions(localVersion, remoteText) <= 0 then + return false + end + + local warningKey = string.format("%s|%s|%s", tostring(target), remoteText, localVersion) + if self.versionWhisperWarnings[warningKey] then + return false + end + self.versionWhisperWarnings[warningKey] = true + + local message = string.format( + L["VERSION_OUTDATED_WHISPER"] or "Your Hail Mary Guild Tools version is outdated. You have %s, the group leader has %s.", + remoteText, + localVersion + ) + + if C_ChatInfo and type(C_ChatInfo.SendChatMessage) == "function" then + C_ChatInfo.SendChatMessage(message, "WHISPER", nil, target) + elseif type(SendChatMessage) == "function" then + SendChatMessage(message, "WHISPER", nil, target) + else + return false + end + + return true +end + function HMGT:SendReliableAck(target, messageId) if not target or target == "" or not messageId or messageId == "" then return @@ -682,6 +756,12 @@ function HMGT:RegisterPeerVersion(playerName, version, protocol, sourceTag) if not playerName then return end self.peerVersions[playerName] = version self:RememberPeerProtocolVersion(playerName, protocol) + if self.versionNoticeWindow and self.versionNoticeWindow.IsShown and self.versionNoticeWindow:IsShown() and self.RefreshVersionNoticeWindow then + self:RefreshVersionNoticeWindow() + end + if version and version ~= "" then + self:SendOutdatedVersionWhisper(playerName, version) + end local mismatch = false local details = {} if version and version ~= "" and version ~= ADDON_VERSION then @@ -4436,11 +4516,17 @@ function HMGT:OnGroupRosterUpdate() self.remoteSpellStateRevisions[name] = nil self.peerVersions[name] = nil self.versionWarnings[name] = nil + if self.peerProtocols then + self.peerProtocols[name] = nil + end end end local count = 0 for _ in pairs(validPlayers) do count = count + 1 end self:Debug("verbose", "OnGroupRosterUpdate validPlayers=%d", count) + if self.versionNoticeWindow and self.versionNoticeWindow.IsShown and self.versionNoticeWindow:IsShown() and self.RefreshVersionNoticeWindow then + self:RefreshVersionNoticeWindow() + end if HMGT.TrackerManager and HMGT.TrackerManager.InvalidateAnchorLayout then HMGT.TrackerManager:InvalidateAnchorLayout() end diff --git a/Locales/deDE.lua b/Locales/deDE.lua index 9062b0c..db24be8 100644 --- a/Locales/deDE.lua +++ b/Locales/deDE.lua @@ -18,10 +18,20 @@ L["SLASH_HINT"] = "/hmgt – Optionen | /hmgt lock/unlock | /hmgt dem L["VERSION_MISMATCH_CHAT"] = "Versionskonflikt mit %s: %s" L["VERSION_MISMATCH_POPUP"] = "HailMaryGuildTools Konflikt mit %s.\n%s\nQuelle: %s" 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_NO_MISMATCH"] = "In deiner aktuellen Gruppe wurde keine neuere HMGT-Version erkannt." L["VERSION_WINDOW_CURRENT"] = "Aktuelle Version: %s | Protokoll: %s" +L["VERSION_WINDOW_STATUS"] = "HMGT bei %d/%d Spielern erkannt" +L["VERSION_WINDOW_REFRESH"] = "Aktualisieren" +L["VERSION_WINDOW_COLUMN_PLAYER"] = "Spieler" +L["VERSION_WINDOW_COLUMN_VERSION"] = "Version" +L["VERSION_WINDOW_COLUMN_PROTOCOL"] = "Protokoll" +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_DEVTOOLS_ONLY"] = "HMGT: /hmgt version ist nur bei aktivierten Entwicklerwerkzeugen verfuegbar." diff --git a/Locales/enUS.lua b/Locales/enUS.lua index 422735d..89da42b 100644 --- a/Locales/enUS.lua +++ b/Locales/enUS.lua @@ -18,10 +18,20 @@ L["SLASH_HINT"] = "/hmgt – options | /hmgt lock/unlock | /hmgt demo L["VERSION_MISMATCH_CHAT"] = "Version mismatch with %s: %s" L["VERSION_MISMATCH_POPUP"] = "HailMaryGuildTools mismatch with %s.\n%s\nSource: %s" 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_NO_MISMATCH"] = "No newer HMGT version has been detected in your current group." L["VERSION_WINDOW_CURRENT"] = "Current version: %s | Protocol: %s" +L["VERSION_WINDOW_STATUS"] = "Detected HMGT on %d/%d players" +L["VERSION_WINDOW_REFRESH"] = "Refresh" +L["VERSION_WINDOW_COLUMN_PLAYER"] = "Player" +L["VERSION_WINDOW_COLUMN_VERSION"] = "Version" +L["VERSION_WINDOW_COLUMN_PROTOCOL"] = "Protocol" +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_DEVTOOLS_ONLY"] = "HMGT: /hmgt version is only available while developer tools are enabled."