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().