diff --git a/GUI_Config.lua b/GUI_Config.lua index 1f51f65..97fe4b1 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) @@ -260,6 +264,7 @@ end function me:CreateWindowModuleSelection(parent) me.ModuleOptions = CreateFrame("Frame", nil, parent) + local useNativeDamageMeter = C_DamageMeter ~= nil local theFrame = me.ModuleOptions @@ -302,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) @@ -347,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) @@ -361,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) @@ -750,7 +773,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 +1032,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 +1094,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 +1129,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) @@ -1346,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) @@ -1394,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 a7a1dbd..4ef82a2 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 @@ -431,7 +431,7 @@ function me:RefreshDeathDetails() local size if Data then - size = table.getn(Data) + size = #(Data) else size = 0 end @@ -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/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..0a9443f 100644 --- a/GUI_Main.lua +++ b/GUI_Main.lua @@ -30,8 +30,15 @@ local GetScreenWidth = GetScreenWidth local IsAltKeyDown = IsAltKeyDown local IsControlKeyDown = IsControlKeyDown local IsShiftKeyDown = IsShiftKeyDown -local SendChatMessage = SendChatMessage -local UIFrameFade = UIFrameFade +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) + end + if fadeInfo.finishedFunc then + fadeInfo.finishedFunc(fadeInfo.finishedArg1) + end +end local GameTooltip = GameTooltip local UIParent = UIParent @@ -473,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 @@ -885,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 @@ -1091,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]()) @@ -1124,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 @@ -1169,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 table.getn(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, table.getn(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 @@ -1187,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) @@ -1223,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 @@ -1253,14 +1301,44 @@ 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 - percent = 100 * v[2] / MaxValue + 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) + end end - me:SetBar(i, lefttext, righttext, percent, "Class", v[3], v[1], me.MainWindowSelectPlayer, v[4]) - me:FixRow(i) - rows[i].name = v[1] else rows[i]:Hide() end @@ -1346,7 +1424,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, @@ -1479,7 +1557,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 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/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 diff --git a/Recount.lua b/Recount.lua index 9a42abe..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 @@ -53,7 +61,6 @@ local UnitName = UnitName local CreateFrame = CreateFrame -local InterfaceOptionsFrame = InterfaceOptionsFrame local UIParent = UIParent local RecountTempTooltip = RecountTempTooltip @@ -209,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 = { @@ -331,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"] @@ -759,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 @@ -909,11 +919,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 @@ -1602,6 +1616,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() @@ -1729,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" ) @@ -1827,8 +1850,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 17b49c9..0c1299b 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: Записывает урон и исцеления и отоброжает различные графики. @@ -37,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/Recount_Modes.lua b/Recount_Modes.lua index 831c841..236c697 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 @@ -663,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() @@ -778,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.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/Tracker_DamageMeter.lua b/Tracker_DamageMeter.lua new file mode 100644 index 0000000..0deba23 --- /dev/null +++ b/Tracker_DamageMeter.lua @@ -0,0 +1,1193 @@ +-- 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 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 + +local pairs = pairs +local ipairs = ipairs +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 +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) + 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 +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 +local IsSecret +local SafeCombatCall +local PROXY_PREFIX = "__RECOUNT_DM__" +local GENERIC_FIGHT_NAME = "Trash Combat" +local overallBaseline = {} + +local TRACKED_TOTAL_FIELDS = { + "Damage", + "Healing", + "Absorbs", + "DamageTaken", + "Interrupts", + "Dispels", + "DeathCount", + "ActiveTime", + "TimeDamage", + "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 = {} +local secretBarValues = {} + +local function ClearSecretDisplayValues() + for _, modeData in pairs(secretDisplayValues) do + wipe(modeData) + 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 + +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 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 + + local modeData = secretDisplayValues[modeKey] + if not modeData then + modeData = {} + secretDisplayValues[modeKey] = modeData + end + + modeData[combatantName] = { + value = rawValue, + perSec = rawPerSec, + label = rawLabel, + } +end + +local function StoreSecretBarValue(modeKey, combatantName, rawValue, rawPerSec, rank) + 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, + rank = rank, + } +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 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 + +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 + +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 + 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 +end + +-- 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 + +IsSecret = function(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, + role = UnitGroupRolesAssigned and UnitGroupRolesAssigned("player") or nil, + } + 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, + 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 +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 + + 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. + local guid = SafeString(source.sourceGUID) + if guid then + return PROXY_PREFIX .. guid + end + + return PROXY_PREFIX .. tostring(orderIndex or 0) +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 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 + 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, 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 + 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 + +local function ApplySpellBreakdown(fightData, sourceData, datatypeAttacks) + if not fightData or not datatypeAttacks or not sourceData or not sourceData.combatSpells then + return 0 + end + + fightData[datatypeAttacks] = fightData[datatypeAttacks] or {} + wipe(fightData[datatypeAttacks]) + + 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) + + 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 + +-- Snapshot current DM data into CurrentFightData (replace, not accumulate) +local function SnapshotSession(verbose) + local sessionDuration = 0 + local foundAny = false + + ClearSecretDisplayValues() + ClearSecretBarValues() + ResetSnapshotData() + + 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 + SetSecretBarScale("Damage", session.maxAmount, session.combatSources[1] and session.combatSources[1].amountPerSecond or nil) + + 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 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) + 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) + local dps = GetDisplayNumber(source.amountPerSecond, i) + if amount > 0 or dps > 0 then + SetTrackedValue(who, "Damage", amount, "DamagePerSecond", dps) + who.LastFightIn = Recount.db2.FightNum + foundAny = true + + if who.Name then + StoreSecretValue("Damage", who.Name, source.totalAmount, source.amountPerSecond, source.name) + 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 + 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 = GENERIC_FIGHT_NAME + 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 + 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 + + 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 + local amount = GetDisplayNumber(source.totalAmount, idx) + local perSec = rateField and GetDisplayNumber(source.amountPerSecond, idx) or 0 + 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) + StoreSecretBarValue(secretKey, who.Name, source.totalAmount, source.amountPerSecond, idx) + end + end + end + end + end + end + + 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 +end + +-- Full parse: final snapshot + spell breakdowns +local function ParseSessionFull() + DP("ParseSessionFull: combatSessionID=" .. tostring(combatSessionID)) + local foundAny = SnapshotSession(true) + + if foundAny then + -- 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() + SafeCombatCall("UpdateTick", function() + 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 = GENERIC_FIGHT_NAME + end + Recount.NewData = true + if Recount.RefreshMainWindow then + Recount:RefreshMainWindow() + end + 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)) + + -- 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", "DamagePerSecond", "Healing", "HealingPerSecond", "Absorbs", "AbsorbPerSecond", "DamageTaken", "DamageTakenPerSecond", "Interrupts", "Dispels", "DeathCount", "ActiveTime", "TimeDamage", "TimeHeal"}) do + who.Fights.CurrentFightData[f] = 0 + end + end + end + + ParseSessionFull() + + if Recount.FightingWho == "" then + Recount.FightingWho = GENERIC_FIGHT_NAME + 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 + ClearSecretDisplayValues() + ClearProxyCombatants() + Recount:PutInCombat() + CaptureOverallBaseline() + 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") + ClearOverallBaseline() + 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") + + DP("Init complete") +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 + ClearSecretDisplayValues() + ClearProxyCombatants() + ClearOverallBaseline() + 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 modeKey, useRate = GetMainWindowModeKey(modeData) + if not modeKey then + return nil, nil + end + + return GetSecretValue(modeKey, combatantName), useRate +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 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 + return valueText + end + end + end + + 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 + + return valueText +end + +function Recount:GetMainWindowBarValueOverride(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, useRate = GetMainWindowModeKey(modeData) + 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 + +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 + 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) + end) + + if not ok then + DE(context .. " error: " .. SafeDebugText(err)) + end + + return ok +end 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/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) 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 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