From ae24696cb553e0c477a90f6f1aaf2a9f63115ce7 Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Sun, 8 Mar 2026 21:16:18 +0200 Subject: [PATCH 01/15] Update Recount for WoW Midnight 12.0.1 API compatibility - TOC Interface bumped to 120001, added IconTexture field - GetSpellInfo migrated to C_Spell.GetSpellInfo with positional return compat - GetMouseFocus replaced with GetMouseFoci compat wrapper (LibDropDown) - ColorPicker opacity API fixed for retail in GUI_Realtime (WOW_RETAIL guard) - IsInScenarioGroup fallback to C_Scenario.IsInScenario - UIFrameFade fallback added for potential removal - Removed dead InterfaceOptionsFrame reference - table.getn() replaced with # operator --- GUI_Config.lua | 8 ++++---- GUI_Detail.lua | 2 +- GUI_Graph.lua | 2 +- GUI_Main.lua | 13 ++++++++++--- GUI_Realtime.lua | 6 ++++-- Recount.lua | 1 - Recount.toc | 5 +++-- Tracker.lua | 11 ++++++++++- deletion.lua | 2 +- libs/LibDropDown-1.0/LibDropdown-1.0.lua | 2 +- 10 files changed, 35 insertions(+), 17 deletions(-) diff --git a/GUI_Config.lua b/GUI_Config.lua index 1f51f65..eaeaef8 100644 --- a/GUI_Config.lua +++ b/GUI_Config.lua @@ -750,7 +750,7 @@ end function me:RefreshStatusBars() local BarTextures = SM:List("statusbar") - local size = table.getn(BarTextures) + local size = #(BarTextures) FauxScrollFrame_Update(me.TextureOptions.ScrollBar, size, 13, 12) local offset = FauxScrollFrame_GetOffset(me.TextureOptions.ScrollBar) @@ -1009,7 +1009,7 @@ function me:CreateTextureSelection(parent) end me:UpdateStatusBars() - if table.getn(BarTextures) <= 13 then + if #(BarTextures) <= 13 then for i = 1, 13 do theFrame.Rows[i]:SetWidth(196) theFrame.Rows[i]:SetPoint("TOP", theFrame, "TOP", 0, -i * 14 - 2) @@ -1071,7 +1071,7 @@ end function me:RefreshFonts() local Fonts = SM:List("font") - local size = table.getn(Fonts) + local size = #(Fonts) FauxScrollFrame_Update(me.FontOptions.ScrollBar, size, 13, 12) local offset = FauxScrollFrame_GetOffset(me.FontOptions.ScrollBar) @@ -1106,7 +1106,7 @@ function me:CreateFontSelection(parent) end me:UpdateFonts() - if table.getn(Fonts) <= 13 then + if #(Fonts) <= 13 then for i = 1, 13 do theFrame.Rows[i]:SetWidth(196) theFrame.Rows[i]:SetPoint("TOP", theFrame, "TOP", 0, -i * 14 - 2) diff --git a/GUI_Detail.lua b/GUI_Detail.lua index a7a1dbd..0fabca2 100644 --- a/GUI_Detail.lua +++ b/GUI_Detail.lua @@ -431,7 +431,7 @@ function me:RefreshDeathDetails() local size if Data then - size = table.getn(Data) + size = #(Data) else size = 0 end diff --git a/GUI_Graph.lua b/GUI_Graph.lua index e35308e..ea50e6a 100644 --- a/GUI_Graph.lua +++ b/GUI_Graph.lua @@ -846,7 +846,7 @@ end function Recount:GraphRefreshCombat() local combat = Recount.db2.CombatTimes - local size = table.getn(combat) + local size = #(combat) FauxScrollFrame_Update(Recount.GraphWindow.ScrollBar2, size, 10, 20) local offset = FauxScrollFrame_GetOffset(Recount.GraphWindow.ScrollBar2) local Rows = Recount.GraphWindow.TimeRows diff --git a/GUI_Main.lua b/GUI_Main.lua index 0942f68..391a56b 100644 --- a/GUI_Main.lua +++ b/GUI_Main.lua @@ -31,7 +31,14 @@ local IsAltKeyDown = IsAltKeyDown local IsControlKeyDown = IsControlKeyDown local IsShiftKeyDown = IsShiftKeyDown local SendChatMessage = SendChatMessage -local UIFrameFade = UIFrameFade +local UIFrameFade = UIFrameFade or function(frame, fadeInfo) + if fadeInfo.mode == "OUT" then + frame:SetAlpha(0) + end + if fadeInfo.finishedFunc then + fadeInfo.finishedFunc(fadeInfo.finishedArg1) + end +end local GameTooltip = GameTooltip local UIParent = UIParent @@ -1170,11 +1177,11 @@ function Recount:RefreshMainWindow(datarefresh) end local RowWidth = MainWindow:GetWidth() - 4 - if table.getn(dispTable) > MainWindow.CurRows and MainWindow_Settings.ShowScrollbar == true then + if #(dispTable) > MainWindow.CurRows and MainWindow_Settings.ShowScrollbar == true then RowWidth = MainWindow:GetWidth() - 23 end - FauxScrollFrame_Update(MainWindow.ScrollBar, table.getn(dispTable), Recount.MainWindow.CurRows, 20) + FauxScrollFrame_Update(MainWindow.ScrollBar, #(dispTable), Recount.MainWindow.CurRows, 20) local offset = FauxScrollFrame_GetOffset(MainWindow.ScrollBar) if type(MainWindow.SpecialTotal) == "function" then diff --git a/GUI_Realtime.lua b/GUI_Realtime.lua index 0dced1e..cdccc0a 100644 --- a/GUI_Realtime.lua +++ b/GUI_Realtime.lua @@ -30,6 +30,8 @@ local UIDropDownMenu_SetAnchor = UIDropDownMenu_SetAnchor local UIParent = UIParent local OpacitySliderFrame = OpacitySliderFrame +local WOW_RETAIL = WOW_PROJECT_ID == WOW_PROJECT_MAINLINE + local me = {} local FreeWindows = {} @@ -145,7 +147,7 @@ local function Color_Change() if not ColorPickerFrame.hasOpacity then TempColor.a = nil else - TempColor.a = OpacitySliderFrame:GetValue() + TempColor.a = WOW_RETAIL and ColorPickerFrame.Content.ColorPicker:GetColorAlpha() or 1.0 - OpacitySliderFrame:GetValue() end Recount.Colors:SetColor(Cur_Branch, Cur_Name, TempColor) @@ -153,7 +155,7 @@ end local function Opacity_Change() local r, g, b = ColorPickerFrame:GetColorRGB() - local a = OpacitySliderFrame:GetValue() + local a = WOW_RETAIL and ColorPickerFrame.Content.ColorPicker:GetColorAlpha() or 1.0 - OpacitySliderFrame:GetValue() TempColor.r = r TempColor.g = g diff --git a/Recount.lua b/Recount.lua index 9a42abe..5ac8c22 100644 --- a/Recount.lua +++ b/Recount.lua @@ -53,7 +53,6 @@ local UnitName = UnitName local CreateFrame = CreateFrame -local InterfaceOptionsFrame = InterfaceOptionsFrame local UIParent = UIParent local RecountTempTooltip = RecountTempTooltip diff --git a/Recount.toc b/Recount.toc index 17b49c9..99fbd9b 100644 --- a/Recount.toc +++ b/Recount.toc @@ -1,5 +1,6 @@ -## Interface: 110107 -## Version: v11.1.7a +## Interface: 120001 +## Version: v12.0.1a +## IconTexture: Interface\Icons\Ability_Warrior_Rampage ## Title: Recount ## Notes: Records Damage and Healing for Graph Based Display. ## Notes-ruRU: Записывает урон и исцеления и отоброжает различные графики. diff --git a/Tracker.lua b/Tracker.lua index 36ce30a..64c61dc 100644 --- a/Tracker.lua +++ b/Tracker.lua @@ -34,7 +34,16 @@ local GetLocale = GetLocale local GetNetStats = GetNetStats local GetNumDeclensionSets = GetNumDeclensionSets local GetNumGroupMembers = GetNumGroupMembers -local GetSpellInfo = GetSpellInfo +local GetSpellInfo = function(spellId) + if C_Spell and C_Spell.GetSpellInfo then + local info = C_Spell.GetSpellInfo(spellId) + if info then + return info.name, nil, info.iconID, info.castTime, info.minRange, info.maxRange, info.spellID, info.originalIconID + end + elseif _G.GetSpellInfo then + return _G.GetSpellInfo(spellId) + end +end local GetTime = GetTime local IsInRaid = IsInRaid local UnitExists = UnitExists diff --git a/deletion.lua b/deletion.lua index fabd21c..5694ae9 100644 --- a/deletion.lua +++ b/deletion.lua @@ -13,7 +13,7 @@ local GetNumPartyMembers = GetNumPartyMembers or GetNumSubgroupMembers local GetNumRaidMembers = GetNumRaidMembers or GetNumGroupMembers local IsInInstance = IsInInstance local IsInRaid = IsInRaid -local IsInScenarioGroup = IsInScenarioGroup +local IsInScenarioGroup = IsInScenarioGroup or function() return C_Scenario and C_Scenario.IsInScenario() end local UnitInRaid = UnitInRaid local UnitIsGhost = UnitIsGhost diff --git a/libs/LibDropDown-1.0/LibDropdown-1.0.lua b/libs/LibDropDown-1.0/LibDropdown-1.0.lua index 27746b8..d1a3f8f 100644 --- a/libs/LibDropDown-1.0/LibDropdown-1.0.lua +++ b/libs/LibDropDown-1.0/LibDropdown-1.0.lua @@ -23,7 +23,7 @@ local wipe = wipe local CreateFrame = CreateFrame local PlaySound = PlaySound local ShowUIPanel = ShowUIPanel -local GetMouseFocus = GetMouseFocus +local GetMouseFocus = GetMouseFocus or function() local foci = GetMouseFoci and GetMouseFoci() or {}; return foci[1] end local UISpecialFrames = UISpecialFrames local ChatFrame1 = ChatFrame1 From 0345e631dcce92c35c10f2dd3791e1dd5730c0d0 Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Sun, 8 Mar 2026 21:21:22 +0200 Subject: [PATCH 02/15] Update ChatThrottleLib v29 to v31 and fix SendChatMessage for Midnight - ChatThrottleLib: hooks C_ChatInfo.SendChatMessage when available (12.0 API move) - ChatThrottleLib: hooks C_BattleNet.SendGameData when available - GUI_Main/GUI_Detail: SendChatMessage alias uses C_ChatInfo.SendChatMessage fallback --- GUI_Detail.lua | 2 +- GUI_Main.lua | 2 +- libs/AceComm-3.0/ChatThrottleLib.lua | 33 +++++++++++++++++++--------- 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/GUI_Detail.lua b/GUI_Detail.lua index 0fabca2..7241c5c 100644 --- a/GUI_Detail.lua +++ b/GUI_Detail.lua @@ -20,7 +20,7 @@ local wipe = wipe local CreateFrame = CreateFrame -local SendChatMessage = SendChatMessage +local SendChatMessage = (C_ChatInfo and C_ChatInfo.SendChatMessage) or SendChatMessage local BNSendWhisper = BNSendWhisper local UIParent = UIParent diff --git a/GUI_Main.lua b/GUI_Main.lua index 391a56b..47e404d 100644 --- a/GUI_Main.lua +++ b/GUI_Main.lua @@ -30,7 +30,7 @@ local GetScreenWidth = GetScreenWidth local IsAltKeyDown = IsAltKeyDown local IsControlKeyDown = IsControlKeyDown local IsShiftKeyDown = IsShiftKeyDown -local SendChatMessage = SendChatMessage +local SendChatMessage = (C_ChatInfo and C_ChatInfo.SendChatMessage) or SendChatMessage local UIFrameFade = UIFrameFade or function(frame, fadeInfo) if fadeInfo.mode == "OUT" then frame:SetAlpha(0) diff --git a/libs/AceComm-3.0/ChatThrottleLib.lua b/libs/AceComm-3.0/ChatThrottleLib.lua index 688d318..6e89a6a 100644 --- a/libs/AceComm-3.0/ChatThrottleLib.lua +++ b/libs/AceComm-3.0/ChatThrottleLib.lua @@ -23,7 +23,7 @@ -- LICENSE: ChatThrottleLib is released into the Public Domain -- -local CTL_VERSION = 29 +local CTL_VERSION = 31 local _G = _G @@ -232,9 +232,15 @@ function ChatThrottleLib:Init() -- Use secure hooks as of v16. Old regular hook support yanked out in v21. self.securelyHooked = true --SendChatMessage - hooksecurefunc("SendChatMessage", function(...) - return ChatThrottleLib.Hook_SendChatMessage(...) - end) + if _G.C_ChatInfo and _G.C_ChatInfo.SendChatMessage then + hooksecurefunc(_G.C_ChatInfo, "SendChatMessage", function(...) + return ChatThrottleLib.Hook_SendChatMessage(...) + end) + else + hooksecurefunc("SendChatMessage", function(...) + return ChatThrottleLib.Hook_SendChatMessage(...) + end) + end --SendAddonMessage hooksecurefunc(_G.C_ChatInfo, "SendAddonMessage", function(...) return ChatThrottleLib.Hook_SendAddonMessage(...) @@ -252,9 +258,15 @@ function ChatThrottleLib:Init() -- v29: Hook BNSendGameData for traffic logging if not self.securelyHookedBNGameData then self.securelyHookedBNGameData = true - hooksecurefunc("BNSendGameData", function(...) - return ChatThrottleLib.Hook_BNSendGameData(...) - end) + if _G.C_BattleNet and _G.C_BattleNet.SendGameData then + hooksecurefunc(_G.C_BattleNet, "SendGameData", function(...) + return ChatThrottleLib.Hook_BNSendGameData(...) + end) + else + hooksecurefunc("BNSendGameData", function(...) + return ChatThrottleLib.Hook_BNSendGameData(...) + end) + end end self.nBypass = 0 @@ -541,7 +553,7 @@ function ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, languag -- Check if there's room in the global available bandwidth gauge to send directly if not self.bQueueing and nSize < self:UpdateAvail() then - local sendResult = PerformSend(_G.SendChatMessage, text, chattype, language, destination) + local sendResult = PerformSend(_G.C_ChatInfo.SendChatMessage or _G.SendChatMessage, text, chattype, language, destination) if not IsThrottledSendResult(sendResult) then local didSend = (sendResult == SendAddonMessageResult.Success) @@ -561,7 +573,7 @@ function ChatThrottleLib:SendChatMessage(prio, prefix, text, chattype, languag -- Message needs to be queued local msg = NewMsg() - msg.f = _G.SendChatMessage + msg.f = _G.C_ChatInfo.SendChatMessage or _G.SendChatMessage msg[1] = text msg[2] = chattype or "SAY" msg[3] = language @@ -642,7 +654,8 @@ function ChatThrottleLib:SendAddonMessageLogged(prio, prefix, text, chattype, ta end local function BNSendGameDataReordered(prefix, text, _, gameAccountID) - return _G.BNSendGameData(gameAccountID, prefix, text) + local bnSendFunc = _G.C_BattleNet and _G.C_BattleNet.SendGameData or _G.BNSendGameData + return bnSendFunc(gameAccountID, prefix, text) end function ChatThrottleLib:BNSendGameData(prio, prefix, text, chattype, gameAccountID, queueName, callbackFn, callbackArg) From 3f25656bccd1af5820cba9ed4dc77472f56dbe59 Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Sun, 8 Mar 2026 21:29:09 +0200 Subject: [PATCH 03/15] Update Ace3 libraries to latest upstream versions - AceConfigDialog-3.0: v87 -> v92 (Settings API migration, removes InterfaceOptions_AddCategory, adds C_SettingsUtil.OpenSettingsPanel compat) - AceConfigRegistry-3.0: v21 -> v22 (relWidth option support) - AceGUIWidget-EditBox: ChatFrameUtil.InsertLink compat for 12.0 - AceGUIWidget-MultiLineEditBox: same ChatFrameUtil migration - AceGUIContainer-TreeGroup: v48 -> v49 (tooltip SetText wrap arg fix) - AceGUIWidget-Keybinding: v26 -> v27 (GamePad button support) - AceGUIWidget-Slider: v23 -> v24 (font loading workaround) Source: https://github.com/WoWUIDev/Ace3 --- .../AceConfigDialog-3.0.lua | 66 +++++++++++-------- .../AceConfigRegistry-3.0.lua | 3 +- .../widgets/AceGUIContainer-TreeGroup.lua | 4 +- .../widgets/AceGUIWidget-EditBox.lua | 6 +- .../widgets/AceGUIWidget-Keybinding.lua | 8 ++- .../widgets/AceGUIWidget-MultiLineEditBox.lua | 6 +- .../widgets/AceGUIWidget-Slider.lua | 3 +- 7 files changed, 63 insertions(+), 33 deletions(-) diff --git a/libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.lua b/libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.lua index 06d1d41..1b6c10e 100644 --- a/libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.lua +++ b/libs/AceConfig-3.0/AceConfigDialog-3.0/AceConfigDialog-3.0.lua @@ -7,7 +7,7 @@ local LibStub = LibStub local gui = LibStub("AceGUI-3.0") local reg = LibStub("AceConfigRegistry-3.0") -local MAJOR, MINOR = "AceConfigDialog-3.0", 87 +local MAJOR, MINOR = "AceConfigDialog-3.0", 92 local AceConfigDialog, oldminor = LibStub:NewLibrary(MAJOR, MINOR) if not AceConfigDialog then return end @@ -517,7 +517,7 @@ local function OptionOnMouseOver(widget, event) if descStyle and descStyle ~= "tooltip" then return end - tooltip:SetText(name, 1, .82, 0, true) + tooltip:SetText(name, 1, .82, 0, 1, true) if opt.type == "multiselect" then tooltip:AddLine(user.text, 0.5, 0.5, 0.8, true) @@ -526,7 +526,7 @@ local function OptionOnMouseOver(widget, event) tooltip:AddLine(desc, 1, 1, 1, true) end if type(usage) == "string" then - tooltip:AddLine("Usage: "..usage, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, true) + tooltip:AddLine(usage, NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b, true) end tooltip:Show() @@ -1083,6 +1083,11 @@ local function InjectInfo(control, options, option, path, rootframe, appName) control:SetCallback("OnRelease", CleanUserData) control:SetCallback("OnLeave", OptionOnMouseLeave) control:SetCallback("OnEnter", OptionOnMouseOver) + + -- forward custom arg data directly + if control.SetCustomData and option.arg then + safecall(control.SetCustomData, control, option.arg) + end end local function CreateControl(userControlType, fallbackControlType) @@ -1436,12 +1441,15 @@ local function FeedOptions(appName, options,container,rootframe,path,group,inlin if control then if control.width ~= "fill" then local width = GetOptionsMemberValue("width",v,options,path,appName) + local relWidth = GetOptionsMemberValue("relWidth",v,options,path,appName) if width == "double" then control:SetWidth(width_multiplier * 2) elseif width == "half" then control:SetWidth(width_multiplier / 2) elseif (type(width) == "number") then control:SetWidth(width_multiplier * width) + elseif width == "relative" and relWidth then + control:SetRelativeWidth(relWidth) elseif width == "full" then control.width = "fill" else @@ -1506,7 +1514,7 @@ local function TreeOnButtonEnter(widget, event, uniquevalue, button) tooltip:SetPoint("LEFT",button,"RIGHT") end - tooltip:SetText(name, 1, .82, 0, true) + tooltip:SetText(name, 1, .82, 0, 1, true) if type(desc) == "string" then tooltip:AddLine(desc, 1, 1, 1, true) @@ -1945,6 +1953,8 @@ else AceConfigDialog.BlizOptions = AceConfigDialog.BlizOptions or {} end +AceConfigDialog.BlizOptionsIDMap = AceConfigDialog.BlizOptionsIDMap or {} + local function FeedToBlizPanel(widget, event) local path = widget:GetUserData("path") AceConfigDialog:Open(widget:GetUserData("appName"), widget, unpack(path or emptyTbl)) @@ -1966,16 +1976,17 @@ end -- has to be a head-level note. -- -- This function returns a reference to the container frame registered with the Interface --- Options. You can use this reference to open the options with the API function --- `InterfaceOptionsFrame_OpenToCategory`. +-- Options, as well as the registered ID. You can use the ID to open the options with +-- the API function `Settings.OpenToCategory`. -- @param appName The application name as given to `:RegisterOptionsTable()` -- @param name A descriptive name to display in the options tree (defaults to appName) -- @param parent The parent to use in the interface options tree. -- @param ... The path in the options table to feed into the interface options panel. -- @return The reference to the frame registered into the Interface Options. --- @return The category ID to pass to Settings.OpenToCategory (or InterfaceOptionsFrame_OpenToCategory) +-- @return The category ID to pass to Settings.OpenToCategory function AceConfigDialog:AddToBlizOptions(appName, name, parent, ...) local BlizOptions = AceConfigDialog.BlizOptions + local BlizOptionsIDMap = AceConfigDialog.BlizOptionsIDMap local key = appName for n = 1, select("#", ...) do @@ -2001,29 +2012,32 @@ function AceConfigDialog:AddToBlizOptions(appName, name, parent, ...) end group:SetCallback("OnShow", FeedToBlizPanel) group:SetCallback("OnHide", ClearBlizPanel) - if Settings and Settings.RegisterCanvasLayoutCategory then - local categoryName = name or appName - if parent then - local category = Settings.GetCategory(parent) - if not category then - error(("The parent category '%s' was not found"):format(parent), 2) - end - local subcategory = Settings.RegisterCanvasLayoutSubcategory(category, group.frame, categoryName) - -- force the generated ID to be used for subcategories, as these can have very simple names like "Profiles" - group:SetName(subcategory.ID, parent) - else - local category = Settings.RegisterCanvasLayoutCategory(group.frame, categoryName) - -- using appName here would be cleaner, but would not be 100% compatible - -- but for top-level categories it should be fine, as these are typically addon names - category.ID = categoryName - group:SetName(categoryName, parent) - Settings.RegisterAddOnCategory(category) + local categoryName = name or appName + if parent then + local parentID = BlizOptionsIDMap[parent] or parent + local category = Settings.GetCategory(parentID) + if not category then + error(("The parent category '%s' was not found"):format(parent), 2) end + local subcategory = Settings.RegisterCanvasLayoutSubcategory(category, group.frame, categoryName) + group:SetName(subcategory.ID, parentID) else - group:SetName(name or appName, parent) - InterfaceOptions_AddCategory(group.frame) + if BlizOptionsIDMap[categoryName] then + error(("%s has already been added to the Blizzard Options Window with the given name: %s"):format(appName, categoryName), 2) + end + + local category = Settings.RegisterCanvasLayoutCategory(group.frame, categoryName) + if not (C_SettingsUtil and C_SettingsUtil.OpenSettingsPanel) then + -- override the ID so the name can be used in Settings.OpenToCategory + -- unfortunately with incoming API changes in 12.0 (and likely classic at some point) this override is no longer possible + category.ID = categoryName + end + group:SetName(category.ID) + BlizOptionsIDMap[categoryName] = category.ID + Settings.RegisterAddOnCategory(category) end + return group.frame, group.frame.name else error(("%s has already been added to the Blizzard Options Window with the given path"):format(appName), 2) diff --git a/libs/AceConfig-3.0/AceConfigRegistry-3.0/AceConfigRegistry-3.0.lua b/libs/AceConfig-3.0/AceConfigRegistry-3.0/AceConfigRegistry-3.0.lua index 7d0d108..72e9c60 100644 --- a/libs/AceConfig-3.0/AceConfigRegistry-3.0/AceConfigRegistry-3.0.lua +++ b/libs/AceConfig-3.0/AceConfigRegistry-3.0/AceConfigRegistry-3.0.lua @@ -11,7 +11,7 @@ -- @release $Id$ local CallbackHandler = LibStub("CallbackHandler-1.0") -local MAJOR, MINOR = "AceConfigRegistry-3.0", 21 +local MAJOR, MINOR = "AceConfigRegistry-3.0", 22 local AceConfigRegistry = LibStub:NewLibrary(MAJOR, MINOR) if not AceConfigRegistry then return end @@ -92,6 +92,7 @@ local basekeys={ func=optmethodfalse, arg={["*"]=true}, width=optstringnumber, + relWidth=optnumber, } local typedkeys={ diff --git a/libs/AceGUI-3.0/widgets/AceGUIContainer-TreeGroup.lua b/libs/AceGUI-3.0/widgets/AceGUIContainer-TreeGroup.lua index 7900937..fef4557 100644 --- a/libs/AceGUI-3.0/widgets/AceGUIContainer-TreeGroup.lua +++ b/libs/AceGUI-3.0/widgets/AceGUIContainer-TreeGroup.lua @@ -2,7 +2,7 @@ TreeGroup Container Container that uses a tree control to switch between groups. -------------------------------------------------------------------------------]] -local Type, Version = "TreeGroup", 48 +local Type, Version = "TreeGroup", 49 local AceGUI = LibStub and LibStub("AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end @@ -206,7 +206,7 @@ local function Button_OnEnter(frame) tooltip:SetOwner(frame, "ANCHOR_NONE") tooltip:ClearAllPoints() tooltip:SetPoint("LEFT",frame,"RIGHT") - tooltip:SetText(frame.text:GetText() or "", 1, .82, 0, true) + tooltip:SetText(frame.text:GetText() or "", 1, .82, 0, 1, true) tooltip:Show() end diff --git a/libs/AceGUI-3.0/widgets/AceGUIWidget-EditBox.lua b/libs/AceGUI-3.0/widgets/AceGUIWidget-EditBox.lua index f2a238b..ae1e969 100644 --- a/libs/AceGUI-3.0/widgets/AceGUIWidget-EditBox.lua +++ b/libs/AceGUI-3.0/widgets/AceGUIWidget-EditBox.lua @@ -19,7 +19,11 @@ Support functions -------------------------------------------------------------------------------]] if not AceGUIEditBoxInsertLink then -- upgradeable hook - hooksecurefunc("ChatEdit_InsertLink", function(...) return _G.AceGUIEditBoxInsertLink(...) end) + if ChatFrameUtil and ChatFrameUtil.InsertLink then + hooksecurefunc(ChatFrameUtil, "InsertLink", function(...) return _G.AceGUIEditBoxInsertLink(...) end) + elseif ChatEdit_InsertLink then + hooksecurefunc("ChatEdit_InsertLink", function(...) return _G.AceGUIEditBoxInsertLink(...) end) + end end function _G.AceGUIEditBoxInsertLink(text) diff --git a/libs/AceGUI-3.0/widgets/AceGUIWidget-Keybinding.lua b/libs/AceGUI-3.0/widgets/AceGUIWidget-Keybinding.lua index 0c779dc..ee5a83b 100644 --- a/libs/AceGUI-3.0/widgets/AceGUIWidget-Keybinding.lua +++ b/libs/AceGUI-3.0/widgets/AceGUIWidget-Keybinding.lua @@ -2,7 +2,7 @@ Keybinding Widget Set Keybindings in the Config UI. -------------------------------------------------------------------------------]] -local Type, Version = "Keybinding", 26 +local Type, Version = "Keybinding", 27 local AceGUI = LibStub and LibStub("AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end @@ -31,12 +31,14 @@ local function Keybinding_OnClick(frame, button) if self.waitingForKey then frame:EnableKeyboard(false) frame:EnableMouseWheel(false) + frame:EnableGamePadButton(false) self.msgframe:Hide() frame:UnlockHighlight() self.waitingForKey = nil else frame:EnableKeyboard(true) frame:EnableMouseWheel(true) + frame:EnableGamePadButton(true) self.msgframe:Show() frame:LockHighlight() self.waitingForKey = true @@ -72,6 +74,7 @@ local function Keybinding_OnKeyDown(frame, key) frame:EnableKeyboard(false) frame:EnableMouseWheel(false) + frame:EnableGamePadButton(false) self.msgframe:Hide() frame:UnlockHighlight() self.waitingForKey = nil @@ -119,6 +122,7 @@ local methods = { self:SetDisabled(false) self.button:EnableKeyboard(false) self.button:EnableMouseWheel(false) + self.button:EnableGamePadButton(false) end, -- ["OnRelease"] = nil, @@ -195,10 +199,12 @@ local function Constructor() button:SetScript("OnKeyDown", Keybinding_OnKeyDown) button:SetScript("OnMouseDown", Keybinding_OnMouseDown) button:SetScript("OnMouseWheel", Keybinding_OnMouseWheel) + button:SetScript("OnGamePadButtonDown", Keybinding_OnKeyDown) button:SetPoint("BOTTOMLEFT") button:SetPoint("BOTTOMRIGHT") button:SetHeight(24) button:EnableKeyboard(false) + button:EnableGamePadButton(false) local text = button:GetFontString() text:SetPoint("LEFT", 7, 0) diff --git a/libs/AceGUI-3.0/widgets/AceGUIWidget-MultiLineEditBox.lua b/libs/AceGUI-3.0/widgets/AceGUIWidget-MultiLineEditBox.lua index f0095b5..3dcbaca 100644 --- a/libs/AceGUI-3.0/widgets/AceGUIWidget-MultiLineEditBox.lua +++ b/libs/AceGUI-3.0/widgets/AceGUIWidget-MultiLineEditBox.lua @@ -16,7 +16,11 @@ Support functions if not AceGUIMultiLineEditBoxInsertLink then -- upgradeable hook - hooksecurefunc("ChatEdit_InsertLink", function(...) return _G.AceGUIMultiLineEditBoxInsertLink(...) end) + if ChatFrameUtil and ChatFrameUtil.InsertLink then + hooksecurefunc(ChatFrameUtil, "InsertLink", function(...) return _G.AceGUIMultiLineEditBoxInsertLink(...) end) + elseif ChatEdit_InsertLink then + hooksecurefunc("ChatEdit_InsertLink", function(...) return _G.AceGUIMultiLineEditBoxInsertLink(...) end) + end end function _G.AceGUIMultiLineEditBoxInsertLink(text) diff --git a/libs/AceGUI-3.0/widgets/AceGUIWidget-Slider.lua b/libs/AceGUI-3.0/widgets/AceGUIWidget-Slider.lua index 483d400..85b2ddb 100644 --- a/libs/AceGUI-3.0/widgets/AceGUIWidget-Slider.lua +++ b/libs/AceGUI-3.0/widgets/AceGUIWidget-Slider.lua @@ -2,7 +2,7 @@ Slider Widget Graphical Slider, like, for Range values. -------------------------------------------------------------------------------]] -local Type, Version = "Slider", 23 +local Type, Version = "Slider", 24 local AceGUI = LibStub and LibStub("AceGUI-3.0", true) if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end @@ -273,6 +273,7 @@ local function Constructor() widget[method] = func end slider.obj, editbox.obj = widget, widget + C_Timer.After(0.3, function() editbox:SetText(" ") UpdateText(widget) end) -- Workaround for font loading issue, making the editboxes blank until the text is changed return AceGUI:RegisterAsWidget(widget) end From 700ebc58f46b69e5a76eb10a98c66b7b563b754a Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Sun, 8 Mar 2026 21:44:47 +0200 Subject: [PATCH 04/15] Fix deprecated FillLocalizedClassList and BNGetFriendInfo APIs - Replace FillLocalizedClassList/LocalizedClassList with LOCALIZED_CLASS_NAMES_MALE global (removed in 11.0.2, replacement available since 10.2.5) - Replace BNGetFriendInfo/BNGetSelectedFriend with C_BattleNet.GetFriendAccountInfo (removed in 8.2.5, replacement available since then) --- GUI_Config.lua | 6 +++++- GUI_Report.lua | 23 ++++++++++++++++------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/GUI_Config.lua b/GUI_Config.lua index eaeaef8..6a40d1f 100644 --- a/GUI_Config.lua +++ b/GUI_Config.lua @@ -35,7 +35,11 @@ local RAID_CLASS_COLORS = RAID_CLASS_COLORS local WOW_RETAIL = WOW_PROJECT_ID == WOW_PROJECT_MAINLINE local WOW_PANDA_CLASSIC = WOW_PROJECT_ID == WOW_PROJECT_MISTS_CLASSIC or WOW_PROJECT_ID == WOW_PROJECT_CATACLYSM_CLASSIC or WOW_PROJECT_ID == WOW_PROJECT_WRATH_CLASSIC or WOW_PROJECT_ID == WOW_PROJECT_BURNING_CRUSADE_CLASSIC -if FillLocalizedClassList then +if LOCALIZED_CLASS_NAMES_MALE then + for k, v in pairs(LOCALIZED_CLASS_NAMES_MALE) do + BC[k] = v + end +elseif FillLocalizedClassList then FillLocalizedClassList(BC, false) -- We are sexist here but not much of a choice, when there is no neutral elseif LocalizedClassList then BC = LocalizedClassList(false) diff --git a/GUI_Report.lua b/GUI_Report.lua index d200924..9826c48 100644 --- a/GUI_Report.lua +++ b/GUI_Report.lua @@ -13,9 +13,7 @@ local ipairs = ipairs local table = table local type = type -local BNGetFriendInfo = BNGetFriendInfo local BNGetNumFriends = BNGetNumFriends -local BNGetSelectedFriend = BNGetSelectedFriend local GetChannelList = GetChannelList local GetNumGroupMembers = GetNumGroupMembers local GetNumPartyMembers = GetNumPartyMembers or GetNumSubgroupMembers @@ -211,7 +209,6 @@ end function me:SendReport() local Num, Loc1, Loc2 - local presenceName, battleTag, isBattleTagPresence, toonName, toonID, client, isOnline, totalBNet Num = me.ReportWindow.slider:GetValue() @@ -223,10 +220,22 @@ function me:SendReport() end if Loc1 == "REALID" then - totalBNet = BNGetNumFriends() - if (BNGetSelectedFriend() > 0) and (totalBNet > 0) then - Loc2, presenceName, battleTag, isBattleTagPresence, toonName, toonID, client, isOnline = BNGetFriendInfo(BNGetSelectedFriend()) - if not isOnline then + local totalBNet = BNGetNumFriends() + if C_BattleNet and C_BattleNet.GetFriendAccountInfo and totalBNet > 0 then + -- Find first online BNet friend to send to + local foundOnline = false + for i = 1, totalBNet do + local accountInfo = C_BattleNet.GetFriendAccountInfo(i) + if accountInfo then + local gameAccountInfo = accountInfo.gameAccountInfo + if gameAccountInfo and gameAccountInfo.isOnline then + Loc2 = accountInfo.bnetAccountID + foundOnline = true + break + end + end + end + if not foundOnline then Recount:Print("No online RealID/Battle Tag Selected") return end From a0177e865d53ad287b0a66e592e4369486317ab8 Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Sun, 8 Mar 2026 22:38:38 +0200 Subject: [PATCH 05/15] Migrate combat tracking from COMBAT_LOG_EVENT_UNFILTERED to C_DamageMeter API WoW 12.0 (Midnight) blocks addons from registering COMBAT_LOG_EVENT_UNFILTERED. This adds Tracker_DamageMeter.lua which uses Blizzard's server-side C_DamageMeter API for damage/healing/absorb tracking. Handles secret values during combat by using synthetic ordering for real-time bar display, with actual values populated when combat ends. Includes spell breakdown support for detail views. --- PROGRESS.md | 68 +++++ Recount.lua | 13 +- Recount.toc | 1 + Tracker_DamageMeter.lua | 564 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 644 insertions(+), 2 deletions(-) create mode 100644 PROGRESS.md create mode 100644 Tracker_DamageMeter.lua diff --git a/PROGRESS.md b/PROGRESS.md new file mode 100644 index 0000000..86ca144 --- /dev/null +++ b/PROGRESS.md @@ -0,0 +1,68 @@ +# Recount Midnight (12.0.1) Migration Progress + +## Status: IN PROGRESS + +## Completed +- [x] TOC version bump (120001, IconTexture) +- [x] GetSpellInfo -> C_Spell.GetSpellInfo compat wrapper +- [x] SendChatMessage -> C_ChatInfo.SendChatMessage fallback +- [x] GetMouseFocus -> GetMouseFoci compat +- [x] UIFrameFade fallback +- [x] ColorPickerFrame retail compat (GUI_Realtime.lua) +- [x] IsInScenarioGroup fallback (deletion.lua) +- [x] table.getn -> # operator +- [x] ChatThrottleLib v29 -> v31 +- [x] Ace3 libraries updated to latest upstream +- [x] FillLocalizedClassList -> LOCALIZED_CLASS_NAMES_MALE +- [x] BNGetFriendInfo -> C_BattleNet.GetFriendAccountInfo + +## Current: C_DamageMeter Migration (CRITICAL) + +### Problem +WoW 12.0 blocks addons from registering `COMBAT_LOG_EVENT_UNFILTERED`. +Both Details and Skada have migrated to Blizzard's server-side `C_DamageMeter` API. + +### Plan +1. [x] Create `Tracker_DamageMeter.lua` - new parser using C_DamageMeter API +2. [x] Modify `Recount.lua` - conditionally use new parser on 12.0+ +3. [x] Modify `Recount.toc` - add new file before Tracker.lua +4. [x] Handle secret values (issecretvalue) during active combat +5. [x] Disable/hide modes that can't be populated by C_DamageMeter + +### What C_DamageMeter CAN provide (Recount modes that WILL work): +- Damage Done / DPS (type 0/1) +- Healing Done / HPS (type 2/3) +- Absorbs (type 4) +- Damage Taken (type 7) +- Interrupts (type 5) +- Dispels (type 6) +- Deaths (type 9) +- Per-spell breakdowns for all above +- Duration/active time from session durationSeconds + +### What C_DamageMeter CANNOT provide (modes that will be DISABLED): +- Friendly Fire (no equivalent type) +- Overhealing (no equivalent type) +- DOT Uptime / HOT Uptime (no aura tracking) +- CC Breaks (no equivalent) +- Power Gains (mana/energy/rage/runic power) +- Per-hit stats (crit/miss/dodge/parry breakdown) +- Target breakdowns (who damaged whom) +- Element/school breakdowns +- Resurrections + +### Key API Details +- Session types: 0=Overall, 1=Current, 2=Expired +- Damage types: 0=DamageDone, 1=DPS, 2=HealingDone, 3=HPS, 4=Absorbs, 5=Interrupts, 6=Dispels, 7=DamageTaken, 8=AvoidableDamageTaken, 9=Deaths +- Events: DAMAGE_METER_COMBAT_SESSION_UPDATED, DAMAGE_METER_CURRENT_SESSION_UPDATED, DAMAGE_METER_RESET +- Secret values: data is opaque during combat, use issecretvalue() to check +- Combat detection: PLAYER_REGEN_DISABLED/ENABLED (same as before) + +## Testing +- [ ] Addon loads without errors +- [ ] Main window shows damage data after combat +- [ ] Per-spell breakdown works in detail view +- [ ] Fight segments rotate correctly +- [ ] Reset data works +- [ ] Modes that have data display correctly +- [ ] Modes without data are hidden/disabled gracefully diff --git a/Recount.lua b/Recount.lua index 5ac8c22..8b007b1 100644 --- a/Recount.lua +++ b/Recount.lua @@ -1601,6 +1601,9 @@ function Recount:PutInCombat() end function Recount:CheckCombat(Time) + if Recount.UseDamageMeter then + return -- Combat end is handled by Tracker_DamageMeter.lua via PLAYER_REGEN_ENABLED + end if Recount:CheckPartyCombatWithPets() then if Recount.db.profile.EnableSync then Recount:CheckVisible() @@ -1826,8 +1829,14 @@ function Recount:OnEnable() Recount:ScheduleTimer("InitPartyBasedDeletion", 2) -- Elsia: Wait 2 seconds before enabling auto-delete to prevent startup popups. --end -- Elsia: This is obsolete due to deletion code also handling visibility and solo collection checks. -- Parser Events - Recount.events:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") - Recount.events:RegisterEvent("INSTANCE_ENCOUNTER_ENGAGE_UNIT") + if Recount.UseDamageMeter then + -- WoW 12.0+: Use C_DamageMeter API instead of COMBAT_LOG_EVENT_UNFILTERED + Recount:InitDamageMeterTracker() + else + -- Pre-12.0: Use traditional CLEU parsing + Recount.events:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED") + Recount.events:RegisterEvent("INSTANCE_ENCOUNTER_ENGAGE_UNIT") + end if RecountDeathTrack then RecountDeathTrack:SetFight(Recount.db.profile.CurDataSet) end diff --git a/Recount.toc b/Recount.toc index 99fbd9b..0c1299b 100644 --- a/Recount.toc +++ b/Recount.toc @@ -38,6 +38,7 @@ TrackerModules\TrackerModule_Interrupts.lua TrackerModules\TrackerModule_Resurrection.lua TrackerModules\TrackerModule_CCBreakers.lua TrackerModules\TrackerModule_PowerGains.lua +Tracker_DamageMeter.lua Tracker.lua roster.lua LazySync.lua diff --git a/Tracker_DamageMeter.lua b/Tracker_DamageMeter.lua new file mode 100644 index 0000000..1f22309 --- /dev/null +++ b/Tracker_DamageMeter.lua @@ -0,0 +1,564 @@ +-- Tracker_DamageMeter.lua +-- C_DamageMeter-based parser for WoW 12.0+ (Midnight) +-- Replaces COMBAT_LOG_EVENT_UNFILTERED tracking with Blizzard's server-side damage meter API + +local Recount = _G.Recount + +-- Only load on Midnight (12.0+) +if not C_DamageMeter then + return +end + +local revision = tonumber(string.sub("$Revision: 1607 $", 12, -3)) +if Recount.Version < revision then + Recount.Version = revision +end + +local pairs = pairs +local ipairs = ipairs +local GetTime = GetTime +local UnitName = UnitName +local UnitClass = UnitClass +local UnitLevel = UnitLevel +local date = date +local issecretvalue = issecretvalue + +-- Mark that we're using the new parser +Recount.UseDamageMeter = true + +local function DP(msg) + print("|cFF00FF00[Recount DM]|r " .. tostring(msg)) +end + +-- DamageMeterType enum values +local DM_DamageDone = 0 +local DM_HealingDone = 2 +local DM_Absorbs = 4 +local DM_Interrupts = 5 +local DM_Dispels = 6 +local DM_DamageTaken = 7 +local DM_Deaths = 9 + +-- SessionType enum +local DM_Overall = 0 +local DM_Current = 1 + +local dmFrame = CreateFrame("Frame") +Recount.dmFrame = dmFrame + +local dbCombatants + +-- Track state +local combatSessionID = nil -- The actual combat session ID (non-zero) +local updateTicker = nil + +-- Safe value access for secret values +local function SafeNumber(val) + if val == nil then return 0 end + if issecretvalue and issecretvalue(val) then return 0 end + return tonumber(val) or 0 +end + +-- Get a numeric value for display/sorting, handling secret values +-- For secret values: uses order-based synthetic value since arithmetic is blocked +-- orderIndex: 1-based index from the sorted combatSources array (1 = highest) +local function GetDisplayNumber(val, orderIndex) + if val == nil then return 0 end + if not (issecretvalue and issecretvalue(val)) then + return tonumber(val) or 0 + end + -- Value is secret - use synthetic value based on sort order + -- API returns sources sorted highest-first, so index 1 = top + return math.max(1, 1000 - (orderIndex or 1)) +end + +local function SafeString(val) + if val == nil then return nil end + if issecretvalue and issecretvalue(val) then return nil end + return tostring(val) +end + +local function IsSecret(val) + return val ~= nil and issecretvalue and issecretvalue(val) +end + +local function GetEnClass(classFilename) + if not classFilename or classFilename == "" then return "UNKNOWN" end + if issecretvalue and issecretvalue(classFilename) then return "UNKNOWN" end + return classFilename:upper() +end + +-- Build a roster lookup table for name resolution during combat +local rosterCache = {} +local rosterCacheTime = 0 + +local function RefreshRosterCache() + local now = GetTime() + if now - rosterCacheTime < 2 then return end -- refresh at most every 2 sec + rosterCacheTime = now + wipe(rosterCache) + + -- Add self + local playerName = UnitName("player") + if playerName then + local _, playerClass = UnitClass("player") + rosterCache[playerName] = { name = playerName, class = playerClass } + end + + -- Add group/raid members + local numGroup = GetNumGroupMembers and GetNumGroupMembers() or 0 + local prefix = IsInRaid and IsInRaid() and "raid" or "party" + for i = 1, numGroup do + local unit = prefix .. i + local uName = UnitName(unit) + if uName then + local _, uClass = UnitClass(unit) + rosterCache[uName] = { name = uName, class = uClass } + end + end +end + +-- Resolve a combatant name from source, handling secret values +local function ResolveName(source, orderIndex) + -- Try direct access first (non-secret) + local name = SafeString(source.name) + if name then return name end + + -- Name is secret - use known info to identify the player + if source.isLocalPlayer then + return UnitName("player") + end + + -- Try roster matching by order (best effort during combat) + RefreshRosterCache() + local rosterNames = {} + for rName in pairs(rosterCache) do + rosterNames[#rosterNames + 1] = rName + end + table.sort(rosterNames) + if orderIndex and rosterNames[orderIndex] then + return rosterNames[orderIndex] + end + + return nil +end + +-- Find or create a combatant from C_DamageMeter source data +local function GetOrCreateCombatant(source, orderIndex) + if not source then return nil end + + local name = ResolveName(source, orderIndex) + if not name then return nil end + + local guid = SafeString(source.sourceGUID) + local classFilename = GetEnClass(source.classFilename) + + if dbCombatants[name] then + local who = dbCombatants[name] + if guid and not who.GUID then + who.GUID = guid + end + return who + end + + local combatant = {} + combatant.Name = name + combatant.GUID = guid + combatant.Owner = false + combatant.enClass = classFilename + combatant.level = UnitLevel("player") or 1 + + if source.isLocalPlayer then + combatant.type = "Self" + local _ + _, combatant.enClass = UnitClass("player") + combatant.level = UnitLevel("player") + elseif classFilename ~= "UNKNOWN" and classFilename ~= "MOB" and classFilename ~= "" then + combatant.type = "Grouped" + else + combatant.type = "Ungrouped" + end + + combatant.Fights = {} + combatant.Fights.OverallData = {} + Recount:InitFightData(combatant.Fights.OverallData) + combatant.Fights.CurrentFightData = {} + Recount:InitFightData(combatant.Fights.CurrentFightData) + + combatant.TimeWindows = {} + combatant.TimeLast = {} + combatant.LastEvents = {} + combatant.LastEventTimes = {} + combatant.LastEventType = {} + combatant.LastEventIncoming = {} + combatant.LastEventHealth = {} + combatant.LastEventHealthMax = {} + combatant.NextEventNum = 1 + combatant.Pet = {} + + dbCombatants[name] = combatant + DP("Created combatant: " .. name .. " type=" .. combatant.type .. " class=" .. combatant.enClass) + return combatant +end + +-- Fetch a session - try combat session ID, then Current type, then Overall type +local function GetSession(dmType) + local ok, session + + -- Try by specific combat session ID first + if combatSessionID then + ok, session = pcall(C_DamageMeter.GetCombatSessionFromID, combatSessionID, dmType) + if ok and session and session.combatSources and #session.combatSources > 0 then + return session + end + end + + -- Try Current session type + ok, session = pcall(C_DamageMeter.GetCombatSessionFromType, DM_Current, dmType) + if ok and session and session.combatSources and #session.combatSources > 0 then + return session + end + + -- Try Overall session type + ok, session = pcall(C_DamageMeter.GetCombatSessionFromType, DM_Overall, dmType) + if ok and session and session.combatSources and #session.combatSources > 0 then + return session + end + + return nil +end + +-- Get spell source data for a combatant +local function GetSpellSource(dmType, guid) + local ok, sourceData + + if combatSessionID then + ok, sourceData = pcall(C_DamageMeter.GetCombatSessionSourceFromID, combatSessionID, dmType, guid) + if ok and sourceData and sourceData.combatSpells and #sourceData.combatSpells > 0 then + return sourceData + end + end + + ok, sourceData = pcall(C_DamageMeter.GetCombatSessionSourceFromType, DM_Current, dmType, guid) + if ok and sourceData and sourceData.combatSpells and #sourceData.combatSpells > 0 then + return sourceData + end + + ok, sourceData = pcall(C_DamageMeter.GetCombatSessionSourceFromType, DM_Overall, dmType, guid) + if ok and sourceData and sourceData.combatSpells and #sourceData.combatSpells > 0 then + return sourceData + end + + return nil +end + +-- Add spell breakdown data for a combatant +local function AddSpellBreakdown(who, dmType, datatypeAttacks) + if not who then return end + + local guid = who.GUID + if not guid or IsSecret(guid) then + DP(" No GUID for spell lookup: " .. (who.Name or "?")) + return + end + + local sourceData = GetSpellSource(dmType, guid) + if not sourceData or not sourceData.combatSpells then + DP(" No spell source data for " .. (who.Name or "?") .. " dmType=" .. dmType .. " guid=" .. guid) + return + end + + local spellCount = 0 + for _, spell in ipairs(sourceData.combatSpells) do + local spellID = spell.spellID + local amount = SafeNumber(spell.totalAmount) + if spellID and amount > 0 then + local spellName + if C_Spell and C_Spell.GetSpellInfo then + local info = C_Spell.GetSpellInfo(spellID) + spellName = info and info.name + end + spellName = spellName or ("Spell " .. spellID) + + if datatypeAttacks and who.Fights then + for _, fightKey in ipairs({"CurrentFightData", "OverallData"}) do + local fightData = who.Fights[fightKey] + if fightData then + fightData[datatypeAttacks] = fightData[datatypeAttacks] or {} + fightData[datatypeAttacks][spellName] = { + count = 1, + amount = amount, + Details = { + ["Hit"] = { + count = 1, + amount = amount, + max = amount, + min = amount, + } + } + } + end + end + end + spellCount = spellCount + 1 + end + end + DP(" Spells for " .. (who.Name or "?") .. " dmType=" .. dmType .. ": " .. spellCount) +end + +-- Snapshot current DM data into CurrentFightData (replace, not accumulate) +local function SnapshotSession(verbose) + local sessionDuration = 0 + local foundAny = false + + local session = GetSession(DM_DamageDone) + if not session or not session.combatSources then + if verbose then DP(" No DamageDone session found") end + return false + end + + if verbose then DP(" DamageDone: " .. #session.combatSources .. " sources") end + + if session.durationSeconds then + sessionDuration = GetDisplayNumber(session.durationSeconds, 1) + end + + local hasSecrets = false + for i, source in ipairs(session.combatSources) do + -- Check if values are secret (API sorts sources highest-first) + local isSecretAmount = IsSecret(source.totalAmount) + if isSecretAmount then hasSecrets = true end + + if verbose and i == 1 then + DP(" First source secret check: name=" .. tostring(IsSecret(source.name)) .. " amount=" .. tostring(isSecretAmount)) + end + + local who = GetOrCreateCombatant(source, i) + if who then + local amount = GetDisplayNumber(source.totalAmount, i) + if amount > 0 then + who.Fights.CurrentFightData.Damage = amount + who.LastFightIn = Recount.db2.FightNum + foundAny = true + + if source.isLocalPlayer and Recount.FightingWho == "" then + Recount.FightingWho = "Combat" + end + end + end + end + + if verbose then + DP(" hasSecrets=" .. tostring(hasSecrets) .. " foundAny=" .. tostring(foundAny)) + end + + if sessionDuration > 0 then + for _, who in pairs(dbCombatants) do + if who.LastFightIn == Recount.db2.FightNum then + who.Fights.CurrentFightData.ActiveTime = sessionDuration + who.Fights.CurrentFightData.TimeDamage = sessionDuration + who.Fights.CurrentFightData.TimeHeal = sessionDuration + end + end + end + + local function ProcessType(dmType, dataField) + local s = GetSession(dmType) + if s and s.combatSources then + for idx, source in ipairs(s.combatSources) do + local who = GetOrCreateCombatant(source, idx) + if who then + local amount = GetDisplayNumber(source.totalAmount, idx) + if amount > 0 then + who.Fights.CurrentFightData[dataField] = amount + who.LastFightIn = Recount.db2.FightNum + foundAny = true + end + end + end + end + end + + ProcessType(DM_HealingDone, "Healing") + ProcessType(DM_Absorbs, "Absorbs") + ProcessType(DM_DamageTaken, "DamageTaken") + ProcessType(DM_Interrupts, "Interrupts") + ProcessType(DM_Dispels, "Dispels") + ProcessType(DM_Deaths, "DeathCount") + + Recount.NewData = true + return foundAny +end + +-- Full parse: snapshot + copy to OverallData + spell breakdowns +local function ParseSessionFull() + DP("ParseSessionFull: combatSessionID=" .. tostring(combatSessionID)) + local foundAny = SnapshotSession(true) + + if foundAny then + -- Copy CurrentFightData to OverallData + for _, who in pairs(dbCombatants) do + if who.LastFightIn == Recount.db2.FightNum and who.Fights and who.Fights.CurrentFightData then + who.Fights.OverallData = who.Fights.OverallData or {} + local cur = who.Fights.CurrentFightData + local ovr = who.Fights.OverallData + for _, field in ipairs({"Damage", "Healing", "Absorbs", "DamageTaken", "Interrupts", "Dispels", "DeathCount", "ActiveTime", "TimeDamage", "TimeHeal"}) do + if cur[field] and cur[field] > 0 then + ovr[field] = (ovr[field] or 0) + cur[field] + end + end + end + end + + -- Spell breakdowns + for _, who in pairs(dbCombatants) do + if who.LastFightIn == Recount.db2.FightNum then + AddSpellBreakdown(who, DM_DamageDone, "Attacks") + AddSpellBreakdown(who, DM_HealingDone, "Heals") + end + end + end + + DP("ParseSessionFull: foundAny=" .. tostring(foundAny)) + return foundAny +end + +-- Real-time update during combat +local tickCount = 0 +local function UpdateTick() + if not Recount.InCombat then + if updateTicker then + updateTicker:Cancel() + updateTicker = nil + end + return + end + + tickCount = tickCount + 1 + local found = SnapshotSession(tickCount <= 2) -- verbose on first two ticks + if tickCount <= 3 then + DP("UpdateTick #" .. tickCount .. ": found=" .. tostring(found) .. " sessionID=" .. tostring(combatSessionID)) + end + + if found then + if Recount.FightingWho == "" then + Recount.FightingWho = "Combat" + end + Recount.NewData = true + if Recount.RefreshMainWindow then + Recount:RefreshMainWindow() + end + end +end + +local function StartUpdateTicker() + if updateTicker then + updateTicker:Cancel() + end + tickCount = 0 + DP("Starting update ticker") + updateTicker = C_Timer.NewTicker(0.5, UpdateTick) +end + +local function StopUpdateTicker() + if updateTicker then + updateTicker:Cancel() + updateTicker = nil + end +end + +-- Called when combat ends +local function OnCombatEnd() + StopUpdateTicker() + DP("OnCombatEnd: InCombat=" .. tostring(Recount.InCombat) .. " sessionID=" .. tostring(combatSessionID)) + + C_Timer.After(0.5, function() + DP("End timer fired: InCombat=" .. tostring(Recount.InCombat)) + + -- Reset CurrentFightData before final parse + for _, who in pairs(dbCombatants) do + if who.LastFightIn == Recount.db2.FightNum and who.Fights and who.Fights.CurrentFightData then + for _, f in ipairs({"Damage", "Healing", "Absorbs", "DamageTaken", "Interrupts", "Dispels", "DeathCount", "ActiveTime", "TimeDamage", "TimeHeal"}) do + who.Fights.CurrentFightData[f] = 0 + end + end + end + + ParseSessionFull() + + if Recount.FightingWho == "" then + Recount.FightingWho = "Combat" + end + + DP("Calling LeaveCombat, FightingWho=" .. Recount.FightingWho) + Recount:LeaveCombat(GetTime()) + Recount:FullRefreshMainWindow() + + -- Keep combatSessionID for potential post-combat queries, but it will be reset on next combat + end) +end + +-- Event handlers +local function OnEvent(self, event, ...) + if event == "PLAYER_REGEN_DISABLED" then + if not Recount.db.profile.GlobalDataCollect or not Recount.CurrentDataCollect then + DP("REGEN_DISABLED but data collect is off: Global=" .. tostring(Recount.db.profile.GlobalDataCollect) .. " Current=" .. tostring(Recount.CurrentDataCollect)) + return + end + DP("PLAYER_REGEN_DISABLED - combat start") + combatSessionID = nil + Recount:PutInCombat() + StartUpdateTicker() + + elseif event == "PLAYER_REGEN_ENABLED" then + DP("PLAYER_REGEN_ENABLED - InCombat=" .. tostring(Recount.InCombat)) + if not Recount.InCombat then return end + OnCombatEnd() + + elseif event == "DAMAGE_METER_COMBAT_SESSION_UPDATED" then + local dmType, sessionId = ... + -- Only track non-zero session IDs (0 is the "overall" session) + if sessionId and sessionId > 0 then + combatSessionID = sessionId + end + + elseif event == "DAMAGE_METER_CURRENT_SESSION_UPDATED" then + Recount.NewData = true + + elseif event == "DAMAGE_METER_RESET" then + DP("DAMAGE_METER_RESET") + if not Recount._resettingData then + Recount:ResetData() + end + + elseif event == "INSTANCE_ENCOUNTER_ENGAGE_UNIT" then + Recount:BossFound() + end +end + +dmFrame:SetScript("OnEvent", OnEvent) + +function Recount:InitDamageMeterTracker() + dbCombatants = Recount.db2.combatants + DP("InitDamageMeterTracker called, available=" .. tostring(C_DamageMeter.IsDamageMeterAvailable())) + + dmFrame:RegisterEvent("PLAYER_REGEN_DISABLED") + dmFrame:RegisterEvent("PLAYER_REGEN_ENABLED") + dmFrame:RegisterEvent("DAMAGE_METER_COMBAT_SESSION_UPDATED") + dmFrame:RegisterEvent("DAMAGE_METER_CURRENT_SESSION_UPDATED") + dmFrame:RegisterEvent("DAMAGE_METER_RESET") + dmFrame:RegisterEvent("INSTANCE_ENCOUNTER_ENGAGE_UNIT") +end + +local originalResetDataUnsafe = Recount.ResetDataUnsafe +function Recount:ResetDataUnsafe() + DP("ResetDataUnsafe called") + Recount._resettingData = true + if originalResetDataUnsafe then + originalResetDataUnsafe(self) + end + Recount._resettingData = false + dbCombatants = Recount.db2.combatants + combatSessionID = nil + DP("ResetDataUnsafe complete") +end From 789aca33943ab2a2636e241e28479cc32a0e4575 Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Mon, 9 Mar 2026 00:42:21 +0200 Subject: [PATCH 06/15] Fix realtime Midnight damage meter display --- GUI_Main.lua | 27 +++- Recount.lua | 4 + Recount_Modes.lua | 10 +- Tracker_DamageMeter.lua | 323 +++++++++++++++++++++++++++++++++++----- 4 files changed, 325 insertions(+), 39 deletions(-) diff --git a/GUI_Main.lua b/GUI_Main.lua index 47e404d..7cc657b 100644 --- a/GUI_Main.lua +++ b/GUI_Main.lua @@ -480,21 +480,30 @@ end function me:FixRow(i) local row = Recount.MainWindow.Rows[i] - local MaxNameWidth = row:GetWidth() - row.RightText:GetStringWidth() - 4 + local MaxNameWidth = row:GetWidth() - 4 + local ok, rightTextWidth = pcall(row.RightText.GetStringWidth, row.RightText) + if ok and type(rightTextWidth) == "number" and not (issecretvalue and issecretvalue(rightTextWidth)) then + MaxNameWidth = MaxNameWidth - rightTextWidth + end if MaxNameWidth < 16 then MaxNameWidth = 16 end - local LText = row.LeftText:GetText() + local okText, LText = pcall(row.LeftText.GetText, row.LeftText) + if not okText or not LText or (issecretvalue and issecretvalue(LText)) then + return + end if not Recount.db.profile.MainWindow.BarText.ServerName then LText = string.gsub(LText, "%-[^ >]+", "") end row.LeftText:SetText(LText) - while row.LeftText:GetStringWidth() > MaxNameWidth and #LText >= 2 do + local okWidth, leftTextWidth = pcall(row.LeftText.GetStringWidth, row.LeftText) + while okWidth and type(leftTextWidth) == "number" and not (issecretvalue and issecretvalue(leftTextWidth)) and leftTextWidth > MaxNameWidth and #LText >= 2 do LText = string.sub(LText, 1, #LText - 1) row.LeftText:SetText(LText.."...") + okWidth, leftTextWidth = pcall(row.LeftText.GetStringWidth, row.LeftText) end end @@ -1260,6 +1269,12 @@ function Recount:RefreshMainWindow(datarefresh) elseif MainWindow_BarText_Percent then righttext = string_format("%s (%.1f%%)", righttext, percent) end + if type(Recount.GetMainWindowBarTextOverride) == "function" then + local overrideText = Recount:GetMainWindowBarTextOverride(v[4], Recount.db.profile.MainWindowMode) + if overrideText then + righttext = overrideText + end + end percent = 100 if MaxValue ~= 0 then @@ -1268,6 +1283,12 @@ function Recount:RefreshMainWindow(datarefresh) me:SetBar(i, lefttext, righttext, percent, "Class", v[3], v[1], me.MainWindowSelectPlayer, v[4]) me:FixRow(i) rows[i].name = v[1] + if type(Recount.GetMainWindowBarLabelOverride) == "function" then + local overrideLabel = Recount:GetMainWindowBarLabelOverride(v[4], Recount.db.profile.MainWindowMode, i + offset) + if overrideLabel then + rows[i].LeftText:SetText(overrideLabel) + end + end else rows[i]:Hide() end diff --git a/Recount.lua b/Recount.lua index 8b007b1..27b2588 100644 --- a/Recount.lua +++ b/Recount.lua @@ -908,11 +908,15 @@ end function Recount:InitFightData(data) -- Init Data tracked data.Damage = 0 + data.DamagePerSecond = 0 data.FDamage = 0 data.DamageTaken = 0 + data.DamageTakenPerSecond = 0 data.Healing = 0 + data.HealingPerSecond = 0 data.HealingTaken = 0 data.Overhealing = 0 + data.AbsorbPerSecond = 0 data.DeathCount = 0 data.DOT_Time = 0 data.HOT_Time = 0 diff --git a/Recount_Modes.lua b/Recount_Modes.lua index 831c841..2efac98 100644 --- a/Recount_Modes.lua +++ b/Recount_Modes.lua @@ -273,7 +273,15 @@ function DataModes:DPSReturner(data, num) return 0 end - local _, dps = Recount:MergedPetDamageDPS(data, Recount.db.profile.CurDataSet) + local fight = data.Fights[Recount.db.profile.CurDataSet] + local dps + + if Recount.UseDamageMeter and Recount.InCombat and fight.DamagePerSecond and fight.DamagePerSecond > 0 then + dps = fight.DamagePerSecond + else + local _ + _, dps = Recount:MergedPetDamageDPS(data, Recount.db.profile.CurDataSet) + end if num == 1 then return dps diff --git a/Tracker_DamageMeter.lua b/Tracker_DamageMeter.lua index 1f22309..9d87883 100644 --- a/Tracker_DamageMeter.lua +++ b/Tracker_DamageMeter.lua @@ -9,7 +9,10 @@ if not C_DamageMeter then return end -local revision = tonumber(string.sub("$Revision: 1607 $", 12, -3)) +local AceLocale = LibStub("AceLocale-3.0") +local L = AceLocale:GetLocale("Recount") + +local revision = tonumber(string.sub("$Revision: 1609 $", 12, -3)) if Recount.Version < revision then Recount.Version = revision end @@ -22,12 +25,35 @@ local UnitClass = UnitClass local UnitLevel = UnitLevel local date = date local issecretvalue = issecretvalue +local string_format = string.format +local string_find = string.find -- Mark that we're using the new parser Recount.UseDamageMeter = true +local DEBUG = false + +local function SafeDebugText(value) + if value == nil then + return "nil" + end + if issecretvalue and issecretvalue(value) then + return "" + end + local ok, text = pcall(tostring, value) + if ok then + return text + end + return "" +end local function DP(msg) - print("|cFF00FF00[Recount DM]|r " .. tostring(msg)) + if DEBUG then + print("|cFF00FF00[Recount DM]|r " .. SafeDebugText(msg)) + end +end + +local function DE(msg) + print("|cFF00FF00[Recount DM]|r " .. SafeDebugText(msg)) end -- DamageMeterType enum values @@ -51,6 +77,55 @@ local dbCombatants -- Track state local combatSessionID = nil -- The actual combat session ID (non-zero) local updateTicker = nil +local IsSecret +local SafeCombatCall +local PROXY_PREFIX = "__RECOUNT_DM__" + +-- Secret value display cache, keyed by mode then combatant name. +-- Raw secret values are only used for UI text while synthetic numeric values drive sorting. +local secretDisplayValues = {} + +local function ClearSecretDisplayValues() + for _, modeData in pairs(secretDisplayValues) do + wipe(modeData) + end +end + +local function StoreSecretValue(modeKey, combatantName, rawValue, rawPerSec, rawLabel) + if not modeKey or not combatantName then return end + if not IsSecret(rawValue) and not IsSecret(rawPerSec) then return end + + local modeData = secretDisplayValues[modeKey] + if not modeData then + modeData = {} + secretDisplayValues[modeKey] = modeData + end + + modeData[combatantName] = { + value = rawValue, + perSec = rawPerSec, + label = rawLabel, + } +end + +local function GetSecretValue(modeKey, combatantName) + local modeData = secretDisplayValues[modeKey] + return modeData and modeData[combatantName] or nil +end + +local function IsProxyCombatantName(name) + return type(name) == "string" and string_find(name, "^" .. PROXY_PREFIX) ~= nil +end + +local function ClearProxyCombatants() + if not dbCombatants then return end + + for name in pairs(dbCombatants) do + if IsProxyCombatantName(name) then + dbCombatants[name] = nil + end + end +end -- Safe value access for secret values local function SafeNumber(val) @@ -78,7 +153,7 @@ local function SafeString(val) return tostring(val) end -local function IsSecret(val) +IsSecret = function(val) return val ~= nil and issecretvalue and issecretvalue(val) end @@ -140,7 +215,7 @@ local function ResolveName(source, orderIndex) return rosterNames[orderIndex] end - return nil + return PROXY_PREFIX .. tostring(orderIndex or 0) end -- Find or create a combatant from C_DamageMeter source data @@ -311,6 +386,8 @@ local function SnapshotSession(verbose) local sessionDuration = 0 local foundAny = false + ClearSecretDisplayValues() + local session = GetSession(DM_DamageDone) if not session or not session.combatSources then if verbose then DP(" No DamageDone session found") end @@ -322,6 +399,9 @@ local function SnapshotSession(verbose) if session.durationSeconds then sessionDuration = GetDisplayNumber(session.durationSeconds, 1) end + if sessionDuration <= 0 and Recount.InCombatT2 then + sessionDuration = math.max(0.1, GetTime() - Recount.InCombatT2) + end local hasSecrets = false for i, source in ipairs(session.combatSources) do @@ -336,11 +416,24 @@ local function SnapshotSession(verbose) local who = GetOrCreateCombatant(source, i) if who then local amount = GetDisplayNumber(source.totalAmount, i) - if amount > 0 then + local dps = GetDisplayNumber(source.amountPerSecond, i) + if amount > 0 or dps > 0 then who.Fights.CurrentFightData.Damage = amount + who.Fights.CurrentFightData.DamagePerSecond = dps + who.Fights.OverallData.Damage = amount + who.Fights.OverallData.DamagePerSecond = dps who.LastFightIn = Recount.db2.FightNum foundAny = true + if who.Name then + StoreSecretValue("Damage", who.Name, source.totalAmount, source.amountPerSecond, source.name) + if verbose and (IsSecret(source.totalAmount) or IsSecret(source.amountPerSecond)) then + DP(" Stored damage secrets for: " .. who.Name) + end + elseif verbose and (IsSecret(source.totalAmount) or IsSecret(source.amountPerSecond)) then + DP(" who.Name is nil, cannot store damage secrets") + end + if source.isLocalPlayer and Recount.FightingWho == "" then Recount.FightingWho = "Combat" end @@ -358,33 +451,45 @@ local function SnapshotSession(verbose) who.Fights.CurrentFightData.ActiveTime = sessionDuration who.Fights.CurrentFightData.TimeDamage = sessionDuration who.Fights.CurrentFightData.TimeHeal = sessionDuration + who.Fights.OverallData.ActiveTime = sessionDuration + who.Fights.OverallData.TimeDamage = sessionDuration + who.Fights.OverallData.TimeHeal = sessionDuration end end end - local function ProcessType(dmType, dataField) + local function ProcessType(dmType, dataField, rateField, secretKey) local s = GetSession(dmType) if s and s.combatSources then for idx, source in ipairs(s.combatSources) do local who = GetOrCreateCombatant(source, idx) if who then local amount = GetDisplayNumber(source.totalAmount, idx) - if amount > 0 then + local perSec = rateField and GetDisplayNumber(source.amountPerSecond, idx) or 0 + if amount > 0 or perSec > 0 then who.Fights.CurrentFightData[dataField] = amount + who.Fights.OverallData[dataField] = amount + if rateField then + who.Fights.CurrentFightData[rateField] = perSec + who.Fights.OverallData[rateField] = perSec + end who.LastFightIn = Recount.db2.FightNum foundAny = true + if who.Name and secretKey then + StoreSecretValue(secretKey, who.Name, source.totalAmount, source.amountPerSecond, source.name) + end end end end end end - ProcessType(DM_HealingDone, "Healing") - ProcessType(DM_Absorbs, "Absorbs") - ProcessType(DM_DamageTaken, "DamageTaken") - ProcessType(DM_Interrupts, "Interrupts") - ProcessType(DM_Dispels, "Dispels") - ProcessType(DM_Deaths, "DeathCount") + ProcessType(DM_HealingDone, "Healing", "HealingPerSecond", "Healing") + ProcessType(DM_Absorbs, "Absorbs", "AbsorbPerSecond", "Absorbs") + ProcessType(DM_DamageTaken, "DamageTaken", "DamageTakenPerSecond", "DamageTaken") + ProcessType(DM_Interrupts, "Interrupts", nil, "Interrupts") + ProcessType(DM_Dispels, "Dispels", nil, "Dispels") + ProcessType(DM_Deaths, "DeathCount", nil, "Deaths") Recount.NewData = true return foundAny @@ -402,9 +507,13 @@ local function ParseSessionFull() who.Fights.OverallData = who.Fights.OverallData or {} local cur = who.Fights.CurrentFightData local ovr = who.Fights.OverallData - for _, field in ipairs({"Damage", "Healing", "Absorbs", "DamageTaken", "Interrupts", "Dispels", "DeathCount", "ActiveTime", "TimeDamage", "TimeHeal"}) do + for _, field in ipairs({"Damage", "DamagePerSecond", "Healing", "HealingPerSecond", "Absorbs", "AbsorbPerSecond", "DamageTaken", "DamageTakenPerSecond", "Interrupts", "Dispels", "DeathCount", "ActiveTime", "TimeDamage", "TimeHeal"}) do if cur[field] and cur[field] > 0 then - ovr[field] = (ovr[field] or 0) + cur[field] + if field == "DamagePerSecond" or field == "HealingPerSecond" or field == "AbsorbPerSecond" or field == "DamageTakenPerSecond" then + ovr[field] = cur[field] + else + ovr[field] = (ovr[field] or 0) + cur[field] + end end end end @@ -426,29 +535,31 @@ end -- Real-time update during combat local tickCount = 0 local function UpdateTick() - if not Recount.InCombat then - if updateTicker then - updateTicker:Cancel() - updateTicker = nil + SafeCombatCall("UpdateTick", function() + if not Recount.InCombat then + if updateTicker then + updateTicker:Cancel() + updateTicker = nil + end + return end - return - end - tickCount = tickCount + 1 - local found = SnapshotSession(tickCount <= 2) -- verbose on first two ticks - if tickCount <= 3 then - DP("UpdateTick #" .. tickCount .. ": found=" .. tostring(found) .. " sessionID=" .. tostring(combatSessionID)) - end - - if found then - if Recount.FightingWho == "" then - Recount.FightingWho = "Combat" + tickCount = tickCount + 1 + local found = SnapshotSession(tickCount <= 2) -- verbose on first two ticks + if tickCount <= 3 then + DP("UpdateTick #" .. tickCount .. ": found=" .. tostring(found) .. " sessionID=" .. tostring(combatSessionID)) end - Recount.NewData = true - if Recount.RefreshMainWindow then - Recount:RefreshMainWindow() + + if found then + if Recount.FightingWho == "" then + Recount.FightingWho = "Combat" + end + Recount.NewData = true + if Recount.RefreshMainWindow then + Recount:RefreshMainWindow() + end end - end + end) end local function StartUpdateTicker() @@ -475,10 +586,14 @@ local function OnCombatEnd() C_Timer.After(0.5, function() DP("End timer fired: InCombat=" .. tostring(Recount.InCombat)) + -- Clear secret display values - real values are now available + ClearSecretDisplayValues() + ClearProxyCombatants() + -- Reset CurrentFightData before final parse for _, who in pairs(dbCombatants) do if who.LastFightIn == Recount.db2.FightNum and who.Fights and who.Fights.CurrentFightData then - for _, f in ipairs({"Damage", "Healing", "Absorbs", "DamageTaken", "Interrupts", "Dispels", "DeathCount", "ActiveTime", "TimeDamage", "TimeHeal"}) do + for _, f in ipairs({"Damage", "DamagePerSecond", "Healing", "HealingPerSecond", "Absorbs", "AbsorbPerSecond", "DamageTaken", "DamageTakenPerSecond", "Interrupts", "Dispels", "DeathCount", "ActiveTime", "TimeDamage", "TimeHeal"}) do who.Fights.CurrentFightData[f] = 0 end end @@ -507,6 +622,8 @@ local function OnEvent(self, event, ...) end DP("PLAYER_REGEN_DISABLED - combat start") combatSessionID = nil + ClearSecretDisplayValues() + ClearProxyCombatants() Recount:PutInCombat() StartUpdateTicker() @@ -548,6 +665,8 @@ function Recount:InitDamageMeterTracker() dmFrame:RegisterEvent("DAMAGE_METER_CURRENT_SESSION_UPDATED") dmFrame:RegisterEvent("DAMAGE_METER_RESET") dmFrame:RegisterEvent("INSTANCE_ENCOUNTER_ENGAGE_UNIT") + + DP("Init complete") end local originalResetDataUnsafe = Recount.ResetDataUnsafe @@ -560,5 +679,139 @@ function Recount:ResetDataUnsafe() Recount._resettingData = false dbCombatants = Recount.db2.combatants combatSessionID = nil + ClearSecretDisplayValues() + ClearProxyCombatants() DP("ResetDataUnsafe complete") end + +local function FormatRealtimeValue(value) + if value == nil then + return nil + end + + if IsSecret(value) then + local ok, text = pcall(string_format, "%.0f", value) + if ok and text then + return text + end + + ok, text = pcall(string_format, "%s", value) + if ok and text then + return text + end + + return nil + end + + return Recount:FormatLongNums(value) +end + +local function GetMainWindowSecretEntry(modeData, combatantName) + if not modeData or not combatantName then + return nil, nil + end + + local modeName = modeData[1] + local modeCategory = modeData[7] + + if modeName == L["DPS"] then + return GetSecretValue("Damage", combatantName), true + end + + if modeCategory == "Damage" then + return GetSecretValue("Damage", combatantName), false + end + + if modeCategory == "Healing" then + return GetSecretValue("Healing", combatantName), false + end + + if modeCategory == "DamageTaken" then + return GetSecretValue("DamageTaken", combatantName), false + end + + if modeName == L["Absorbs"] then + return GetSecretValue("Absorbs", combatantName), false + end + + if modeName == L["Deaths"] then + return GetSecretValue("Deaths", combatantName), false + end + + return nil, nil +end + +function Recount:GetMainWindowBarLabelOverride(combatant, modeIndex, rank) + if not self.UseDamageMeter or not self.InCombat then + return nil + end + + local combatantName = type(combatant) == "table" and combatant.Name or combatant + if not combatantName then + return nil + end + + local modeData = self.MainWindowData and self.MainWindowData[modeIndex] + local entry = select(1, GetMainWindowSecretEntry(modeData, combatantName)) + local label = entry and entry.label + if not label then + return nil + end + + if self.db.profile.MainWindow.BarText.RankNum and rank then + local ok, text = pcall(string_format, "%d. %s", rank, label) + if ok and text then + return text + end + end + + return label +end + +function Recount:GetMainWindowBarTextOverride(combatant, modeIndex) + if not self.UseDamageMeter or not self.InCombat then + return nil + end + + local combatantName = type(combatant) == "table" and combatant.Name or combatant + if not combatantName then + return nil + end + + local modeData = self.MainWindowData and self.MainWindowData[modeIndex] + local entry, useRate = GetMainWindowSecretEntry(modeData, combatantName) + if not entry then + return nil + end + + local valueText = FormatRealtimeValue(useRate and entry.perSec or entry.value) + if not valueText then + return nil + end + + if useRate then + return valueText + end + + local barText = self.db and self.db.profile and self.db.profile.MainWindow and self.db.profile.MainWindow.BarText + if barText and barText.PerSec then + local perSecText = FormatRealtimeValue(entry.perSec) + if perSecText then + return string_format("%s (%s)", valueText, perSecText) + end + end + + return valueText +end + +SafeCombatCall = function(context, func) + local ok, err = xpcall(func, function(message) + return SafeDebugText(message) + end) + + if not ok then + DE(context .. " error: " .. SafeDebugText(err)) + end + + return ok +end From 8c83d92eaf41349785ae45ba043dfedd318ff0df Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Mon, 9 Mar 2026 00:47:22 +0200 Subject: [PATCH 07/15] Remove PROGRESS.md tracking file --- PROGRESS.md | 68 ----------------------------------------------------- 1 file changed, 68 deletions(-) delete mode 100644 PROGRESS.md diff --git a/PROGRESS.md b/PROGRESS.md deleted file mode 100644 index 86ca144..0000000 --- a/PROGRESS.md +++ /dev/null @@ -1,68 +0,0 @@ -# Recount Midnight (12.0.1) Migration Progress - -## Status: IN PROGRESS - -## Completed -- [x] TOC version bump (120001, IconTexture) -- [x] GetSpellInfo -> C_Spell.GetSpellInfo compat wrapper -- [x] SendChatMessage -> C_ChatInfo.SendChatMessage fallback -- [x] GetMouseFocus -> GetMouseFoci compat -- [x] UIFrameFade fallback -- [x] ColorPickerFrame retail compat (GUI_Realtime.lua) -- [x] IsInScenarioGroup fallback (deletion.lua) -- [x] table.getn -> # operator -- [x] ChatThrottleLib v29 -> v31 -- [x] Ace3 libraries updated to latest upstream -- [x] FillLocalizedClassList -> LOCALIZED_CLASS_NAMES_MALE -- [x] BNGetFriendInfo -> C_BattleNet.GetFriendAccountInfo - -## Current: C_DamageMeter Migration (CRITICAL) - -### Problem -WoW 12.0 blocks addons from registering `COMBAT_LOG_EVENT_UNFILTERED`. -Both Details and Skada have migrated to Blizzard's server-side `C_DamageMeter` API. - -### Plan -1. [x] Create `Tracker_DamageMeter.lua` - new parser using C_DamageMeter API -2. [x] Modify `Recount.lua` - conditionally use new parser on 12.0+ -3. [x] Modify `Recount.toc` - add new file before Tracker.lua -4. [x] Handle secret values (issecretvalue) during active combat -5. [x] Disable/hide modes that can't be populated by C_DamageMeter - -### What C_DamageMeter CAN provide (Recount modes that WILL work): -- Damage Done / DPS (type 0/1) -- Healing Done / HPS (type 2/3) -- Absorbs (type 4) -- Damage Taken (type 7) -- Interrupts (type 5) -- Dispels (type 6) -- Deaths (type 9) -- Per-spell breakdowns for all above -- Duration/active time from session durationSeconds - -### What C_DamageMeter CANNOT provide (modes that will be DISABLED): -- Friendly Fire (no equivalent type) -- Overhealing (no equivalent type) -- DOT Uptime / HOT Uptime (no aura tracking) -- CC Breaks (no equivalent) -- Power Gains (mana/energy/rage/runic power) -- Per-hit stats (crit/miss/dodge/parry breakdown) -- Target breakdowns (who damaged whom) -- Element/school breakdowns -- Resurrections - -### Key API Details -- Session types: 0=Overall, 1=Current, 2=Expired -- Damage types: 0=DamageDone, 1=DPS, 2=HealingDone, 3=HPS, 4=Absorbs, 5=Interrupts, 6=Dispels, 7=DamageTaken, 8=AvoidableDamageTaken, 9=Deaths -- Events: DAMAGE_METER_COMBAT_SESSION_UPDATED, DAMAGE_METER_CURRENT_SESSION_UPDATED, DAMAGE_METER_RESET -- Secret values: data is opaque during combat, use issecretvalue() to check -- Combat detection: PLAYER_REGEN_DISABLED/ENABLED (same as before) - -## Testing -- [ ] Addon loads without errors -- [ ] Main window shows damage data after combat -- [ ] Per-spell breakdown works in detail view -- [ ] Fight segments rotate correctly -- [ ] Reset data works -- [ ] Modes that have data display correctly -- [ ] Modes without data are hidden/disabled gracefully From 65d5a14deeb54edcd99ab482c450b269590259cb Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Mon, 9 Mar 2026 00:57:20 +0200 Subject: [PATCH 08/15] Stabilize realtime group member labels --- Tracker_DamageMeter.lua | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Tracker_DamageMeter.lua b/Tracker_DamageMeter.lua index 9d87883..46df05f 100644 --- a/Tracker_DamageMeter.lua +++ b/Tracker_DamageMeter.lua @@ -204,15 +204,12 @@ local function ResolveName(source, orderIndex) return UnitName("player") end - -- Try roster matching by order (best effort during combat) - RefreshRosterCache() - local rosterNames = {} - for rName in pairs(rosterCache) do - rosterNames[#rosterNames + 1] = rName - end - table.sort(rosterNames) - if orderIndex and rosterNames[orderIndex] then - return rosterNames[orderIndex] + -- For other live group members, keep an opaque internal key and render the + -- secret name directly in the UI. Realtime session rows are damage-sorted, so + -- guessing from party order is unreliable and causes identity swaps. + local guid = SafeString(source.sourceGUID) + if guid then + return PROXY_PREFIX .. guid end return PROXY_PREFIX .. tostring(orderIndex or 0) From a5ac488b2611e680146bd663744d7d61e48f736c Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Mon, 9 Mar 2026 01:07:19 +0200 Subject: [PATCH 09/15] Fix Midnight healing and metric support --- Recount_Modes.lua | 27 +++++ Tracker_DamageMeter.lua | 240 +++++++++++++++++++++++++++++----------- 2 files changed, 201 insertions(+), 66 deletions(-) diff --git a/Recount_Modes.lua b/Recount_Modes.lua index 2efac98..236c697 100644 --- a/Recount_Modes.lua +++ b/Recount_Modes.lua @@ -671,6 +671,26 @@ local MainWindowModes = { {L["Activity"], DataModes.ActiveTime, TooltipFuncs.ActiveTime, nil, nil, nil, nil}, } +local DamageMeterSupportedModes = { + [L["Damage Done"]] = true, + [L["DPS"]] = true, + [L["Damage Taken"]] = true, + [L["Healing Done"]] = true, + [L["Absorbs"]] = true, + [L["Deaths"]] = true, + [L["Activity"]] = true, + [L["Interrupts"]] = true, + [L["Dispels"]] = true, +} + +local function IsDamageMeterSupportedMode(modeName) + if not Recount.UseDamageMeter then + return true + end + + return DamageMeterSupportedModes[modeName] == true +end + function Recount:AddModeTooltip(lname, modefunc, toolfunc, ...) tinsert(MainWindowModes, {lname, modefunc, toolfunc, ...}) Recount:SetupMainWindow() @@ -786,6 +806,13 @@ function Recount:SetupMainWindow() end end end + if Recount.UseDamageMeter then + for k, v in pairs(MainWindowModes) do + if v and not IsDamageMeterSupportedMode(v[1]) then + MainWindowModes[k] = nil + end + end + end for i = 1, #MainWindowModes do if MainWindowModes[i] == nil then local x = i + 1 diff --git a/Tracker_DamageMeter.lua b/Tracker_DamageMeter.lua index 46df05f..0926fab 100644 --- a/Tracker_DamageMeter.lua +++ b/Tracker_DamageMeter.lua @@ -80,6 +80,20 @@ local updateTicker = nil local IsSecret local SafeCombatCall local PROXY_PREFIX = "__RECOUNT_DM__" +local overallBaseline = {} + +local TRACKED_TOTAL_FIELDS = { + "Damage", + "Healing", + "Absorbs", + "DamageTaken", + "Interrupts", + "Dispels", + "DeathCount", + "ActiveTime", + "TimeDamage", + "TimeHeal", +} -- Secret value display cache, keyed by mode then combatant name. -- Raw secret values are only used for UI text while synthetic numeric values drive sorting. @@ -91,6 +105,45 @@ local function ClearSecretDisplayValues() end end +local function ClearOverallBaseline() + wipe(overallBaseline) +end + +local function CaptureOverallBaseline() + ClearOverallBaseline() + if not dbCombatants then + return + end + + for name, who in pairs(dbCombatants) do + local fightData = who and who.Fights and who.Fights.OverallData + local baseline = {} + for _, field in ipairs(TRACKED_TOTAL_FIELDS) do + baseline[field] = fightData and fightData[field] or 0 + end + overallBaseline[name] = baseline + end +end + +local function GetOverallBaseline(who, field) + if not who or not field or not who.Name then + return 0 + end + + local baseline = overallBaseline[who.Name] + if not baseline then + baseline = {} + overallBaseline[who.Name] = baseline + end + + if baseline[field] == nil then + local fightData = who.Fights and who.Fights.OverallData + baseline[field] = fightData and fightData[field] or 0 + end + + return baseline[field] or 0 +end + local function StoreSecretValue(modeKey, combatantName, rawValue, rawPerSec, rawLabel) if not modeKey or not combatantName then return end if not IsSecret(rawValue) and not IsSecret(rawPerSec) then return end @@ -301,9 +354,17 @@ local function GetSession(dmType) end -- Get spell source data for a combatant -local function GetSpellSource(dmType, guid) +local function GetSpellSource(dmType, guid, sessionType) local ok, sourceData + if sessionType == DM_Overall then + ok, sourceData = pcall(C_DamageMeter.GetCombatSessionSourceFromType, DM_Overall, dmType, guid) + if ok and sourceData and sourceData.combatSpells and #sourceData.combatSpells > 0 then + return sourceData + end + return nil + end + if combatSessionID then ok, sourceData = pcall(C_DamageMeter.GetCombatSessionSourceFromID, combatSessionID, dmType, guid) if ok and sourceData and sourceData.combatSpells and #sourceData.combatSpells > 0 then @@ -324,21 +385,13 @@ local function GetSpellSource(dmType, guid) return nil end --- Add spell breakdown data for a combatant -local function AddSpellBreakdown(who, dmType, datatypeAttacks) - if not who then return end - - local guid = who.GUID - if not guid or IsSecret(guid) then - DP(" No GUID for spell lookup: " .. (who.Name or "?")) - return +local function ApplySpellBreakdown(fightData, sourceData, datatypeAttacks) + if not fightData or not datatypeAttacks or not sourceData or not sourceData.combatSpells then + return 0 end - local sourceData = GetSpellSource(dmType, guid) - if not sourceData or not sourceData.combatSpells then - DP(" No spell source data for " .. (who.Name or "?") .. " dmType=" .. dmType .. " guid=" .. guid) - return - end + fightData[datatypeAttacks] = fightData[datatypeAttacks] or {} + wipe(fightData[datatypeAttacks]) local spellCount = 0 for _, spell in ipairs(sourceData.combatSpells) do @@ -352,29 +405,47 @@ local function AddSpellBreakdown(who, dmType, datatypeAttacks) end spellName = spellName or ("Spell " .. spellID) - if datatypeAttacks and who.Fights then - for _, fightKey in ipairs({"CurrentFightData", "OverallData"}) do - local fightData = who.Fights[fightKey] - if fightData then - fightData[datatypeAttacks] = fightData[datatypeAttacks] or {} - fightData[datatypeAttacks][spellName] = { - count = 1, - amount = amount, - Details = { - ["Hit"] = { - count = 1, - amount = amount, - max = amount, - min = amount, - } - } - } - end - end - end + fightData[datatypeAttacks][spellName] = { + count = 1, + amount = amount, + Details = { + ["Hit"] = { + count = 1, + amount = amount, + max = amount, + min = amount, + } + } + } spellCount = spellCount + 1 end end + + return spellCount +end + +-- Add spell breakdown data for a combatant +local function AddSpellBreakdown(who, dmType, datatypeAttacks) + if not who then return end + + local guid = who.GUID + if not guid or IsSecret(guid) then + DP(" No GUID for spell lookup: " .. (who.Name or "?")) + return + end + + local currentSource = GetSpellSource(dmType, guid, DM_Current) + if currentSource then + ApplySpellBreakdown(who.Fights and who.Fights.CurrentFightData, currentSource, datatypeAttacks) + end + + local overallSource = GetSpellSource(dmType, guid, DM_Overall) + if not overallSource or not overallSource.combatSpells then + DP(" No spell source data for " .. (who.Name or "?") .. " dmType=" .. dmType .. " guid=" .. guid) + return + end + + local spellCount = ApplySpellBreakdown(who.Fights and who.Fights.OverallData, overallSource, datatypeAttacks) DP(" Spells for " .. (who.Name or "?") .. " dmType=" .. dmType .. ": " .. spellCount) end @@ -400,6 +471,25 @@ local function SnapshotSession(verbose) sessionDuration = math.max(0.1, GetTime() - Recount.InCombatT2) end + local function SetTrackedValue(who, dataField, amount, rateField, perSec) + if not who or not who.Fights then + return + end + + local currentFight = who.Fights.CurrentFightData + local overallFight = who.Fights.OverallData + if not currentFight or not overallFight then + return + end + + currentFight[dataField] = amount + overallFight[dataField] = GetOverallBaseline(who, dataField) + amount + if rateField then + currentFight[rateField] = perSec + overallFight[rateField] = perSec + end + end + local hasSecrets = false for i, source in ipairs(session.combatSources) do -- Check if values are secret (API sorts sources highest-first) @@ -415,10 +505,7 @@ local function SnapshotSession(verbose) local amount = GetDisplayNumber(source.totalAmount, i) local dps = GetDisplayNumber(source.amountPerSecond, i) if amount > 0 or dps > 0 then - who.Fights.CurrentFightData.Damage = amount - who.Fights.CurrentFightData.DamagePerSecond = dps - who.Fights.OverallData.Damage = amount - who.Fights.OverallData.DamagePerSecond = dps + SetTrackedValue(who, "Damage", amount, "DamagePerSecond", dps) who.LastFightIn = Recount.db2.FightNum foundAny = true @@ -448,9 +535,9 @@ local function SnapshotSession(verbose) who.Fights.CurrentFightData.ActiveTime = sessionDuration who.Fights.CurrentFightData.TimeDamage = sessionDuration who.Fights.CurrentFightData.TimeHeal = sessionDuration - who.Fights.OverallData.ActiveTime = sessionDuration - who.Fights.OverallData.TimeDamage = sessionDuration - who.Fights.OverallData.TimeHeal = sessionDuration + who.Fights.OverallData.ActiveTime = GetOverallBaseline(who, "ActiveTime") + sessionDuration + who.Fights.OverallData.TimeDamage = GetOverallBaseline(who, "TimeDamage") + sessionDuration + who.Fights.OverallData.TimeHeal = GetOverallBaseline(who, "TimeHeal") + sessionDuration end end end @@ -464,12 +551,7 @@ local function SnapshotSession(verbose) local amount = GetDisplayNumber(source.totalAmount, idx) local perSec = rateField and GetDisplayNumber(source.amountPerSecond, idx) or 0 if amount > 0 or perSec > 0 then - who.Fights.CurrentFightData[dataField] = amount - who.Fights.OverallData[dataField] = amount - if rateField then - who.Fights.CurrentFightData[rateField] = perSec - who.Fights.OverallData[rateField] = perSec - end + SetTrackedValue(who, dataField, amount, rateField, perSec) who.LastFightIn = Recount.db2.FightNum foundAny = true if who.Name and secretKey then @@ -492,30 +574,12 @@ local function SnapshotSession(verbose) return foundAny end --- Full parse: snapshot + copy to OverallData + spell breakdowns +-- Full parse: final snapshot + spell breakdowns local function ParseSessionFull() DP("ParseSessionFull: combatSessionID=" .. tostring(combatSessionID)) local foundAny = SnapshotSession(true) if foundAny then - -- Copy CurrentFightData to OverallData - for _, who in pairs(dbCombatants) do - if who.LastFightIn == Recount.db2.FightNum and who.Fights and who.Fights.CurrentFightData then - who.Fights.OverallData = who.Fights.OverallData or {} - local cur = who.Fights.CurrentFightData - local ovr = who.Fights.OverallData - for _, field in ipairs({"Damage", "DamagePerSecond", "Healing", "HealingPerSecond", "Absorbs", "AbsorbPerSecond", "DamageTaken", "DamageTakenPerSecond", "Interrupts", "Dispels", "DeathCount", "ActiveTime", "TimeDamage", "TimeHeal"}) do - if cur[field] and cur[field] > 0 then - if field == "DamagePerSecond" or field == "HealingPerSecond" or field == "AbsorbPerSecond" or field == "DamageTakenPerSecond" then - ovr[field] = cur[field] - else - ovr[field] = (ovr[field] or 0) + cur[field] - end - end - end - end - end - -- Spell breakdowns for _, who in pairs(dbCombatants) do if who.LastFightIn == Recount.db2.FightNum then @@ -622,6 +686,7 @@ local function OnEvent(self, event, ...) ClearSecretDisplayValues() ClearProxyCombatants() Recount:PutInCombat() + CaptureOverallBaseline() StartUpdateTicker() elseif event == "PLAYER_REGEN_ENABLED" then @@ -641,6 +706,7 @@ local function OnEvent(self, event, ...) elseif event == "DAMAGE_METER_RESET" then DP("DAMAGE_METER_RESET") + ClearOverallBaseline() if not Recount._resettingData then Recount:ResetData() end @@ -678,6 +744,7 @@ function Recount:ResetDataUnsafe() combatSessionID = nil ClearSecretDisplayValues() ClearProxyCombatants() + ClearOverallBaseline() DP("ResetDataUnsafe complete") end @@ -731,6 +798,14 @@ local function GetMainWindowSecretEntry(modeData, combatantName) return GetSecretValue("Absorbs", combatantName), false end + if modeName == L["Interrupts"] then + return GetSecretValue("Interrupts", combatantName), false + end + + if modeName == L["Dispels"] then + return GetSecretValue("Dispels", combatantName), false + end + if modeName == L["Deaths"] then return GetSecretValue("Deaths", combatantName), false end @@ -776,6 +851,39 @@ function Recount:GetMainWindowBarTextOverride(combatant, modeIndex) end local modeData = self.MainWindowData and self.MainWindowData[modeIndex] + local modeCategory = modeData and modeData[7] + if modeCategory == "Healing" and self.db and self.db.profile and self.db.profile.MergeAbsorbs then + local healingEntry = GetSecretValue("Healing", combatantName) + local absorbEntry = GetSecretValue("Absorbs", combatantName) + if healingEntry or absorbEntry then + local healingValueText = FormatRealtimeValue(healingEntry and healingEntry.value) + local absorbValueText = FormatRealtimeValue(absorbEntry and absorbEntry.value) + + if healingEntry and absorbEntry and not IsSecret(healingEntry.value) and not IsSecret(absorbEntry.value) then + healingValueText = Recount:FormatLongNums(SafeNumber(healingEntry.value) + SafeNumber(absorbEntry.value)) + absorbValueText = nil + end + + if healingValueText then + local valueText = absorbValueText and string_format("%s + %s", healingValueText, absorbValueText) or healingValueText + local barText = self.db and self.db.profile and self.db.profile.MainWindow and self.db.profile.MainWindow.BarText + if barText and barText.PerSec then + local healingPerSecText = FormatRealtimeValue(healingEntry and healingEntry.perSec) + local absorbPerSecText = FormatRealtimeValue(absorbEntry and absorbEntry.perSec) + if healingEntry and absorbEntry and healingPerSecText and absorbPerSecText and not IsSecret(healingEntry.perSec) and not IsSecret(absorbEntry.perSec) then + healingPerSecText = Recount:FormatLongNums(SafeNumber(healingEntry.perSec) + SafeNumber(absorbEntry.perSec)) + absorbPerSecText = nil + end + if healingPerSecText then + local perSecText = absorbPerSecText and string_format("%s + %s", healingPerSecText, absorbPerSecText) or healingPerSecText + return string_format("%s (%s)", valueText, perSecText) + end + end + return valueText + end + end + end + local entry, useRate = GetMainWindowSecretEntry(modeData, combatantName) if not entry then return nil From 42d3888bfbc7099779ada16733605837c1aea45e Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Mon, 9 Mar 2026 01:09:49 +0200 Subject: [PATCH 10/15] Stabilize fight segment dropdown order --- GUI_Main.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GUI_Main.lua b/GUI_Main.lua index 7cc657b..49e094a 100644 --- a/GUI_Main.lua +++ b/GUI_Main.lua @@ -1374,7 +1374,7 @@ function Recount:OpenFightDropDown(myframe) local currentorder = 1 - for k, v in pairs(Recount.db2.FoughtWho) do + for k, v in ipairs(Recount.db2.FoughtWho) do fightopts.args["fight"..currentorder] = { order = 30 + (currentorder - 1) * 10, name = L["Fight"].." "..k.." - "..v, @@ -1507,7 +1507,7 @@ function me:CreateFightDropdown(level) end UIDropDownMenu_AddButton(info, level) - for k, v in pairs(Recount.db2.FoughtWho) do + for k, v in ipairs(Recount.db2.FoughtWho) do info.checked = nil info.text = L["Fight"].." "..k.." - "..v if Recount.db.profile.CurDataSet == "Fight"..k then From 464b4c9c516d1a153a8c0616dd935ffe7e2ca22e Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Mon, 9 Mar 2026 01:21:47 +0200 Subject: [PATCH 11/15] Polish Midnight unsupported mode UX --- GUI_Config.lua | 34 +++++++++++++++++++++--- GUI_Detail.lua | 43 +++++++++++++++++++++++-------- Recount.lua | 57 ++++++++++++++++++++++++++--------------- Tracker_DamageMeter.lua | 7 ++--- 4 files changed, 105 insertions(+), 36 deletions(-) diff --git a/GUI_Config.lua b/GUI_Config.lua index 6a40d1f..97fe4b1 100644 --- a/GUI_Config.lua +++ b/GUI_Config.lua @@ -264,6 +264,7 @@ end function me:CreateWindowModuleSelection(parent) me.ModuleOptions = CreateFrame("Frame", nil, parent) + local useNativeDamageMeter = C_DamageMeter ~= nil local theFrame = me.ModuleOptions @@ -306,7 +307,11 @@ function me:CreateWindowModuleSelection(parent) end end) theFrame.Deaths = me:CreateSavedCheckbox(L["Deaths"], theFrame, "Modules", "Deaths") - theFrame.Deaths:SetPoint("TOPLEFT", theFrame.OverhealingDone, "BOTTOMLEFT", 0, 0) + if useNativeDamageMeter then + theFrame.Deaths:SetPoint("TOPLEFT", theFrame, "TOPLEFT", 8, -20) + else + theFrame.Deaths:SetPoint("TOPLEFT", theFrame.OverhealingDone, "BOTTOMLEFT", 0, 0) + end theFrame.Deaths:SetScript("OnClick", function(this) if this:GetChecked() then this:SetChecked(true) @@ -351,7 +356,11 @@ function me:CreateWindowModuleSelection(parent) end end) theFrame.Activity = me:CreateSavedCheckbox(L["Activity"], theFrame, "Modules", "Activity") - theFrame.Activity:SetPoint("TOPLEFT", theFrame.HOTUptime, "BOTTOMLEFT", 0, 0) + if useNativeDamageMeter then + theFrame.Activity:SetPoint("TOPLEFT", theFrame.Deaths, "BOTTOMLEFT", 0, 0) + else + theFrame.Activity:SetPoint("TOPLEFT", theFrame.HOTUptime, "BOTTOMLEFT", 0, 0) + end theFrame.Activity:SetScript("OnClick", function(this) if this:GetChecked() then this:SetChecked(true) @@ -365,6 +374,16 @@ function me:CreateWindowModuleSelection(parent) Recount:RefreshMainWindow() end end) + if useNativeDamageMeter then + theFrame.HealingTaken:Hide() + theFrame.HealingTaken:Disable() + theFrame.OverhealingDone:Hide() + theFrame.OverhealingDone:Disable() + theFrame.DOTUptime:Hide() + theFrame.DOTUptime:Disable() + theFrame.HOTUptime:Hide() + theFrame.HOTUptime:Disable() + end end function me:CreateClassColorSelection(parent) @@ -1350,6 +1369,7 @@ end function me:SetupRealtimeOptions(parent) me.RealtimeOptions = CreateFrame("Frame", nil, parent) + local useNativeDamageMeter = C_DamageMeter ~= nil local theFrame = me.RealtimeOptions theFrame:SetHeight(parent:GetHeight() - 34) theFrame:SetWidth(200) @@ -1398,10 +1418,18 @@ function me:SetupRealtimeOptions(parent) Recount:CreateRealtimeWindow("!RAID", "HEALINGTAKEN", "Raid HTPS") end) theFrame.RHTPSButton:SetText(L["HTPS"]) + if useNativeDamageMeter then + theFrame.RHTPSButton:Hide() + theFrame.RHTPSButton:Disable() + end theFrame.TitleRaid = theFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") theFrame.TitleRaid:SetText(L["Network"]) - theFrame.TitleRaid:SetPoint("TOP", theFrame, "TOP", 0, -106) + if useNativeDamageMeter then + theFrame.TitleRaid:SetPoint("TOP", theFrame, "TOP", 0, -80) + else + theFrame.TitleRaid:SetPoint("TOP", theFrame, "TOP", 0, -106) + end theFrame.FPSButton = CreateFrame("Button", nil, theFrame, "UIPanelButtonTemplate") theFrame.FPSButton:SetWidth(90) diff --git a/GUI_Detail.lua b/GUI_Detail.lua index 7241c5c..4ef82a2 100644 --- a/GUI_Detail.lua +++ b/GUI_Detail.lua @@ -1436,11 +1436,16 @@ function Recount:UpdateSummaryMode(name) local activetime = (data2.ActiveTime or 0) + Epsilon local damagetaken = data2.DamageTaken or 0 local dot_time = data2.DOT_Time or 0 + local unsupportedText = "N/A" theFrame.Damage.Total:SetValue(damage) theFrame.Damage.Taken:SetValue(damagetaken) theFrame.Damage.PerSec:SetValue((math.floor(10 * damage / (activetime) + 0.5) / 10)) theFrame.Damage.Time:SetValue(timedamage.."s ("..math.floor(100 * timedamage / (TotalTime + Epsilon) + 0.5).."%)") - theFrame.Damage.Misc:SetValue(math.floor(10 * dot_time / (activetime + Epsilon) + 0.5) / 10) + if Recount.UseDamageMeter then + theFrame.Damage.Misc:SetValue(unsupportedText) + else + theFrame.Damage.Misc:SetValue(math.floor(10 * dot_time / (activetime + Epsilon) + 0.5) / 10) + end --Set Pet Data if data.Pet and #data.Pet > 0 then @@ -1480,8 +1485,12 @@ function Recount:UpdateSummaryMode(name) theFrame.Pet.Taken:SetValue(petdamagetaken) theFrame.Pet.PerSec:SetValue((math.floor(10 * petdamage / petactivetime + 0.5) / 10)) theFrame.Pet.Time:SetValue(pettimedamage) - Num, Focus = me:CalculateFocus(pettimedamaging) - theFrame.Pet.Focus:SetValue(Num.." ("..Focus.."%)") + if Recount.UseDamageMeter then + theFrame.Pet.Focus:SetValue(unsupportedText) + else + Num, Focus = me:CalculateFocus(pettimedamaging) + theFrame.Pet.Focus:SetValue(Num.." ("..Focus.."%)") + end if #data.Pet > 1 then theFrame.Pet.Page:SetText(L["Click for next Pet"]) @@ -1506,21 +1515,35 @@ function Recount:UpdateSummaryMode(name) local hot_time = data2.HOT_Time or 0 theFrame.Healing.Total:SetValue((healing + absorbs).." ("..(math.floor(10 * (healing + absorbs) / (activetime) + 0.5) / 10)..")") - theFrame.Healing.Taken:SetValue(healingtaken) - theFrame.Healing.Overhealing:SetValue(overhealing.." ("..(math.floor(10 * overhealing / (activetime) + 0.5) / 10)..")".." ("..(math.floor(1000 * overhealing / (overhealing + healing + Epsilon) + 0.5) / 10).."%)") theFrame.Healing.Time:SetValue(timeheal.."s ("..math.floor(100 * timeheal / (TotalTime + Epsilon) + 0.5).."%)") - theFrame.Healing.Misc:SetValue(math.floor(10 * hot_time / (activetime + Epsilon) + 0.5) / 10) + if Recount.UseDamageMeter then + theFrame.Healing.Taken:SetValue(unsupportedText) + theFrame.Healing.Overhealing:SetValue(unsupportedText) + theFrame.Healing.Misc:SetValue(unsupportedText) + else + theFrame.Healing.Taken:SetValue(healingtaken) + theFrame.Healing.Overhealing:SetValue(overhealing.." ("..(math.floor(10 * overhealing / (activetime) + 0.5) / 10)..")".." ("..(math.floor(1000 * overhealing / (overhealing + healing + Epsilon) + 0.5) / 10).."%)") + theFrame.Healing.Misc:SetValue(math.floor(10 * hot_time / (activetime + Epsilon) + 0.5) / 10) + end local timedamaging = data2.TimeDamaging - Num, Focus = me:CalculateFocus(timedamaging) - theFrame.Damage.Focus:SetValue(Num.." ("..Focus.."%)") + if Recount.UseDamageMeter then + theFrame.Damage.Focus:SetValue(unsupportedText) + else + Num, Focus = me:CalculateFocus(timedamaging) + theFrame.Damage.Focus:SetValue(Num.." ("..Focus.."%)") + end local timehealing = data2.TimeHealing - Num, Focus = me:CalculateFocus(timehealing) - theFrame.Healing.Focus:SetValue(Num.." ("..Focus.."%)") + if Recount.UseDamageMeter then + theFrame.Healing.Focus:SetValue(unsupportedText) + else + Num, Focus = me:CalculateFocus(timehealing) + theFrame.Healing.Focus:SetValue(Num.." ("..Focus.."%)") + end theFrame.Name = name diff --git a/Recount.lua b/Recount.lua index 27b2588..05a4844 100644 --- a/Recount.lua +++ b/Recount.lua @@ -38,6 +38,14 @@ local C_PetBattles = C_PetBattles local GetNumGroupMembers = GetNumGroupMembers local GetNumPartyMembers = GetNumPartyMembers or GetNumSubgroupMembers local GetNumRaidMembers = GetNumRaidMembers or GetNumGroupMembers +local USE_NATIVE_DAMAGE_METER = C_DamageMeter ~= nil + +local function LegacyBindingLabel(label) + if USE_NATIVE_DAMAGE_METER then + return nil + end + return L["Display"].." "..L[label] +end local GetTime = GetTime local IsInRaid = IsInRaid local UnitAffectingCombat = UnitAffectingCombat @@ -208,11 +216,11 @@ local Default_Profile = { Font = "Arial Narrow", Scaling = 1, Modules = { - HealingTaken = true, - OverhealingDone = true, + HealingTaken = not USE_NATIVE_DAMAGE_METER, + OverhealingDone = not USE_NATIVE_DAMAGE_METER, Deaths = true, - DOTUptime = true, - HOTUptime = true, + DOTUptime = not USE_NATIVE_DAMAGE_METER, + HOTUptime = not USE_NATIVE_DAMAGE_METER, Activity = true, }, MainWindow = { @@ -330,28 +338,28 @@ BINDING_NAME_RECOUNT_PREVIOUSPAGE = L["Show previous main page"] BINDING_NAME_RECOUNT_NEXTPAGE = L["Show next main page"] BINDING_NAME_RECOUNT_DAMAGE = L["Display"].." "..L["Damage Done"] BINDING_NAME_RECOUNT_DPS = L["Display"].." "..L["DPS"] -BINDING_NAME_RECOUNT_FRIENDLYFIRE = L["Display"].." "..L["Friendly Fire"] +BINDING_NAME_RECOUNT_FRIENDLYFIRE = LegacyBindingLabel("Friendly Fire") BINDING_NAME_RECOUNT_DAMAGETAKEN = L["Display"].." "..L["Damage Taken"] BINDING_NAME_RECOUNT_HEALING = L["Display"].." "..L["Healing Done"] -BINDING_NAME_RECOUNT_HEALINGTAKEN = L["Display"].." "..L["Healing Taken"] -BINDING_NAME_RECOUNT_OVERHEALING = L["Display"].." "..L["Overhealing Done"] +BINDING_NAME_RECOUNT_HEALINGTAKEN = LegacyBindingLabel("Healing Taken") +BINDING_NAME_RECOUNT_OVERHEALING = LegacyBindingLabel("Overhealing Done") BINDING_NAME_RECOUNT_DEATHS = L["Display"].." "..L["Deaths"] -BINDING_NAME_RECOUNT_DOTS = L["Display"].." "..L["DOT Uptime"] -BINDING_NAME_RECOUNT_HOTS = L["Display"].." "..L["HOT Uptime"] +BINDING_NAME_RECOUNT_DOTS = LegacyBindingLabel("DOT Uptime") +BINDING_NAME_RECOUNT_HOTS = LegacyBindingLabel("HOT Uptime") BINDING_NAME_RECOUNT_ACTIVITY = L["Display"].." "..L["Activity"] BINDING_NAME_RECOUNT_DISPELS = L["Display"].." "..L["Dispels"] -BINDING_NAME_RECOUNT_DISPELLED = L["Display"].." "..L["Dispelled"] +BINDING_NAME_RECOUNT_DISPELLED = LegacyBindingLabel("Dispelled") BINDING_NAME_RECOUNT_INTERRUPTS = L["Display"].." "..L["Interrupts"] -BINDING_NAME_RECOUNT_RESURRECT = L["Display"].." "..L["Ressers"] -BINDING_NAME_RECOUNT_CCBREAKER = L["Display"].." "..L["CC Breakers"] -BINDING_NAME_RECOUNT_MANA = L["Display"].." "..L["Mana Gained"] -BINDING_NAME_RECOUNT_ENERGY = L["Display"].." "..L["Energy Gained"] -BINDING_NAME_RECOUNT_RAGE = L["Display"].." "..L["Rage Gained"] -BINDING_NAME_RECOUNT_RUNICPOWER = L["Display"].." "..L["Runic Power Gained"] -BINDING_NAME_RECOUNT_LUNAR_POWER = L["Display"].." "..L["Astral Power Gained"] -BINDING_NAME_RECOUNT_MAELSTROM = L["Display"].." "..L["Maelstorm Gained"] -BINDING_NAME_RECOUNT_FURY = L["Display"].." "..L["Fury Gained"] -BINDING_NAME_RECOUNT_PAIN = L["Display"].." "..L["Pain Gained"] +BINDING_NAME_RECOUNT_RESURRECT = LegacyBindingLabel("Ressers") +BINDING_NAME_RECOUNT_CCBREAKER = LegacyBindingLabel("CC Breakers") +BINDING_NAME_RECOUNT_MANA = LegacyBindingLabel("Mana Gained") +BINDING_NAME_RECOUNT_ENERGY = LegacyBindingLabel("Energy Gained") +BINDING_NAME_RECOUNT_RAGE = LegacyBindingLabel("Rage Gained") +BINDING_NAME_RECOUNT_RUNICPOWER = LegacyBindingLabel("Runic Power Gained") +BINDING_NAME_RECOUNT_LUNAR_POWER = LegacyBindingLabel("Astral Power Gained") +BINDING_NAME_RECOUNT_MAELSTROM = LegacyBindingLabel("Maelstorm Gained") +BINDING_NAME_RECOUNT_FURY = LegacyBindingLabel("Fury Gained") +BINDING_NAME_RECOUNT_PAIN = LegacyBindingLabel("Pain Gained") BINDING_NAME_RECOUNT_REPORT_MAIN = L["Report the Main Window Data"] BINDING_NAME_RECOUNT_REPORT_DETAILS = L["Report the Detail Window Data"] @@ -758,6 +766,9 @@ Recount.consoleOptions2.args.realtime = { name = L["HTPS"], desc = L["Tracks Raid Healing Taken Per Second"], type = 'execute', + hidden = function() + return USE_NATIVE_DAMAGE_METER + end, func = function() Recount:CreateRealtimeWindow("!RAID", "HEALINGTAKEN", "Raid HTPS") end @@ -1735,6 +1746,12 @@ function Recount:OnInitialize() Recount.db2 = RecountPerCharDB Recount.db2.char = nil -- Elsia: Dump old db data hard. Recount.db2.global = nil + if USE_NATIVE_DAMAGE_METER then + Recount.db.profile.Modules.HealingTaken = false + Recount.db.profile.Modules.OverhealingDone = false + Recount.db.profile.Modules.DOTUptime = false + Recount.db.profile.Modules.HOTUptime = false + end Recount:InitCombatData() Recount.LocalizeCombatants() self.db.RegisterCallback( self, "OnNewProfile", "HandleProfileChanges" ) diff --git a/Tracker_DamageMeter.lua b/Tracker_DamageMeter.lua index 0926fab..8aec2e8 100644 --- a/Tracker_DamageMeter.lua +++ b/Tracker_DamageMeter.lua @@ -80,6 +80,7 @@ local updateTicker = nil local IsSecret local SafeCombatCall local PROXY_PREFIX = "__RECOUNT_DM__" +local GENERIC_FIGHT_NAME = "Trash Combat" local overallBaseline = {} local TRACKED_TOTAL_FIELDS = { @@ -519,7 +520,7 @@ local function SnapshotSession(verbose) end if source.isLocalPlayer and Recount.FightingWho == "" then - Recount.FightingWho = "Combat" + Recount.FightingWho = GENERIC_FIGHT_NAME end end end @@ -613,7 +614,7 @@ local function UpdateTick() if found then if Recount.FightingWho == "" then - Recount.FightingWho = "Combat" + Recount.FightingWho = GENERIC_FIGHT_NAME end Recount.NewData = true if Recount.RefreshMainWindow then @@ -663,7 +664,7 @@ local function OnCombatEnd() ParseSessionFull() if Recount.FightingWho == "" then - Recount.FightingWho = "Combat" + Recount.FightingWho = GENERIC_FIGHT_NAME end DP("Calling LeaveCombat, FightingWho=" .. Recount.FightingWho) From 8ad2ac022603d30b69beed85374545c6287c20e4 Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Mon, 9 Mar 2026 11:33:03 +0200 Subject: [PATCH 12/15] Fix duplicate realtime party combatants --- Tracker_DamageMeter.lua | 146 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 144 insertions(+), 2 deletions(-) diff --git a/Tracker_DamageMeter.lua b/Tracker_DamageMeter.lua index 8aec2e8..7501028 100644 --- a/Tracker_DamageMeter.lua +++ b/Tracker_DamageMeter.lua @@ -23,6 +23,8 @@ local GetTime = GetTime local UnitName = UnitName local UnitClass = UnitClass local UnitLevel = UnitLevel +local UnitGroupRolesAssigned = UnitGroupRolesAssigned +local IsInRaid = IsInRaid local date = date local issecretvalue = issecretvalue local string_format = string.format @@ -96,6 +98,13 @@ local TRACKED_TOTAL_FIELDS = { "TimeHeal", } +local TRACKED_RATE_FIELDS = { + "DamagePerSecond", + "HealingPerSecond", + "AbsorbPerSecond", + "DamageTakenPerSecond", +} + -- Secret value display cache, keyed by mode then combatant name. -- Raw secret values are only used for UI text while synthetic numeric values drive sorting. local secretDisplayValues = {} @@ -145,6 +154,40 @@ local function GetOverallBaseline(who, field) return baseline[field] or 0 end +local function ResetSnapshotCombatant(who) + if not who or not who.Fights then + return + end + + local currentFight = who.Fights.CurrentFightData + local overallFight = who.Fights.OverallData + if not currentFight or not overallFight then + return + end + + for _, field in ipairs(TRACKED_TOTAL_FIELDS) do + currentFight[field] = 0 + overallFight[field] = GetOverallBaseline(who, field) + end + + for _, field in ipairs(TRACKED_RATE_FIELDS) do + currentFight[field] = 0 + overallFight[field] = 0 + end + + who.LastFightIn = nil +end + +local function ResetSnapshotData() + if not dbCombatants then + return + end + + for _, who in pairs(dbCombatants) do + ResetSnapshotCombatant(who) + end +end + local function StoreSecretValue(modeKey, combatantName, rawValue, rawPerSec, rawLabel) if not modeKey or not combatantName then return end if not IsSecret(rawValue) and not IsSecret(rawPerSec) then return end @@ -181,6 +224,43 @@ local function ClearProxyCombatants() end end +local function MoveNamedState(map, oldName, newName) + if not map or not oldName or not newName or oldName == newName then + return + end + + if map[oldName] and map[newName] == nil then + map[newName] = map[oldName] + end + map[oldName] = nil +end + +local function RenameCombatant(oldName, newName) + if not dbCombatants or not oldName or not newName or oldName == newName then + return dbCombatants and dbCombatants[newName] or nil + end + + local who = dbCombatants[oldName] + if not who then + return dbCombatants[newName] + end + + if dbCombatants[newName] and dbCombatants[newName] ~= who then + return dbCombatants[newName] + end + + dbCombatants[oldName] = nil + who.Name = newName + dbCombatants[newName] = who + + for _, modeData in pairs(secretDisplayValues) do + MoveNamedState(modeData, oldName, newName) + end + MoveNamedState(overallBaseline, oldName, newName) + + return who +end + -- Safe value access for secret values local function SafeNumber(val) if val == nil then return 0 end @@ -231,7 +311,11 @@ local function RefreshRosterCache() local playerName = UnitName("player") if playerName then local _, playerClass = UnitClass("player") - rosterCache[playerName] = { name = playerName, class = playerClass } + rosterCache[playerName] = { + name = playerName, + class = playerClass, + role = UnitGroupRolesAssigned and UnitGroupRolesAssigned("player") or nil, + } end -- Add group/raid members @@ -242,9 +326,50 @@ local function RefreshRosterCache() local uName = UnitName(unit) if uName then local _, uClass = UnitClass(unit) - rosterCache[uName] = { name = uName, class = uClass } + rosterCache[uName] = { + name = uName, + class = uClass, + role = UnitGroupRolesAssigned and UnitGroupRolesAssigned(unit) or nil, + } + end + end +end + +local function ResolveUniqueRosterName(source) + RefreshRosterCache() + + local desiredClass = GetEnClass(source.classFilename) + if desiredClass == "UNKNOWN" or desiredClass == "MOB" then + return nil + end + + local desiredRole = SafeString(source.role) + local playerName = UnitName("player") + local classMatch + local classCount = 0 + local roleMatch + local roleCount = 0 + + for name, rosterEntry in pairs(rosterCache) do + if name ~= playerName and rosterEntry.class == desiredClass then + classMatch = name + classCount = classCount + 1 + if desiredRole and desiredRole ~= "" and rosterEntry.role == desiredRole then + roleMatch = name + roleCount = roleCount + 1 + end end end + + if desiredRole and desiredRole ~= "" and roleCount == 1 then + return roleMatch + end + + if classCount == 1 then + return classMatch + end + + return nil end -- Resolve a combatant name from source, handling secret values @@ -258,6 +383,11 @@ local function ResolveName(source, orderIndex) return UnitName("player") end + local rosterName = ResolveUniqueRosterName(source) + if rosterName then + return rosterName + end + -- For other live group members, keep an opaque internal key and render the -- secret name directly in the UI. Realtime session rows are damage-sorted, so -- guessing from party order is unreliable and causes identity swaps. @@ -279,6 +409,17 @@ local function GetOrCreateCombatant(source, orderIndex) local guid = SafeString(source.sourceGUID) local classFilename = GetEnClass(source.classFilename) + if guid then + for existingName, existingCombatant in pairs(dbCombatants) do + if existingCombatant and existingCombatant.GUID == guid then + if existingName ~= name and not IsProxyCombatantName(name) then + return RenameCombatant(existingName, name) + end + return existingCombatant + end + end + end + if dbCombatants[name] then local who = dbCombatants[name] if guid and not who.GUID then @@ -456,6 +597,7 @@ local function SnapshotSession(verbose) local foundAny = false ClearSecretDisplayValues() + ResetSnapshotData() local session = GetSession(DM_DamageDone) if not session or not session.combatSources then From 3b1b6fefc3123d93194d7c2333a761303a7a3ae7 Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Mon, 9 Mar 2026 11:50:09 +0200 Subject: [PATCH 13/15] Fix Midnight realtime bar scaling --- GUI_Main.lua | 34 ++++++--- Tracker_DamageMeter.lua | 155 ++++++++++++++++++++++++++++++++-------- 2 files changed, 152 insertions(+), 37 deletions(-) diff --git a/GUI_Main.lua b/GUI_Main.lua index 49e094a..1caf23a 100644 --- a/GUI_Main.lua +++ b/GUI_Main.lua @@ -1276,14 +1276,32 @@ function Recount:RefreshMainWindow(datarefresh) end end - percent = 100 - if MaxValue ~= 0 then - percent = 100 * v[2] / MaxValue - end - me:SetBar(i, lefttext, righttext, percent, "Class", v[3], v[1], me.MainWindowSelectPlayer, v[4]) - me:FixRow(i) - rows[i].name = v[1] - if type(Recount.GetMainWindowBarLabelOverride) == "function" then + percent = 100 + if MaxValue ~= 0 then + percent = 100 * v[2] / MaxValue + end + me:SetBar(i, lefttext, righttext, percent, "Class", v[3], v[1], me.MainWindowSelectPlayer, v[4]) + if type(Recount.GetMainWindowBarValueOverride) == "function" then + local overrideValue, overrideMax = Recount:GetMainWindowBarValueOverride(v[4], Recount.db.profile.MainWindowMode) + if overrideValue ~= nil and overrideMax ~= nil then + local rowBar = rows[i].StatusBar + local okRange = pcall(rowBar.SetMinMaxValues, rowBar, 0, overrideMax) + local okValue = pcall(rowBar.SetValue, rowBar, overrideValue) + if not okRange or not okValue then + rowBar:SetMinMaxValues(0, 100) + rowBar:SetValue(percent) + end + else + rows[i].StatusBar:SetMinMaxValues(0, 100) + rows[i].StatusBar:SetValue(percent) + end + else + rows[i].StatusBar:SetMinMaxValues(0, 100) + rows[i].StatusBar:SetValue(percent) + end + me:FixRow(i) + rows[i].name = v[1] + if type(Recount.GetMainWindowBarLabelOverride) == "function" then local overrideLabel = Recount:GetMainWindowBarLabelOverride(v[4], Recount.db.profile.MainWindowMode, i + offset) if overrideLabel then rows[i].LeftText:SetText(overrideLabel) diff --git a/Tracker_DamageMeter.lua b/Tracker_DamageMeter.lua index 7501028..5d51921 100644 --- a/Tracker_DamageMeter.lua +++ b/Tracker_DamageMeter.lua @@ -108,6 +108,7 @@ local TRACKED_RATE_FIELDS = { -- Secret value display cache, keyed by mode then combatant name. -- Raw secret values are only used for UI text while synthetic numeric values drive sorting. local secretDisplayValues = {} +local secretBarValues = {} local function ClearSecretDisplayValues() for _, modeData in pairs(secretDisplayValues) do @@ -115,6 +116,16 @@ local function ClearSecretDisplayValues() end end +local function ClearSecretBarValues() + for _, modeData in pairs(secretBarValues) do + if modeData.entries then + wipe(modeData.entries) + end + modeData.maxValue = nil + modeData.maxPerSec = nil + end +end + local function ClearOverallBaseline() wipe(overallBaseline) end @@ -205,11 +216,48 @@ local function StoreSecretValue(modeKey, combatantName, rawValue, rawPerSec, raw } end +local function StoreSecretBarValue(modeKey, combatantName, rawValue, rawPerSec) + if not modeKey or not combatantName then + return + end + + local modeData = secretBarValues[modeKey] + if not modeData then + modeData = { entries = {} } + secretBarValues[modeKey] = modeData + end + + modeData.entries[combatantName] = { + value = rawValue, + perSec = rawPerSec, + } +end + +local function SetSecretBarScale(modeKey, rawMaxValue, rawMaxPerSec) + if not modeKey then + return + end + + local modeData = secretBarValues[modeKey] + if not modeData then + modeData = { entries = {} } + secretBarValues[modeKey] = modeData + end + + modeData.maxValue = rawMaxValue + modeData.maxPerSec = rawMaxPerSec +end + local function GetSecretValue(modeKey, combatantName) local modeData = secretDisplayValues[modeKey] return modeData and modeData[combatantName] or nil end +local function GetSecretBarValue(modeKey, combatantName) + local modeData = secretBarValues[modeKey] + return modeData and modeData.entries and modeData.entries[combatantName] or nil +end + local function IsProxyCombatantName(name) return type(name) == "string" and string_find(name, "^" .. PROXY_PREFIX) ~= nil end @@ -256,6 +304,11 @@ local function RenameCombatant(oldName, newName) for _, modeData in pairs(secretDisplayValues) do MoveNamedState(modeData, oldName, newName) end + for _, modeData in pairs(secretBarValues) do + if modeData and modeData.entries then + MoveNamedState(modeData.entries, oldName, newName) + end + end MoveNamedState(overallBaseline, oldName, newName) return who @@ -597,6 +650,7 @@ local function SnapshotSession(verbose) local foundAny = false ClearSecretDisplayValues() + ClearSecretBarValues() ResetSnapshotData() local session = GetSession(DM_DamageDone) @@ -606,6 +660,7 @@ local function SnapshotSession(verbose) end if verbose then DP(" DamageDone: " .. #session.combatSources .. " sources") end + SetSecretBarScale("Damage", session.maxAmount, session.combatSources[1] and session.combatSources[1].amountPerSecond or nil) if session.durationSeconds then sessionDuration = GetDisplayNumber(session.durationSeconds, 1) @@ -652,11 +707,12 @@ local function SnapshotSession(verbose) who.LastFightIn = Recount.db2.FightNum foundAny = true - if who.Name then - StoreSecretValue("Damage", who.Name, source.totalAmount, source.amountPerSecond, source.name) - if verbose and (IsSecret(source.totalAmount) or IsSecret(source.amountPerSecond)) then - DP(" Stored damage secrets for: " .. who.Name) - end + if who.Name then + StoreSecretValue("Damage", who.Name, source.totalAmount, source.amountPerSecond, source.name) + StoreSecretBarValue("Damage", who.Name, source.totalAmount, source.amountPerSecond) + if verbose and (IsSecret(source.totalAmount) or IsSecret(source.amountPerSecond)) then + DP(" Stored damage secrets for: " .. who.Name) + end elseif verbose and (IsSecret(source.totalAmount) or IsSecret(source.amountPerSecond)) then DP(" who.Name is nil, cannot store damage secrets") end @@ -688,6 +744,9 @@ local function SnapshotSession(verbose) local function ProcessType(dmType, dataField, rateField, secretKey) local s = GetSession(dmType) if s and s.combatSources then + if secretKey then + SetSecretBarScale(secretKey, s.maxAmount, s.combatSources[1] and s.combatSources[1].amountPerSecond or nil) + end for idx, source in ipairs(s.combatSources) do local who = GetOrCreateCombatant(source, idx) if who then @@ -696,12 +755,13 @@ local function SnapshotSession(verbose) if amount > 0 or perSec > 0 then SetTrackedValue(who, dataField, amount, rateField, perSec) who.LastFightIn = Recount.db2.FightNum - foundAny = true - if who.Name and secretKey then - StoreSecretValue(secretKey, who.Name, source.totalAmount, source.amountPerSecond, source.name) + foundAny = true + if who.Name and secretKey then + StoreSecretValue(secretKey, who.Name, source.totalAmount, source.amountPerSecond, source.name) + StoreSecretBarValue(secretKey, who.Name, source.totalAmount, source.amountPerSecond) + end end end - end end end end @@ -1009,19 +1069,6 @@ function Recount:GetMainWindowBarTextOverride(combatant, modeIndex) if healingValueText then local valueText = absorbValueText and string_format("%s + %s", healingValueText, absorbValueText) or healingValueText - local barText = self.db and self.db.profile and self.db.profile.MainWindow and self.db.profile.MainWindow.BarText - if barText and barText.PerSec then - local healingPerSecText = FormatRealtimeValue(healingEntry and healingEntry.perSec) - local absorbPerSecText = FormatRealtimeValue(absorbEntry and absorbEntry.perSec) - if healingEntry and absorbEntry and healingPerSecText and absorbPerSecText and not IsSecret(healingEntry.perSec) and not IsSecret(absorbEntry.perSec) then - healingPerSecText = Recount:FormatLongNums(SafeNumber(healingEntry.perSec) + SafeNumber(absorbEntry.perSec)) - absorbPerSecText = nil - end - if healingPerSecText then - local perSecText = absorbPerSecText and string_format("%s + %s", healingPerSecText, absorbPerSecText) or healingPerSecText - return string_format("%s (%s)", valueText, perSecText) - end - end return valueText end end @@ -1041,15 +1088,65 @@ function Recount:GetMainWindowBarTextOverride(combatant, modeIndex) return valueText end - local barText = self.db and self.db.profile and self.db.profile.MainWindow and self.db.profile.MainWindow.BarText - if barText and barText.PerSec then - local perSecText = FormatRealtimeValue(entry.perSec) - if perSecText then - return string_format("%s (%s)", valueText, perSecText) - end + return valueText +end + +function Recount:GetMainWindowBarValueOverride(combatant, modeIndex) + if not self.UseDamageMeter or not self.InCombat then + return nil end - return valueText + local combatantName = type(combatant) == "table" and combatant.Name or combatant + if not combatantName then + return nil + end + + local modeData = self.MainWindowData and self.MainWindowData[modeIndex] + local modeName = modeData and modeData[1] + local modeCategory = modeData and modeData[7] + + if modeCategory == "Healing" and self.db and self.db.profile and self.db.profile.MergeAbsorbs then + return nil + end + + local modeKey + local useRate = false + if modeName == L["DPS"] then + modeKey = "Damage" + useRate = true + elseif modeCategory == "Damage" then + modeKey = "Damage" + elseif modeCategory == "Healing" then + modeKey = "Healing" + elseif modeCategory == "DamageTaken" then + modeKey = "DamageTaken" + elseif modeName == L["Absorbs"] then + modeKey = "Absorbs" + elseif modeName == L["Interrupts"] then + modeKey = "Interrupts" + elseif modeName == L["Dispels"] then + modeKey = "Dispels" + elseif modeName == L["Deaths"] then + modeKey = "Deaths" + end + + if not modeKey then + return nil + end + + local modeBarData = secretBarValues[modeKey] + local entry = GetSecretBarValue(modeKey, combatantName) + if not modeBarData or not entry then + return nil + end + + local value = useRate and entry.perSec or entry.value + local maxValue = useRate and modeBarData.maxPerSec or modeBarData.maxValue + if value == nil or maxValue == nil then + return nil + end + + return value, maxValue end SafeCombatCall = function(context, func) From e889189883293cf4c46f9d843c1a377d21b3c40b Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Mon, 9 Mar 2026 12:18:30 +0200 Subject: [PATCH 14/15] Sort Midnight realtime bars by live rank --- GUI_Main.lua | 13 +++++ Tracker_DamageMeter.lua | 125 +++++++++++++++++++++------------------- 2 files changed, 79 insertions(+), 59 deletions(-) diff --git a/GUI_Main.lua b/GUI_Main.lua index 1caf23a..df19703 100644 --- a/GUI_Main.lua +++ b/GUI_Main.lua @@ -901,6 +901,19 @@ end --Actual Data Functions local function sortFunc(a, b) + if Recount and Recount.UseDamageMeter and Recount.InCombat and type(Recount.GetMainWindowSortRankOverride) == "function" and Recount.db and Recount.db.profile then + local modeIndex = Recount.db.profile.MainWindowMode + local rankA = Recount:GetMainWindowSortRankOverride(a[4], modeIndex) + local rankB = Recount:GetMainWindowSortRankOverride(b[4], modeIndex) + if rankA and rankB and rankA ~= rankB then + return rankA < rankB + elseif rankA and not rankB then + return true + elseif rankB and not rankA then + return false + end + end + if a[2] > b[2] then return true elseif a[2] == b[2] then diff --git a/Tracker_DamageMeter.lua b/Tracker_DamageMeter.lua index 5d51921..753cf88 100644 --- a/Tracker_DamageMeter.lua +++ b/Tracker_DamageMeter.lua @@ -216,7 +216,7 @@ local function StoreSecretValue(modeKey, combatantName, rawValue, rawPerSec, raw } end -local function StoreSecretBarValue(modeKey, combatantName, rawValue, rawPerSec) +local function StoreSecretBarValue(modeKey, combatantName, rawValue, rawPerSec, rank) if not modeKey or not combatantName then return end @@ -230,6 +230,7 @@ local function StoreSecretBarValue(modeKey, combatantName, rawValue, rawPerSec) modeData.entries[combatantName] = { value = rawValue, perSec = rawPerSec, + rank = rank, } end @@ -258,6 +259,38 @@ local function GetSecretBarValue(modeKey, combatantName) return modeData and modeData.entries and modeData.entries[combatantName] or nil end +local function GetMainWindowModeKey(modeData) + local modeName = modeData and modeData[1] + local modeCategory = modeData and modeData[7] + + if modeName == L["DPS"] then + return "Damage", true + end + if modeCategory == "Damage" then + return "Damage", false + end + if modeCategory == "Healing" then + return "Healing", false + end + if modeCategory == "DamageTaken" then + return "DamageTaken", false + end + if modeName == L["Absorbs"] then + return "Absorbs", false + end + if modeName == L["Interrupts"] then + return "Interrupts", false + end + if modeName == L["Dispels"] then + return "Dispels", false + end + if modeName == L["Deaths"] then + return "Deaths", false + end + + return nil, false +end + local function IsProxyCombatantName(name) return type(name) == "string" and string_find(name, "^" .. PROXY_PREFIX) ~= nil end @@ -709,7 +742,7 @@ local function SnapshotSession(verbose) if who.Name then StoreSecretValue("Damage", who.Name, source.totalAmount, source.amountPerSecond, source.name) - StoreSecretBarValue("Damage", who.Name, source.totalAmount, source.amountPerSecond) + StoreSecretBarValue("Damage", who.Name, source.totalAmount, source.amountPerSecond, i) if verbose and (IsSecret(source.totalAmount) or IsSecret(source.amountPerSecond)) then DP(" Stored damage secrets for: " .. who.Name) end @@ -758,7 +791,7 @@ local function SnapshotSession(verbose) foundAny = true if who.Name and secretKey then StoreSecretValue(secretKey, who.Name, source.totalAmount, source.amountPerSecond, source.name) - StoreSecretBarValue(secretKey, who.Name, source.totalAmount, source.amountPerSecond) + StoreSecretBarValue(secretKey, who.Name, source.totalAmount, source.amountPerSecond, idx) end end end @@ -978,42 +1011,12 @@ local function GetMainWindowSecretEntry(modeData, combatantName) return nil, nil end - local modeName = modeData[1] - local modeCategory = modeData[7] - - if modeName == L["DPS"] then - return GetSecretValue("Damage", combatantName), true - end - - if modeCategory == "Damage" then - return GetSecretValue("Damage", combatantName), false - end - - if modeCategory == "Healing" then - return GetSecretValue("Healing", combatantName), false - end - - if modeCategory == "DamageTaken" then - return GetSecretValue("DamageTaken", combatantName), false - end - - if modeName == L["Absorbs"] then - return GetSecretValue("Absorbs", combatantName), false - end - - if modeName == L["Interrupts"] then - return GetSecretValue("Interrupts", combatantName), false - end - - if modeName == L["Dispels"] then - return GetSecretValue("Dispels", combatantName), false - end - - if modeName == L["Deaths"] then - return GetSecretValue("Deaths", combatantName), false + local modeKey, useRate = GetMainWindowModeKey(modeData) + if not modeKey then + return nil, nil end - return nil, nil + return GetSecretValue(modeKey, combatantName), useRate end function Recount:GetMainWindowBarLabelOverride(combatant, modeIndex, rank) @@ -1102,34 +1105,13 @@ function Recount:GetMainWindowBarValueOverride(combatant, modeIndex) end local modeData = self.MainWindowData and self.MainWindowData[modeIndex] - local modeName = modeData and modeData[1] local modeCategory = modeData and modeData[7] if modeCategory == "Healing" and self.db and self.db.profile and self.db.profile.MergeAbsorbs then return nil end - local modeKey - local useRate = false - if modeName == L["DPS"] then - modeKey = "Damage" - useRate = true - elseif modeCategory == "Damage" then - modeKey = "Damage" - elseif modeCategory == "Healing" then - modeKey = "Healing" - elseif modeCategory == "DamageTaken" then - modeKey = "DamageTaken" - elseif modeName == L["Absorbs"] then - modeKey = "Absorbs" - elseif modeName == L["Interrupts"] then - modeKey = "Interrupts" - elseif modeName == L["Dispels"] then - modeKey = "Dispels" - elseif modeName == L["Deaths"] then - modeKey = "Deaths" - end - + local modeKey, useRate = GetMainWindowModeKey(modeData) if not modeKey then return nil end @@ -1149,6 +1131,31 @@ function Recount:GetMainWindowBarValueOverride(combatant, modeIndex) return value, maxValue end +function Recount:GetMainWindowSortRankOverride(combatant, modeIndex) + if not self.UseDamageMeter or not self.InCombat then + return nil + end + + local combatantName = type(combatant) == "table" and combatant.Name or combatant + if not combatantName then + return nil + end + + local modeData = self.MainWindowData and self.MainWindowData[modeIndex] + local modeCategory = modeData and modeData[7] + if modeCategory == "Healing" and self.db and self.db.profile and self.db.profile.MergeAbsorbs then + return nil + end + + local modeKey = GetMainWindowModeKey(modeData) + if not modeKey then + return nil + end + + local entry = GetSecretBarValue(modeKey, combatantName) + return entry and entry.rank or nil +end + SafeCombatCall = function(context, func) local ok, err = xpcall(func, function(message) return SafeDebugText(message) From 9e32d53eb4686bd12dfdbfa53488231def33d3b5 Mon Sep 17 00:00:00 2001 From: Radu Ursache Date: Mon, 9 Mar 2026 12:31:18 +0200 Subject: [PATCH 15/15] Hide stale rows before Midnight live updates --- GUI_Main.lua | 79 +++++++++++++++++++++++++---------------- Tracker_DamageMeter.lua | 24 +++++++++++++ 2 files changed, 73 insertions(+), 30 deletions(-) diff --git a/GUI_Main.lua b/GUI_Main.lua index df19703..0a9443f 100644 --- a/GUI_Main.lua +++ b/GUI_Main.lua @@ -1120,6 +1120,8 @@ function Recount:RefreshMainWindow(datarefresh) local Total = 0 local TotalPerSec = 0 local Value, PerSec + local liveCombatOnly = Recount.UseDamageMeter and Recount.InCombat and type(Recount.HasMainWindowLiveEntry) == "function" + local modeIndex = Recount.db and Recount.db.profile and Recount.db.profile.MainWindowMode if type(Recount.MainWindowData[Recount.db.profile.MainWindowMode][6]) == "function" then MainWindow.Title:SetText(Recount.MainWindowData[Recount.db.profile.MainWindowMode][6]()) @@ -1153,37 +1155,39 @@ function Recount:RefreshMainWindow(datarefresh) if v and v.type and FiltersShow[v.type] and not (v.type == "Pet" and Recount.db.profile.MergePets and v.Owner and Combatants[v.Owner] and not FiltersShow[Combatants[v.Owner].type]) then -- Elsia: Added owner inheritance filtering for pets if v.Fights and v.Fights[Recount.db.profile.CurDataSet] then Value, PerSec = MainWindow:GetData(v, 1) + else + Value = 0 + end - if Value > 0 then - if v.type ~= "Pet" or not Recount.db.profile.MergePets then -- Elsia: Only add to total if not merging pets. - Total = Total + Value - if type(PerSec) == "number" then - TotalPerSec = TotalPerSec + PerSec - end + if Value > 0 then + if v.type ~= "Pet" or not Recount.db.profile.MergePets then -- Elsia: Only add to total if not merging pets. + Total = Total + Value + if type(PerSec) == "number" then + TotalPerSec = TotalPerSec + PerSec end + end - if type(lookup[k]) == "table" then - if Value ~= lookup[k][2] then - lookup[k][1] = k - lookup[k][2] = Value - lookup[k][3] = v.enClass -- ClassColors[v.enClass] - lookup[k][4] = v - lookup[k][5] = PerSec - noUpdates = false - end - else - lookup[k] = {k, Value, v.enClass, v, PerSec} -- Recount.Colors:GetColor("Class",v.enClass) - tinsert(dispTable, lookup[k]) + if type(lookup[k]) == "table" then + if Value ~= lookup[k][2] then + lookup[k][1] = k + lookup[k][2] = Value + lookup[k][3] = v.enClass -- ClassColors[v.enClass] + lookup[k][4] = v + lookup[k][5] = PerSec noUpdates = false end - elseif type(lookup[k]) == "table" then - lookup[k] = nil + else + lookup[k] = {k, Value, v.enClass, v, PerSec} -- Recount.Colors:GetColor("Class",v.enClass) + tinsert(dispTable, lookup[k]) + noUpdates = false + end + elseif type(lookup[k]) == "table" then + lookup[k] = nil - for k2, v2 in ipairs(dispTable) do - if v2[1] == k then - tremove(dispTable, k2) - break - end + for k2, v2 in ipairs(dispTable) do + if v2[1] == k then + tremove(dispTable, k2) + break end end end @@ -1198,12 +1202,27 @@ function Recount:RefreshMainWindow(datarefresh) MaxValue = dispTable[1][2] end + local visibleDispTable = dispTable + if liveCombatOnly then + visibleDispTable = {} + for _, entry in ipairs(dispTable) do + if Recount:HasMainWindowLiveEntry(entry[4], modeIndex) then + tinsert(visibleDispTable, entry) + end + end + if #visibleDispTable > 0 then + MaxValue = visibleDispTable[1][2] + else + MaxValue = 0 + end + end + local RowWidth = MainWindow:GetWidth() - 4 - if #(dispTable) > MainWindow.CurRows and MainWindow_Settings.ShowScrollbar == true then + if #(visibleDispTable) > MainWindow.CurRows and MainWindow_Settings.ShowScrollbar == true then RowWidth = MainWindow:GetWidth() - 23 end - FauxScrollFrame_Update(MainWindow.ScrollBar, #(dispTable), Recount.MainWindow.CurRows, 20) + FauxScrollFrame_Update(MainWindow.ScrollBar, #(visibleDispTable), Recount.MainWindow.CurRows, 20) local offset = FauxScrollFrame_GetOffset(MainWindow.ScrollBar) if type(MainWindow.SpecialTotal) == "function" then @@ -1216,7 +1235,7 @@ function Recount:RefreshMainWindow(datarefresh) local MainWindow_BarText_PerSec = MainWindow_Settings.BarText.PerSec local MainWindow_BarText_Percent = MainWindow_Settings.BarText.Percent - if not MainWindow_Settings.HideTotalBar and MainWindow.CurRows > 0 and Total > 0 then + if not liveCombatOnly and not MainWindow_Settings.HideTotalBar and MainWindow.CurRows > 0 and Total > 0 then if TotalPerSec > 0 then PerSec = Recount:FormatLongNums(TotalPerSec) --PerSec = string_format("%.1f", TotalPerSec) @@ -1252,8 +1271,8 @@ function Recount:RefreshMainWindow(datarefresh) end end - for i = 1, MainWindow.CurRows do - local v = dispTable[i + offset] + for i = 1, MainWindow.CurRows do + local v = visibleDispTable[i + offset] if v then local percent = 100 diff --git a/Tracker_DamageMeter.lua b/Tracker_DamageMeter.lua index 753cf88..0deba23 100644 --- a/Tracker_DamageMeter.lua +++ b/Tracker_DamageMeter.lua @@ -1131,6 +1131,30 @@ function Recount:GetMainWindowBarValueOverride(combatant, modeIndex) return value, maxValue end +function Recount:HasMainWindowLiveEntry(combatant, modeIndex) + if not self.UseDamageMeter or not self.InCombat then + return false + end + + local combatantName = type(combatant) == "table" and combatant.Name or combatant + if not combatantName then + return false + end + + local modeData = self.MainWindowData and self.MainWindowData[modeIndex] + local modeCategory = modeData and modeData[7] + if modeCategory == "Healing" and self.db and self.db.profile and self.db.profile.MergeAbsorbs then + return GetSecretBarValue("Healing", combatantName) ~= nil or GetSecretBarValue("Absorbs", combatantName) ~= nil + end + + local modeKey = GetMainWindowModeKey(modeData) + if not modeKey then + return false + end + + return GetSecretBarValue(modeKey, combatantName) ~= nil +end + function Recount:GetMainWindowSortRankOverride(combatant, modeIndex) if not self.UseDamageMeter or not self.InCombat then return nil