Migrate to ClassicAPI for macro display, auras and timing

Squash of the classicapi_next branch (10 commits). ClassicAPI is now a hard
requirement, and the Lua-side reimplementations it supersedes are gone:
net -1087 lines across 12 files.

Macro display: resolved macro actions are published through
C_Macro.SetMacroDisplay, so the client draws macro buttons and the
action-bar function overrides this addon used to install are removed.
Ownership is per macro -- ClassicAPI keeps the macros we never claim.

Auras and timing: ClassicAPI is the source of truth for non-player aura
timing via the positional C_UnitAuras.UnitAura, replacing the write-only
buff tracking tables and the overflow-slot fallback.

Cleanup: the hasPfUI76 flag and the pfUI 7.6 branches it gated, two
silently-shadowed code paths, and per-call allocations in the event and
publish paths (SPELL_CAST_EVENT merged). Packaging moves to
brues-code/packager@vCAPI.

Macro syntax: a clause may carry a leading run of [group] blocks sharing
one action, Blizzard-style -- groups are OR'd, first pass wins, and []
always passes. The ;-separated form is unchanged and mixes freely. @focus
clauses with no focus set now fail quietly instead of printing
"Invalid target" on the way past.
This commit is contained in:
Brues
2026-09-11 00:30:36 -05:00
parent 9f8a5553bc
commit 5213bd5474
12 changed files with 729 additions and 1816 deletions
+25 -372
View File
@@ -22,19 +22,6 @@ function Extension.DLOG(msg)
end
end
function Extension.FocusNameHook()
local hook = Extension.internal.memberHooks[CleveRoids]["GetFocusName"]
local target = hook.original()
if pfUI and pfUI.uf and pfUI.uf.focus and pfUI.uf.focus.unitname then
target = pfUI.uf.focus.unitname
end
--Extension.DLOG(target)
return target
end
-- Check if pfUI's macrotweak module is loaded
function Extension.IsPfUIMacrotweakLoaded()
if not pfUI then return false end
@@ -92,334 +79,6 @@ function Extension.HandleSendChatMessageHook()
end
end
-- Helper function to check for Carnage duration override
-- Returns override duration and timeleft if found, nil otherwise
local function GetCarnageOverride(effect)
if not effect or not CleveRoids.carnageDurationOverrides then
return nil, nil
end
for spellID, override in pairs(CleveRoids.carnageDurationOverrides) do
local spellName = C_Spell.GetSpellName(spellID)
if spellName then
local baseName = CleveRoids.StripRank(spellName)
if baseName == effect and override.timestamp and (GetTime() - override.timestamp) < 5 then
local timeleft = override.duration - (GetTime() - override.timestamp)
if timeleft < 0 then timeleft = 0 end
return override.duration, timeleft
end
end
end
return nil, nil
end
-- Hook pfUI's libdebuff to use our combo-aware durations
-- NOTE: pfUI 7.6+ (GetUnitField edition) handles combo durations and Carnage internally.
-- We only inject when there's a mismatch between pfUI's data and ours.
function Extension.HookPfUILibdebuff()
if not pfUI or not pfUI.api or not pfUI.api.libdebuff then
return false
end
local pflib = pfUI.api.libdebuff
-- Check if pfUI 7.6+ with GetUnitField-based libdebuff is active
-- If so, pfUI handles combo durations and Carnage internally - we only override on mismatch
local hasPfUI76 = CleveRoids.hasPfUI76
-- pfUI 7.6+ handles all durations internally - no hooks needed
if hasPfUI76 then
Extension.DLOG("Skipped all libdebuff hooks (pfUI 7.6+ handles internally)")
return false
end
-- Hook GetDuration if it exists
-- pfUI's GetDuration signature: function(effect, rank) where effect is spell NAME
if pflib.GetDuration and not Extension.pfLibDebuffHooked then
local originalGetDuration = pflib.GetDuration
pflib.GetDuration = function(self, effect, rank)
local pfuiDuration = originalGetDuration(self, effect, rank)
-- Check for Carnage duration overrides (only if pfUI doesn't have it)
local carnageDuration = GetCarnageOverride(effect)
if carnageDuration then
-- Only override if pfUI's duration is significantly different (>1s difference)
if not pfuiDuration or math.abs(carnageDuration - pfuiDuration) > 1 then
if Extension.Debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00aaff[pfUI Duration Override]|r %s: Carnage %.1fs (pfUI: %.1fs)",
effect, carnageDuration, pfuiDuration or 0)
)
end
return carnageDuration
end
end
-- Check name-based tracking for fresh combo casts (only if pfUI returned 0 or nil)
if (not pfuiDuration or pfuiDuration == 0) and CleveRoids.ComboPointTracking and CleveRoids.ComboPointTracking[effect] then
local tracking = CleveRoids.ComboPointTracking[effect]
if tracking.duration and tracking.confirmed and (GetTime() - tracking.cast_time) < 0.5 then
if Extension.Debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00aaff[pfUI Duration Override]|r %s: Combo tracking %.1fs (pfUI: %.1fs)",
effect, tracking.duration, pfuiDuration or 0)
)
end
return tracking.duration
end
end
return pfuiDuration
end
Extension.pfLibDebuffHooked = true
Extension.DLOG("Hooked pfUI.api.libdebuff.GetDuration (mismatch-only mode)")
end
-- Hook AddEffect if it exists (pre-7.6 only - 7.6+ returns early above)
if pflib.AddEffect and not Extension.pfLibAddEffectHooked then
local originalAddEffect = pflib.AddEffect
pflib.AddEffect = function(self, unit, unitlevel, effect, duration, caster)
-- RANK CHECKING: Preserve higher rank's remaining time if lower rank was cast
-- NOTE: 'unit' is a unit NAME (e.g., "Expert Training Dummy"), not a unit ID
-- Defensive: verify libdebuff is a table, not a function
if caster == "player" and type(CleveRoids.libdebuff) == "table" and duration and duration > 0 then
-- Try to find the GUID for this unit name
local unitGUID = nil
-- Check if this is the current target
if UnitName("target") == unit then
unitGUID = CleveRoids.GetGUID("target")
end
-- If we couldn't match to current target, check guidToName mapping
if not unitGUID and CleveRoids.libdebuff.guidToName then
for guid, name in pairs(CleveRoids.libdebuff.guidToName) do
if name == unit then
unitGUID = guid
break
end
end
end
-- Check if a higher rank of this spell is already active
if unitGUID and CleveRoids.libdebuff.objects and CleveRoids.libdebuff.objects[unitGUID] then
-- Find all spell IDs that match this effect name
for spellID, rec in pairs(CleveRoids.libdebuff.objects[unitGUID]) do
if rec and rec.start and rec.duration then
-- Get spell name for this ID
local spellName = C_Spell.GetSpellName(spellID)
if spellName then
local baseName = CleveRoids.StripRank(spellName)
if baseName == effect then
-- Same spell - check if still active
local remaining = rec.duration + rec.start - GetTime()
if remaining > 0 then
-- If incoming duration > remaining time, we're trying to add more time
-- This means either a refresh or lower rank cast - preserve existing timer
if duration > remaining then
if Extension.Debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00aaff[pfUI Rank Preserve]|r %s: Preserving timer (%.1fs remaining vs %.1fs incoming)",
effect, remaining, duration)
)
end
-- Preserve the existing timer
duration = remaining
end
break
end
end
end
end
end
end
end
-- Check for Carnage duration overrides FIRST (highest priority)
local carnageDuration = GetCarnageOverride(effect)
if carnageDuration then
duration = duration or carnageDuration
caster = caster or "player" -- Ensure caster is set for UnitOwnDebuff filtering
end
-- Check if this is a combo scaling spell by name
if not duration and CleveRoids.IsComboScalingSpell and CleveRoids.IsComboScalingSpell(effect) then
if CleveRoids.ComboPointTracking and CleveRoids.ComboPointTracking[effect] then
local tracking = CleveRoids.ComboPointTracking[effect]
if tracking.duration and tracking.confirmed and (GetTime() - tracking.cast_time) < 0.5 then
duration = tracking.duration
caster = caster or "player"
end
end
end
return originalAddEffect(self, unit, unitlevel, effect, duration, caster)
end
Extension.pfLibAddEffectHooked = true
Extension.DLOG("Hooked pfUI.api.libdebuff.AddEffect")
end
-- Hook UnitDebuff to return Carnage override duration to display code
-- Only override when pfUI's duration differs significantly from ours
if pflib.UnitDebuff and not Extension.pfLibUnitDebuffHooked then
local originalUnitDebuff = pflib.UnitDebuff
pflib.UnitDebuff = function(self, unit, id)
local effect, rank, texture, stacks, dtype, duration, timeleft, caster = originalUnitDebuff(self, unit, id)
-- Only check Carnage override if pfUI returned data but duration might be wrong
if effect then
local carnageDuration, carnageTimeleft = GetCarnageOverride(effect)
if carnageDuration then
-- Only override if there's a significant difference (>1s)
if not duration or math.abs(carnageDuration - duration) > 1 then
if Extension.Debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00aaff[pfUI UnitDebuff Override]|r %s: Carnage %.1fs/%.1fs (pfUI: %.1fs/%.1fs)",
effect, carnageDuration, carnageTimeleft, duration or 0, timeleft or 0)
)
end
duration = carnageDuration
timeleft = carnageTimeleft
end
end
end
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
end
Extension.pfLibUnitDebuffHooked = true
Extension.DLOG("Hooked pfUI.api.libdebuff.UnitDebuff (mismatch-only mode)")
end
-- Hook UnitOwnDebuff to return Carnage override duration when selfdebuff is enabled
-- Only override when pfUI's duration differs significantly from ours
if pflib.UnitOwnDebuff and not Extension.pfLibUnitOwnDebuffHooked then
local originalUnitOwnDebuff = pflib.UnitOwnDebuff
pflib.UnitOwnDebuff = function(self, unit, id)
local effect, rank, texture, stacks, dtype, duration, timeleft, caster = originalUnitOwnDebuff(self, unit, id)
if effect then
local carnageDuration, carnageTimeleft = GetCarnageOverride(effect)
if carnageDuration then
-- Only override if there's a significant difference (>1s)
if not duration or math.abs(carnageDuration - duration) > 1 then
if Extension.Debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00aaff[pfUI UnitOwnDebuff Override]|r %s: Carnage %.1fs/%.1fs (pfUI: %.1fs/%.1fs)",
effect, carnageDuration, carnageTimeleft, duration or 0, timeleft or 0)
)
end
duration = carnageDuration
timeleft = carnageTimeleft
end
end
-- If UnitOwnDebuff returned nil but we have a Carnage override, synthesize from UnitDebuff
-- This fallback is only needed for edge cases where pfUI doesn't track the debuff yet
elseif not effect and CleveRoids.carnageDurationOverrides then
-- Use pflib:UnitDebuff which includes our Carnage override hook
local baseEffect, baseRank, baseTex, baseStacks, baseDtype, baseDur, baseLeft, _ = pflib:UnitDebuff(unit, id)
if baseEffect then
local carnageDuration2, carnageTimeleft2 = GetCarnageOverride(baseEffect)
if carnageDuration2 then
return baseEffect, baseRank, baseTex, baseStacks, baseDtype, carnageDuration2, carnageTimeleft2, "player"
end
end
end
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
end
Extension.pfLibUnitOwnDebuffHooked = true
Extension.DLOG("Hooked pfUI.api.libdebuff.UnitOwnDebuff (mismatch-only mode)")
end
return Extension.pfLibDebuffHooked or Extension.pfLibAddEffectHooked or Extension.pfLibUnitDebuffHooked or Extension.pfLibUnitOwnDebuffHooked
end
-- Synchronize combo durations to pfUI's libdebuff objects
-- NOTE: pfUI 7.6+ handles combo durations internally - skip sync entirely
function Extension.SyncComboDurationToPfUI(guid, spellID, duration)
if not pfUI or not pfUI.api or not pfUI.api.libdebuff then
return
end
-- pfUI 7.6+ handles all durations internally
if CleveRoids.hasPfUI76 then
return
end
-- Get unit name from GUID
local unitName = nil
local unitLevel = 0
-- Check if this is the current target
local targetGUID = CleveRoids.GetGUID("target")
if targetGUID == guid then
unitName = UnitName("target")
unitLevel = UnitLevel("target") or 0
end
-- If we couldn't find the unit, use GUID to name mapping from libdebuff
if not unitName and CleveRoids.libdebuff and CleveRoids.libdebuff.guidToName then
unitName = CleveRoids.libdebuff.guidToName[guid]
-- Default to level 0 if we don't have the unit targeted
unitLevel = 0
end
if not unitName then
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[pfUI Sync]|r Could not find unit name for GUID")
end
return
end
-- Get spell name from spell ID
local spellName = C_Spell.GetSpellName(spellID)
if not spellName then
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[pfUI Sync]|r Could not find spell name for ID " .. spellID)
end
return
end
-- Remove rank from spell name to match pfUI's format
local effectName = CleveRoids.StripRank(spellName)
-- Update pfUI's stored debuff duration
local pflib = pfUI.api.libdebuff
if pflib.objects and pflib.objects[unitName] then
-- Try both the specific level and level 0 (fallback)
for _, level in ipairs({unitLevel, 0}) do
if pflib.objects[unitName][level] and pflib.objects[unitName][level][effectName] then
local old_duration = pflib.objects[unitName][level][effectName].duration
pflib.objects[unitName][level][effectName].duration = duration
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ffaa[pfUI Sync]|r Updated %s on %s (L%d): %ds -> %ds",
effectName, unitName, level, old_duration or 0, duration)
)
end
return
end
end
end
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffaaaa00[pfUI Sync]|r Effect not found in pfUI storage: %s on %s",
effectName, unitName)
)
end
end
-- Register action event handler for pfUI button updates
function Extension.RegisterPfUIActionEventHandler()
if not pfUI or Extension.actionHandlerRegistered then
@@ -429,7 +88,17 @@ function Extension.RegisterPfUIActionEventHandler()
-- Register a handler that will be called whenever CleveRoids updates macro states
if CleveRoids.RegisterActionEventHandler then
Extension.DLOG("Registering pfUI action event handler")
CleveRoids.RegisterActionEventHandler(function(slot, event, ...)
-- Declared without `...` on purpose: in 1.12's Lua 5.0 a vararg function
-- allocates an `arg` table on every call, and this handler never read it.
-- That mattered because UpdateAllManagedCooldowns fans
-- ACTIONBAR_UPDATE_COOLDOWN out across every managed slot -- up to 120 calls
-- -- on each SPELL_UPDATE_COOLDOWN, which fires on every GCD and cooldown
-- tick. Those calls all allocated a table and then did nothing, because the
-- whole body only ever applied to ACTIONBAR_SLOT_CHANGED. Hence the early
-- return before any work.
CleveRoids.RegisterActionEventHandler(function(slot, event)
if event ~= "ACTIONBAR_SLOT_CHANGED" then return end
local button = pfUI.bars and pfUI.bars.buttons and pfUI.bars.buttons[slot]
if Extension.Debug then
@@ -441,23 +110,21 @@ function Extension.RegisterPfUIActionEventHandler()
))
end
-- For slot change events, do a full button update so the icon,
-- cooldown, and tooltip refresh through CleveRoids' hooked
-- GetActionTexture / GetActionCooldown / GameTooltip:SetAction.
-- Full button update so the icon, cooldown and tooltip refresh.
-- pfUI's ButtonMacroScan defers to us for managed macros (leaves
-- spellslot nil), so its ButtonFullUpdate routes through those hooks
-- and follows the active conditional — no manual cooldown override
-- needed.
if event == "ACTIONBAR_SLOT_CHANGED" then
-- Mark the slot for update in pfUI's cache (processed next OnUpdate)
if pfUI.bars and pfUI.bars.update then
pfUI.bars.update[slot] = true
end
-- spellslot nil), so ButtonFullUpdate reads the stock action-bar
-- functions -- which now resolve through the value we publish with
-- C_Macro.SetMacroDisplay, rather than the Lua overrides this addon
-- used to install.
-- Also directly call ButtonFullUpdate if the button exists
if button and pfUI.bars.ButtonFullUpdate then
pfUI.bars.ButtonFullUpdate(button)
end
-- Mark the slot for update in pfUI's cache (processed next OnUpdate)
if pfUI.bars and pfUI.bars.update then
pfUI.bars.update[slot] = true
end
-- Also directly call ButtonFullUpdate if the button exists
if button and pfUI.bars.ButtonFullUpdate then
pfUI.bars.ButtonFullUpdate(button)
end
end)
@@ -474,9 +141,6 @@ function Extension.SetupCompatibility()
if Extension.pfUILoaded then
Extension.DLOG("pfUI detected")
-- Hook libdebuff for combo duration support
Extension.HookPfUILibdebuff()
-- Register action event handler for button updates
Extension.RegisterPfUIActionEventHandler()
@@ -631,16 +295,9 @@ function Extension.SetupPfUIEventHooks(lib)
ev:UnregisterEvent("AURA_CAST_ON_OTHER")
ev:UnregisterEvent("DEBUFF_ADDED_OTHER")
ev:UnregisterEvent("DEBUFF_REMOVED_OTHER")
ev:UnregisterEvent("BUFF_ADDED_OTHER")
ev:UnregisterEvent("BUFF_REMOVED_SELF")
ev:UnregisterEvent("BUFF_REMOVED_OTHER")
-- pfUI 7.6+ also handles cast tracking internally
if lib.hasPfUI76 then
ev:UnregisterEvent("SPELL_START_OTHER")
ev:UnregisterEvent("SPELL_FAILED_OTHER")
end
-- Keep registered: SPELL_START_SELF (channel duration capture before early return),
-- UNIT_DIED (AllCasterAuraTracking + OverflowBuff cleanup), UNIT_CASTEVENT (SuperWoW),
-- PLAYER_TARGET_CHANGED, UNIT_AURA (SeedUnit)
@@ -700,7 +357,6 @@ end
function Extension.OnLoad()
Extension.DLOG("Extension pfUI Loaded.")
Extension.HookMethod(CleveRoids, "GetFocusName", "FocusNameHook", true)
-- Export extension for external access
CleveRoids.Compatibility_pfUI = Extension
@@ -743,7 +399,7 @@ end
function Extension.OnPlayerLogin()
-- Ensure lib.objects is linked correctly (InitPfUIIntegration is idempotent).
if pfUI and not CleveRoids.hasPfUI76 then
if pfUI then
local lib = CleveRoids.libdebuff
if lib and lib.InitPfUIIntegration then
lib:InitPfUIIntegration()
@@ -775,9 +431,6 @@ function Extension.OnPlayerLogin()
-- Print startup status only if pfUI global exists and compatibility was set up
if Extension.pfUILoaded and pfUI then
local statusMsg = "|cff00ff00[SCRM]|r pfUI compatibility loaded"
if CleveRoids.hasPfUI76 then
statusMsg = statusMsg .. " (7.6+ GUID cast tracking)"
end
-- statusMsg = statusMsg .. ". Use /pfuicd for debug."
DEFAULT_CHAT_FRAME:AddMessage(statusMsg)
if not Extension.actionHandlerRegistered then