From 97df6c9a98e9c1b8d6a86f5a408c4780d95135d2 Mon Sep 17 00:00:00 2001 From: Bluewhale1337 <295648290+Bluewhale1337@users.noreply.github.com> Date: Sun, 4 Oct 2026 19:24:34 +0200 Subject: [PATCH] feat: add Nampower integration for player HoT timers display for active auras --- HealBot_Controller_Aura.lua | 26 ++++++ HealBot_Controller_Comms.lua | 12 ++- HealBot_Controller_Events.lua | 10 ++ HealBot_Data.lua | 1 + HealBot_Integrations.lua | 21 ++++- HealBot_Model.lua | 4 +- HealBot_Options_Integrations.lua | 1 + HealBot_Options_Integrations.xml | 31 ++++++- HealBot_View_Layout.lua | 152 +++++++++++++++++++++++++++++++ README.md | 2 + 10 files changed, 254 insertions(+), 6 deletions(-) diff --git a/HealBot_Controller_Aura.lua b/HealBot_Controller_Aura.lua index c815279..5efcad9 100644 --- a/HealBot_Controller_Aura.lua +++ b/HealBot_Controller_Aura.lua @@ -272,9 +272,11 @@ function HealBot_OnEvent_UnitAura(this, unit) end local b = 1 + local activeBuffs = HealBot_GetTable() while true do local buff = UnitBuff(unit, b) if not buff then break end + activeBuffs[buff] = true if HealBot_TrackedHoTs[buff] and iconCount < 10 then iconCount = iconCount + 1 HealBot_UnitIcons[unit][iconCount] = buff @@ -282,6 +284,30 @@ function HealBot_OnEvent_UnitAura(this, unit) b = b + 1 end + if HealBot_Integrations_Nampower_Active and HealBot_Nampower_Auras then + local guid = HealBot_GetUnitGUID and HealBot_GetUnitGUID(unit) + if guid then + if not HealBot_Nampower_NameCache then HealBot_Nampower_NameCache = HealBot_GetTable() end + HealBot_Nampower_NameCache[guid] = UnitName(unit) + + if HealBot_Nampower_Auras[guid] then + local validHoTs = { + ["Renew"] = "Interface\\Icons\\Spell_Holy_Renew", + ["Rejuvenation"] = "Interface\\Icons\\Spell_Nature_Rejuvenation", + ["Regrowth"] = "Interface\\Icons\\Spell_Nature_ResistNature", + ["Power Word: Shield"] = "Interface\\Icons\\Spell_Holy_PowerWordShield", + } + for spellName, _ in pairs(HealBot_Nampower_Auras[guid]) do + local tex = validHoTs[spellName] + if tex and not activeBuffs[tex] then + HealBot_Nampower_Auras[guid][spellName] = nil + end + end + end + end + end + HealBot_ReleaseTable(activeBuffs) + local d = 1 while true do local debuff = UnitDebuff(unit, d) diff --git a/HealBot_Controller_Comms.lua b/HealBot_Controller_Comms.lua index dded2b2..2267662 100644 --- a/HealBot_Controller_Comms.lua +++ b/HealBot_Controller_Comms.lua @@ -143,9 +143,17 @@ function HealBot_OnEvent_AddonMsg(this, addon_id, inc_msg, dist_target, sender_i -- Clear reusable args array for i=1, 10 do HealBot_Comms_Args[i] = nil end local argCount = 0 - for word in string.gfind(inc_msg, "[^/]+") do + local start_pos = 1 + local next_pos = string.find(inc_msg, "/", start_pos, true) + while next_pos do argCount = argCount + 1 - HealBot_Comms_Args[argCount] = word + HealBot_Comms_Args[argCount] = string.sub(inc_msg, start_pos, next_pos - 1) + start_pos = next_pos + 1 + next_pos = string.find(inc_msg, "/", start_pos, true) + end + if start_pos <= string.len(inc_msg) then + argCount = argCount + 1 + HealBot_Comms_Args[argCount] = string.sub(inc_msg, start_pos) end local cmd = HealBot_Comms_Args[1] diff --git a/HealBot_Controller_Events.lua b/HealBot_Controller_Events.lua index c096839..5682a44 100644 --- a/HealBot_Controller_Events.lua +++ b/HealBot_Controller_Events.lua @@ -182,6 +182,16 @@ function HealBot_OnUpdate(this, arg1) HealBot_RecalcSpells(); end end + if HealBot_Config.HealBot_Integrations_ShowHoTTimers == 1 then + if not HealBot_HoT_Update_Timer or GetTime() >= HealBot_HoT_Update_Timer then + HealBot_HoT_Update_Timer = GetTime() + 0.25 + if HealBot_Action_UpdatePlayerHoTs then + HealBot_Action_UpdatePlayerHoTs() + end + end + elseif HealBot_PlayerHoTsListFrame and HealBot_PlayerHoTsListFrame:IsVisible() then + HealBot_PlayerHoTsListFrame:Hide() + end HealBot_Timer1 = HealBot_Timer1 + arg1; if HealBot_Timer1 >= 2.5 then diff --git a/HealBot_Data.lua b/HealBot_Data.lua index 5894ef4..a4a4c8a 100644 --- a/HealBot_Data.lua +++ b/HealBot_Data.lua @@ -261,6 +261,7 @@ HealBot_ConfigDefaults = { HealBot_Integrations_Nampower = 0, HealBot_Integrations_SuperWoW = 0, HealBot_Integrations_ClassicAPI = 0, + HealBot_Integrations_ShowHoTTimers = 0, }; HealBot_Config = {}; diff --git a/HealBot_Integrations.lua b/HealBot_Integrations.lua index 42ac6c2..65a34a7 100644 --- a/HealBot_Integrations.lua +++ b/HealBot_Integrations.lua @@ -10,6 +10,8 @@ function HealBot_GetUnitGUID(unit) return UnitGUID(unit) elseif HealBot_Integrations_SuperWoW_Active and GetUnitGUID then return GetUnitGUID(unit) + elseif HealBot_Integrations_Nampower_Active and GetUnitGUID then + return GetUnitGUID(unit) end return nil end @@ -40,6 +42,11 @@ function HealBot_Integrations_Toggle() local spellID, caster, target, _, _, _, _, duration = arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8 if not caster or not target or duration <= 0 then return end + -- The player is ALWAYS the caster in these events for Nampower + if not HealBot_PlayerGUID then + HealBot_PlayerGUID = caster + end + local spellName = GetSpellRecField(spellID, "name") if not spellName then return end @@ -76,7 +83,7 @@ function HealBot_Integrations_PruneNampower() local currentTime = GetTime() for targetName, auras in pairs(HealBot_Nampower_Auras) do -- Check if target is still in the raid/party - if not HealBot_Model:GetUnitIDByName(targetName) then + if not HealBot_Model:GetUnitByGUID(targetName) and not HealBot_Model:GetUnitIDByName(targetName) then HealBot_ReleaseTable(auras) HealBot_Nampower_Auras[targetName] = nil else @@ -127,4 +134,16 @@ end HealBot_Integrations_ClassicAPI_Active = false; HealBot_AddDebug("ClassicAPI Integration: DISABLED"); end + + if HealBot_Options_Integrations_ShowHoTTimers then + if HealBot_Integrations_Nampower_Active or HealBot_Integrations_SuperWoW_Active or HealBot_Integrations_ClassicAPI_Active then + HealBot_Options_Integrations_ShowHoTTimers:Enable() + getglobal(HealBot_Options_Integrations_ShowHoTTimers:GetName().."Text"):SetTextColor(1, 1, 1) + else + HealBot_Options_Integrations_ShowHoTTimers:Disable() + getglobal(HealBot_Options_Integrations_ShowHoTTimers:GetName().."Text"):SetTextColor(0.5, 0.5, 0.5) + HealBot_Options_Integrations_ShowHoTTimers:SetChecked(0) + HealBot_Config.HealBot_Integrations_ShowHoTTimers = 0 + end + end end diff --git a/HealBot_Model.lua b/HealBot_Model.lua index 5d3511b..2b8fd20 100644 --- a/HealBot_Model.lua +++ b/HealBot_Model.lua @@ -156,7 +156,7 @@ function HealBot_Model:UpdateUnitIdentity(unit) self.units[unit].englishClass = englishClass self.units[unit].class = UnitClass(unit) - if (HealBot_Integrations_SuperWoW_Active or HealBot_Integrations_ClassicAPI_Active) and HealBot_GetUnitGUID then + if (HealBot_Integrations_SuperWoW_Active or HealBot_Integrations_ClassicAPI_Active or HealBot_Integrations_Nampower_Active) and HealBot_GetUnitGUID then local guid = HealBot_GetUnitGUID(unit) if guid and guid ~= "0" and guid ~= "0x0000000000000000" then self.unitGUIDs[unit] = guid @@ -185,7 +185,7 @@ end -- HealBot_Model:PreserveStateByGUID: Preserves icon and debuff state when group indices shift during combat. function HealBot_Model:PreserveStateByGUID() - if not (HealBot_Integrations_SuperWoW_Active or HealBot_Integrations_ClassicAPI_Active) or not HealBot_GetUnitGUID then return end + if not (HealBot_Integrations_SuperWoW_Active or HealBot_Integrations_ClassicAPI_Active or HealBot_Integrations_Nampower_Active) or not HealBot_GetUnitGUID then return end for k in pairs(pool_oldGUIDs) do pool_oldGUIDs[k] = nil end local oldGUIDs = pool_oldGUIDs diff --git a/HealBot_Options_Integrations.lua b/HealBot_Options_Integrations.lua index 41cb703..d9d754b 100644 --- a/HealBot_Options_Integrations.lua +++ b/HealBot_Options_Integrations.lua @@ -4,4 +4,5 @@ function HealBot_Options_Integrations_OnShow(this) HealBot_Options_Integrations_Nampower:SetChecked(HealBot_Config.HealBot_Integrations_Nampower); HealBot_Options_Integrations_SuperWoW:SetChecked(HealBot_Config.HealBot_Integrations_SuperWoW); HealBot_Options_Integrations_ClassicAPI:SetChecked(HealBot_Config.HealBot_Integrations_ClassicAPI); + HealBot_Options_Integrations_ShowHoTTimers:SetChecked(HealBot_Config.HealBot_Integrations_ShowHoTTimers); end diff --git a/HealBot_Options_Integrations.xml b/HealBot_Options_Integrations.xml index ab6cc20..f839c0f 100644 --- a/HealBot_Options_Integrations.xml +++ b/HealBot_Options_Integrations.xml @@ -65,7 +65,7 @@ - + @@ -80,6 +80,24 @@ + + + + + + + + + + + getglobal(this:GetName().."Text"):SetText("Track Player HoT Timers"); + + + HealBot_Config.HealBot_Integrations_ShowHoTTimers = this:GetChecked() and 1 or 0; + + + + @@ -94,6 +112,17 @@ + + + + + + + + + + + diff --git a/HealBot_View_Layout.lua b/HealBot_View_Layout.lua index f8b35b4..e206bba 100644 --- a/HealBot_View_Layout.lua +++ b/HealBot_View_Layout.lua @@ -1239,3 +1239,155 @@ function HealBot_Action_AppendNewUnits() end HealBot_ReleaseTable(unitsToCheck) end + +local HealBot_PlayerHoTsListFrame = nil +local HealBot_PlayerHoTFrames = {} + +local HealBot_PlayerHoTs_ValidHoTs = { + ["Renew"] = "Interface\\Icons\\Spell_Holy_Renew", + ["Rejuvenation"] = "Interface\\Icons\\Spell_Nature_Rejuvenation", + ["Regrowth"] = "Interface\\Icons\\Spell_Nature_ResistNature", + ["Power Word: Shield"] = "Interface\\Icons\\Spell_Holy_PowerWordShield", +} + +local HealBot_PlayerHoTs_ListCache = {} + +function HealBot_Action_UpdatePlayerHoTs() + if not HealBot_PlayerHoTsListFrame then + HealBot_PlayerHoTsListFrame = CreateFrame("Frame", "HealBot_PlayerHoTsListFrame", HealBot_Action) + HealBot_PlayerHoTsListFrame:SetWidth(128) + HealBot_PlayerHoTsListFrame:SetHeight(1) + HealBot_PlayerHoTsListFrame:SetPoint("TOPLEFT", HealBot_Action, "BOTTOMLEFT", 0, -2) + HealBot_PlayerHoTsListFrame:Show() + end + + if HealBot_Config.HealBot_Integrations_ShowHoTTimers ~= 1 then + HealBot_PlayerHoTsListFrame:Hide() + return + else + HealBot_PlayerHoTsListFrame:Show() + end + + local currentTime = GetTime() + + -- Clear cache instead of reallocating + local numHoTs = 0 + + if HealBot_Integrations_Nampower_Active and HealBot_Nampower_Auras then + for targetName, auras in pairs(HealBot_Nampower_Auras) do + for spellName, expiration in pairs(auras) do + if HealBot_PlayerHoTs_ValidHoTs[spellName] then + if expiration > currentTime then + numHoTs = numHoTs + 1 + if not HealBot_PlayerHoTs_ListCache[numHoTs] then + HealBot_PlayerHoTs_ListCache[numHoTs] = {} + end + HealBot_PlayerHoTs_ListCache[numHoTs].name = targetName + HealBot_PlayerHoTs_ListCache[numHoTs].spell = spellName + HealBot_PlayerHoTs_ListCache[numHoTs].expires = expiration + end + end + end + end + end + + -- Blank out remaining cache entries so sort doesn't read them + for i = numHoTs + 1, table.getn(HealBot_PlayerHoTs_ListCache) do + HealBot_PlayerHoTs_ListCache[i].expires = 9999999999 + end + + -- Sort only the active portion by shifting expired ones to the end + table.sort(HealBot_PlayerHoTs_ListCache, function(a, b) + return (a.expires or 0) < (b.expires or 0) + end) + + for i = 1, math.max(numHoTs, table.getn(HealBot_PlayerHoTFrames)) do + local frame = HealBot_PlayerHoTFrames[i] + if i <= numHoTs then + if not frame then + frame = CreateFrame("Frame", "HealBot_PlayerHoT" .. i, HealBot_PlayerHoTsListFrame) + frame:SetWidth(128) + frame:SetHeight(16) + if i == 1 then + frame:SetPoint("TOPLEFT", HealBot_PlayerHoTsListFrame, "TOPLEFT", 0, 0) + else + frame:SetPoint("TOPLEFT", HealBot_PlayerHoTFrames[i-1], "BOTTOMLEFT", 0, -2) + end + + local icon = frame:CreateTexture(nil, "ARTWORK") + icon:SetWidth(14) + icon:SetHeight(14) + icon:SetPoint("LEFT", frame, "LEFT", 2, 0) + frame.icon = icon + + local text = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall") + text:SetPoint("LEFT", icon, "RIGHT", 4, 0) + text:SetJustifyH("LEFT") + frame.text = text + + HealBot_PlayerHoTFrames[i] = frame + end + + local hot = HealBot_PlayerHoTs_ListCache[i] + local timeLeft = math.floor(hot.expires - currentTime) + + -- Resolve GUID to Name + if not HealBot_Nampower_NameCache then HealBot_Nampower_NameCache = HealBot_GetTable() end + + local dispName = HealBot_Nampower_NameCache[hot.name] + + if not dispName then + dispName = hot.name + local resolved = false + + local unitID = HealBot_Model:GetUnitByGUID(hot.name) or HealBot_Model:GetUnitIDByName(hot.name) + if unitID then + local n = UnitName(unitID) + if n then + dispName = n + resolved = true + end + elseif string.sub(hot.name, 1, 2) == "0x" or string.sub(hot.name, 1, 2) == "0X" then + if HealBot_PlayerGUID and string.lower(HealBot_PlayerGUID) == string.lower(hot.name) then + dispName = UnitName("player") or "Player" + resolved = true + elseif HealBot_GetUnitGUID then + if string.lower(HealBot_GetUnitGUID("player") or "") == string.lower(hot.name) then + dispName = UnitName("player") or "Player" + resolved = true + elseif string.lower(HealBot_GetUnitGUID("target") or "") == string.lower(hot.name) then + dispName = UnitName("target") or "Target" + resolved = true + elseif string.lower(HealBot_GetUnitGUID("mouseover") or "") == string.lower(hot.name) then + dispName = UnitName("mouseover") or "Mouseover" + resolved = true + end + end + end + + if resolved then + HealBot_Nampower_NameCache[hot.name] = dispName + elseif string.sub(hot.name, 1, 2) == "0x" then + dispName = "Unknown" + end + end + + local spellId = HealBot_GetSpellId and HealBot_GetSpellId(hot.spell) or nil + local tex = HealBot_PlayerHoTs_ValidHoTs[hot.spell] or "Interface\\Icons\\INV_Misc_QuestionMark" + if spellId then + tex = GetSpellTexture(spellId, "BOOKTYPE_SPELL") or tex + end + + frame.icon:SetTexture(tex) + frame.text:SetText(dispName .. ": " .. timeLeft .. "s") + frame.text:SetTextColor(0.8, 1, 0.8) + frame:Show() + else + if frame then + frame:Hide() + end + end + end + + HealBot_PlayerHoTsListFrame:SetHeight(math.max(1, numHoTs * 18)) +end diff --git a/README.md b/README.md index 565fa39..6103ade 100644 --- a/README.md +++ b/README.md @@ -55,12 +55,14 @@ Default installation path: `C:\Program Files\World of Warcraft\Interface\AddOns\ * **Performance Fix - Frame Rendering** - Eliminated redundant UI layout recalculations in `HealBot_Action_EnableButton`. The UI no longer forces `SetFont`, `SetWidth`, `SetHeight`, and `GetGlobal` lookups on every single combat tick (which previously caused 4,000+ layout updates per second in 40-man raids). * **Performance Fix - Range Checking** - Greatly reduced CPU stutter caused by range calculations. `HealBot_Range_Check` now caches map positioning math and API calls per-frame, preventing the addon from calculating the exact same `math.sqrt` distance up to 5 times per unit during heavy combat. * **Performance Fix - Unit ID Lookup** - Optimized `HealBot_FindUnitID` by pre-allocating raid unit tokens, preventing massive string concatenation and garbage collection spam during high-frequency combat events like `CHAT_MSG_ADDON` (HealComm). +* **Performance Fix - String GC Leak** - Fixed garbage collection hitches caused by `string.gfind` closures in the HealComm parsing loop by rewriting it to use closure-free `string.find` native loops. * **Debug Output Command** - Added `/hb debug` (or `/hb dbg`) slash command to toggle real-time debug message printing directly into the standard chat frame without requiring custom chat channels. * **Performance Fix - Spell Lookup** - Fixed a massive CPU drain and garbage generation issue where the entire spellbook was linearly scanned every frame per unit. Spell IDs are now cached via O(1) dictionary lookup on login and talent/spell changes. * **Performance Fix - Unit Hash Lookup** - Eliminated heavy C-API calls to `UnitName()` during incoming heals by caching raid and party unit tokens in a fast O(1) hash table that updates on roster changes. * **Performance Fix - Range Cache GC Leak** - Fixed a continuous memory leak in the range checker caused by string concatenation (`unit .. "_" .. range`) running every frame. Range states are now stored in a nested numeric table cache. * **Performance Fix - Talent Rank Scan** - Fixed severe CPU stutter when gaining or losing auras that affect stats. Previously, inventory updates forced a full equipment and spell recalculation which linearly scanned the entire talent tree multiple times per rank. Talent ranks are now cached natively. * **Integrations Bug Fix** - Removed stray translation text from `HealBot_Integrations.lua` that caused Lua syntax errors on load. +* **Feature - HoT Tracking** - Added an option in the Integrations tab to track Player-cast HoT timers in a dedicated list below the main HealBot frame. Requires Nampower, SuperWoW, or ClassicAPI to fetch exact durations. **v1.7.3** * **Performance Fix - Table Pooling Part 2** - Fixed remaining un-pooled table allocations in layout updates, macro parsing, nampower integration, and aura tracking by utilizing HealBot_GetTable().