From 16bc4d95eea610f3473100134986ea26011c3c8a Mon Sep 17 00:00:00 2001 From: Bluewhale1337 <295648290+Bluewhale1337@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:55:47 +0200 Subject: [PATCH] refactor: implement reliable shapeshift unshifting with a queued cast system and Nampower integration settings --- HealBot_Controller_Events.lua | 82 ++++++++++++++++++++++++-------- HealBot_Controller_Spells.lua | 34 +++++++++---- HealBot_Data.lua | 3 +- HealBot_Options_Integrations.xml | 21 ++++++-- README.md | 2 + 5 files changed, 109 insertions(+), 33 deletions(-) diff --git a/HealBot_Controller_Events.lua b/HealBot_Controller_Events.lua index dd6a816..56a6853 100644 --- a/HealBot_Controller_Events.lua +++ b/HealBot_Controller_Events.lua @@ -78,33 +78,75 @@ function HealBot_OnUpdate(this, arg1) end if HealBot_PendingShapeshiftCast then - if GetTime() > HealBot_PendingShapeshiftCast.expires then - HealBot_PendingShapeshiftCast = nil - else - -- Only cast once the form has successfully been removed - local currentForm = HealBot_GetShapeshiftForm() - if not currentForm then - local pendingCast = HealBot_PendingShapeshiftCast - HealBot_PendingShapeshiftCast = nil - + local pendingCast = HealBot_PendingShapeshiftCast + + -- Wait for server to process the unshift before attempting the cast + if GetTime() >= pendingCast.fireTime then + if not HealBot_GetShapeshiftForm() then + -- Target the unit if necessary before casting -- Target the unit if necessary before casting if pendingCast.oldTarget ~= UnitName(pendingCast.target) then TargetUnit(pendingCast.target) end - -- Attempt the cast now that form is cleared - HealBot_StartCasting(pendingCast.spell, pendingCast.target, "direct") - - if pendingCast.targetEnemy then - HealBot_TargetRestorePending = { type = "enemy" } - elseif pendingCast.oldTarget and pendingCast.oldTarget ~= UnitName(pendingCast.target) then - HealBot_TargetRestorePending = { type = "friend" } - elseif not pendingCast.oldTarget then - HealBot_TargetRestorePending = { type = "clear" } + -- 1. Initialize the MVC side effects (AnnounceCast, Incoming Heals) only ONCE + if not pendingCast.started then + pendingCast.started = true + + -- Extract base spell + local baseSpell = pendingCast.spell + local parenIndex = string.find(pendingCast.spell, "%(") + if parenIndex then + baseSpell = string.sub(pendingCast.spell, 1, parenIndex - 1) + end + baseSpell = string.gsub(baseSpell, "%s+$", "") + + -- Force MVC updates since we guarantee the cast will eventually pierce the server + HealBot_CastFailed = false + HealBot_CastingSpell = baseSpell + HealBot_CastingTarget = pendingCast.target + HealBot_Process_HealValue(baseSpell, pendingCast.target) + HealBot_AnnounceCast(pendingCast.spell, pendingCast.target) + + -- Restore target logic + if pendingCast.targetEnemy then + HealBot_TargetRestorePending = { type = "enemy" } + elseif pendingCast.oldTarget and pendingCast.oldTarget ~= UnitName(pendingCast.target) then + HealBot_TargetRestorePending = { type = "friend" } + elseif not pendingCast.oldTarget then + HealBot_TargetRestorePending = { type = "clear" } + end + HealBot_TargetRestoreTimer = 0 + end + + -- 2. Spam the raw cast silently to guarantee it pierces the server delay + if pendingCast.started then + if not pendingCast.nextSpam or GetTime() >= pendingCast.nextSpam then + pendingCast.nextSpam = GetTime() + 0.15 + + -- Safely suppress UI errors + UIErrorsFrame:UnregisterEvent("UI_ERROR_MESSAGE") + + -- Only use HealBot's native cast wrapper + HealBot_CastSpellByName(pendingCast.spell) + + if SpellCanTargetUnit(pendingCast.target) then + SpellTargetUnit(pendingCast.target) + elseif SpellIsTargeting() then + SpellTargetUnit(pendingCast.target) + SpellStopTargeting() + end + + UIErrorsFrame:RegisterEvent("UI_ERROR_MESSAGE") + end end - HealBot_TargetRestoreTimer = 0 end end + + -- Failsafe timeout + if HealBot_PendingShapeshiftCast and GetTime() > pendingCast.expires then + HealBot_PendingShapeshiftCast = nil + end end -- Process Dirty Queue for MVC View @@ -524,6 +566,7 @@ end -- HealBot_OnEvent_SpellcastStart: Internal utility: HealBot_OnEvent_SpellcastStart function HealBot_OnEvent_SpellcastStart(this, spell, duration) HealBot_IsCasting = true; + HealBot_PendingShapeshiftCast = nil; HealBot_RecalcHeals(); HealBot_CheckCasting(); if spell == HEALBOT_RESURRECTION or spell == HEALBOT_ANCESTRALSPIRIT or spell == HEALBOT_REBIRTH or spell == HEALBOT_REDEMPTION then @@ -537,6 +580,7 @@ end -- HealBot_OnEvent_SpellcastStop: Internal utility: HealBot_OnEvent_SpellcastStop function HealBot_OnEvent_SpellcastStop(this, eventName) HealBot_IsCasting = false; + HealBot_PendingShapeshiftCast = nil; if eventName == "SPELLCAST_FAILED" then HealBot_CastFailed = true; end diff --git a/HealBot_Controller_Spells.lua b/HealBot_Controller_Spells.lua index 36fd542..14e96d0 100644 --- a/HealBot_Controller_Spells.lua +++ b/HealBot_Controller_Spells.lua @@ -171,6 +171,19 @@ end -- HealBot_StartCasting: Initiates spell cast and broadcasts incoming heal. function HealBot_StartCasting(spell, target, ttype) HealBot_CastFailed = false; + + -- Extract base spell for internal tracking + local baseSpell = spell + local parenIndex = string.find(spell, " %(") + if parenIndex then + baseSpell = string.sub(spell, 1, parenIndex - 1) + else + parenIndex = string.find(spell, "%(") + if parenIndex then + baseSpell = string.sub(spell, 1, parenIndex - 1) + end + end + HealBot_CastSpellByName(spell); if ( SpellCanTargetUnit(target) ) then SpellTargetUnit(target); @@ -184,14 +197,16 @@ function HealBot_StartCasting(spell, target, ttype) end end - if ttype == "fired" and HealBot_Spells[spell] then + if ttype == "fired" and HealBot_Spells[baseSpell] then if not HealBot_CastFailed then - HealBot_CastingSpell = spell; + HealBot_CastingSpell = baseSpell; HealBot_CastingTarget = target; - HealBot_Process_HealValue(spell, target); + HealBot_Process_HealValue(baseSpell, target); HealBot_AnnounceCast(spell, target); end end + + return ttype == "fired" end -- HealBot_StopCasting: Internal utility: HealBot_StopCasting @@ -308,7 +323,9 @@ function HealBot_CastSpellOnFriend(spell, target) end if formCancelled then - HealBot_PendingShapeshiftCast = { spell = spell, target = target, targetEnemy = targetEnemy, oldTarget = oldTarget, expires = GetTime() + 1.0 } + -- ALWAYS put the cast into the Pending queue so the OnUpdate loop can wait for the server + -- to process the unshift before attempting the cast, otherwise we get "You are in shapeshift form". + HealBot_PendingShapeshiftCast = { spell = spell, target = target, targetEnemy = targetEnemy, oldTarget = oldTarget, fireTime = GetTime() + 0.05, expires = GetTime() + 1.0 } return; end @@ -839,9 +856,9 @@ function HealBot_Generic_Patten(matchStr, matchPattern) return tmpTest, _HealsMin, _HealsMax; end --- HealBot_UpdateShapeshiftForm: Internal utility: HealBot_UpdateShapeshiftForm +-- HealBot_UpdateShapeshiftForm: Called on UPDATE_SHAPESHIFT_FORM function HealBot_UpdateShapeshiftForm() - -- Deprecated, state is pulled in real-time on cast + -- Deprecated end -- HealBot_GetShapeshiftForm: Detects active druid form to prevent invalid casts. @@ -852,10 +869,7 @@ function HealBot_GetShapeshiftForm() for i=1,forms do local icon,name,active = GetShapeshiftFormInfo(i); if active then - local icon_lower = string.lower(icon); - if not string.find(icon_lower, "humanoidform") and not string.find(icon_lower, "treeoflife") and not string.find(icon_lower, "stoneclawtotem") then - return i; - end + return i; end end end diff --git a/HealBot_Data.lua b/HealBot_Data.lua index bc576d9..cf04277 100644 --- a/HealBot_Data.lua +++ b/HealBot_Data.lua @@ -27,8 +27,9 @@ HealBot_ConfigDefaults = { ActionVisible = 1, HideSolo = 0, OverHeal = 0.25, - CastNotify = 1, + UpdateFreq = 0.5, AutoUnshift = 1, + Integrations_Nampower_Active = true, ChatMessages = { [1] = { Spell = "None", Message = "Casting #Spell# on #Target#", Channel = "None" }, [2] = { Spell = "None", Message = "Casting #Spell# on #Target#", Channel = "None" }, diff --git a/HealBot_Options_Integrations.xml b/HealBot_Options_Integrations.xml index 00d73d3..af31f10 100644 --- a/HealBot_Options_Integrations.xml +++ b/HealBot_Options_Integrations.xml @@ -33,7 +33,7 @@ - getglobal(this:GetName().."Text"):SetText("Enable nampower Integration (Buffs/Heals)"); + getglobal(this:GetName().."Text"):SetText("Enable nampower Integration (Buffs/Heals/Queueing)"); HealBot_Config.HealBot_Integrations_Nampower = this:GetChecked() and 1 or 0; @@ -44,9 +44,9 @@ - + - + @@ -81,6 +81,21 @@ + + + + + + + + + + + + + + + HealBot_Options_Integrations_OnShow(this); diff --git a/README.md b/README.md index 051f1b5..9362c81 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,8 @@ Default installation path: `C:\Program Files\World of Warcraft\Interface\AddOns\ * **Bug Fix - Slow Client Init** - Fixed missing class colors after `/reload` by adding lazy-load identity fetches directly into the render pipeline for when `UnitClass` data is delayed by the server. * **Bug Fix - Debuff Tracking** - HealBot now automatically tracks all curable debuffs for the player's class out of the box. Previously, users had to manually assign a cure spell to a click binding to enable CDC debuff tracking. * **Bug Fix - CDC Filter** - Fixed an issue where the CDC module displayed all debuffs instead of filtering for dispellable ones, restoring proper health bar coloring. +* **Bug Fix - UnitClass Localization** - Added a fallback mapping for the Vanilla `UnitClass` API to always return the uppercase English class string. This fixes a bug where default options (like Debuff tracking) failed to initialize. +* **Feature - Shapeshift Spell Queue** - Built a native spell queue system for Druid auto-unshifting. Bypasses the "Cannot cast while shapeshifted" error and server lag when AutoUnshift is enabled, firing the heal seamlessly the exact millisecond the form is dropped. Now completely unified to use the standard casting API for maximum reliability across all client setups. **v1.6.5** * **Performance Update - Mana Tracking** - Added a fast-path redraw pipeline for unit power changes. This optimization prevents full frame redraws when players naturally regenerate or consume mana, resolving severe FPS drops in 40-man raids while tracking mana.