From 6ab3f7a157c4281c5d9d9db0876a41393ac1163a Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Fri, 13 Mar 2026 08:25:23 -0400 Subject: [PATCH] fix debuff bug, reformat pfui hooks, add rangedclip and norangedclip conditionals --- Compatibility/pfUI.lua | 109 ++++++++++ Conditionals.lua | 456 +++++++++++++++++++++++------------------ Utility.lua | 110 ++-------- 3 files changed, 375 insertions(+), 300 deletions(-) diff --git a/Compatibility/pfUI.lua b/Compatibility/pfUI.lua index 7607fdc..f570013 100644 --- a/Compatibility/pfUI.lua +++ b/Compatibility/pfUI.lua @@ -627,6 +627,115 @@ function CleveRoids.GetActionActiveSpellName(actionSlot) return nil end +-- ============================================================================ +-- pfUI Event Hook Management +-- Centralizes all pfUI libdebuff event unregistration and hook registration. +-- Called from lib:InitPfUIIntegration() in Utility.lua. +-- ============================================================================ + +-- Available hooks (registered on pfUI global tables): +-- pfUI.libdebuff_spell_go_hooks["key"] = fn(spellId, arg1..arg7) +-- pfUI.libdebuff_spell_go_other_hooks["key"] = fn(spellId, casterGuid, targetGuid) +-- pfUI.libdebuff_spell_start_self_hooks["key"] = fn(spellId, casterGuid, targetGuid, castTime) +-- pfUI.libdebuff_spell_start_other_hooks["key"] = fn(spellId, casterGuid, targetGuid, castTime) +-- pfUI.libdebuff_spell_failed_other_hooks["key"] = fn(casterGuid, spellId) +-- pfUI.libdebuff_spell_cast_hooks["key"] = fn(success, spellId, castType, targetGuid) +-- pfUI.libdebuff_aura_cast_on_self_hooks["key"] = fn(spellId, casterGuid, targetGuid) +-- pfUI.libdebuff_aura_cast_on_other_hooks["key"] = fn(spellId, casterGuid, targetGuid) +-- pfUI.libdebuff_debuff_added_other_hooks["key"] = fn(guid, luaSlot, spellId, stackCount) +-- pfUI.libdebuff_debuff_removed_other_hooks["key"] = fn(guid, luaSlot, spellId, stackCount) +-- pfUI.libdebuff_unit_health_hooks["key"] = fn(unitToken) +-- pfUI.libdebuff_unit_died_hooks["key"] = fn(guid) +-- pfUI.libdebuff_player_target_changed_hooks["key"] = fn() +-- +-- Note: AURA_CAST hooks don't provide durationMs/auraCapStatus, so +-- AllCasterAuraTracking and overflow buff tracking remain on their own +-- event frame (CleveRoidsAutoAttackFrame in Conditionals.lua). + +function Extension.SetupPfUIEventHooks(lib) + if not pfUI or not lib then return end + + -- ---------------------------------------------------------------- + -- Part 1: Unregister Nampower events from CleveRoidsLibDebuffFrame + -- that pfUI handles. All these handlers early-return when + -- hasPfUIEnhanced, so unregistering avoids wasted event dispatch. + -- ---------------------------------------------------------------- + local ev = CleveRoidsLibDebuffFrame + if ev then + -- Events whose handlers early-return when hasPfUIEnhanced + ev:UnregisterEvent("SPELL_GO_SELF") + ev:UnregisterEvent("SPELL_GO_OTHER") + ev:UnregisterEvent("AURA_CAST_ON_SELF") + 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) + + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99[libdebuff]|r Unregistered redundant events (pfUI handles via hooks)") + end + end + + -- ---------------------------------------------------------------- + -- Part 2: Register pfUI libdebuff hooks for supplementary processing. + -- Hooks fire after pfUI processes each event, avoiding duplicate + -- event listeners. + -- ---------------------------------------------------------------- + local HOOK_KEY = "SuperCleveRoidMacros" + local registered = 0 + + -- UNIT_DIED hook: supplementary cleanup for our private tables + -- (AllCasterAuraTracking, OverflowBuffs) that pfUI doesn't manage. + -- Our UNIT_DIED event handler on the libdebuff frame also does this, + -- but the hook provides a second path in case event ordering shifts. + if type(pfUI.libdebuff_unit_died_hooks) == "table" then + pfUI.libdebuff_unit_died_hooks[HOOK_KEY] = function(guid) + if not guid then return end + guid = CleveRoids.NormalizeGUID(guid) + + -- Clean up AllCasterAuraTracking (our own table, not shared with pfUI) + if CleveRoids.AllCasterAuraTracking and CleveRoids.AllCasterAuraTracking[guid] then + CleveRoids.AllCasterAuraTracking[guid] = nil + end + + -- Clean up OverflowBuffs on player death + local playerGUID = CleveRoids.GetGUID("player") + if playerGUID and guid == playerGUID then + if CleveRoids.OverflowBuffs then + for k in pairs(CleveRoids.OverflowBuffs) do + CleveRoids.OverflowBuffs[k] = nil + end + end + if CleveRoids.AuraCapStatus then + CleveRoids.AuraCapStatus.playerBuffCapped = false + CleveRoids.AuraCapStatus.playerDebuffCapped = false + end + end + end + registered = registered + 1 + end + + lib.pfUIHooksRegistered = registered > 0 + + if CleveRoids.debug and registered > 0 then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff33ff99[libdebuff]|r Registered %d pfUI libdebuff hook(s)", registered) + ) + end +end + function Extension.OnLoad() Extension.DLOG("Extension pfUI Loaded.") Extension.HookMethod(CleveRoids, "GetFocusName", "FocusNameHook", true) diff --git a/Conditionals.lua b/Conditionals.lua index d0d17bf..a795573 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -156,6 +156,21 @@ function CleveRoids.InvalidateSpellNameCache() _spellNameToIDsBuilt = false end +-- Get the specific spell ID for a given spell name and rank number +-- Returns: spellID or nil (if rank not found, caller falls back to rank-agnostic matching) +local function GetSpellIDForRank(baseName, rankNum) + local matchIDs = GetSpellIDsForName(baseName) + if not matchIDs then return nil end + local targetRank = "Rank " .. rankNum + for _, sid in ipairs(matchIDs) do + local rank = GetSpellRecField and GetSpellRecField(sid, "rank") + if rank and rank == targetRank then + return sid + end + end + return nil +end + -- Check if a debuff is shared by spell ID or by name (for custom/Turtle WoW IDs). -- Falls back to checking if the debuff NAME matches any known shared debuff name -- in the spell name cache, even if the specific spell ID is unregistered. @@ -2440,6 +2455,40 @@ function CleveRoids.GetInstantWindowPercent() return (maxInstantStart / attackSpeed) * 100 end +-- Returns timeRemaining, rangedSpeed from SP_SwingTimer, or nil, nil +function CleveRoids.GetRangedTimerRaw() + if st_timerRange ~= nil and st_timerRangeMax ~= nil and st_timerRangeMax > 0 then + return st_timerRange, st_timerRangeMax + end + return nil, nil +end + +-- Validate if casting the action's spell NOW will NOT clip the ranged auto-shot +-- Returns true if rangedTimeRemaining >= spellCastTime (enough time to finish cast before next auto) +-- If ranged timer is not active, returns true (nothing to clip) +function CleveRoids.ValidateNoRangedClip(conditionals) + local remaining, rangedSpeed = CleveRoids.GetRangedTimerRaw() + if remaining == nil then + -- No ranged timer available — either no addon or timer not active (nothing to clip) + if st_timerRangeMax == nil and not CleveRoids._rangedClipErrorShown then + DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [norangedclip] conditional requires SP_SwingTimer. Get it at: https://github.com/jrc13245/SP_SwingTimer", 1, 0.5, 0.5) + CleveRoids._rangedClipErrorShown = true + end + return true + end + + -- Get the cast time of the action spell + local actionName = conditionals and conditionals.action + if not actionName then return true end + + local castTime = CleveRoids.GetSpellCastTime(actionName) + if not castTime or castTime <= 0 then + return true -- Instant cast, no clip possible + end + + return remaining >= castTime +end + -- Validate if current swing timer is within the Slam window (no clip) -- Returns true if casting Slam NOW will NOT clip the auto-attack function CleveRoids.ValidateNoSlamClip() @@ -3549,6 +3598,13 @@ function CleveRoids.ValidateAura(unit, args, isbuff) local stacks, remaining local i = isPlayer and 0 or 1 + -- Extract rank before stripping (for rank-specific matching) + local rankNum + if args.name then + local _, _, rn = string.find(args.name, "%(Rank%s+(%d+)%)") + rankNum = rn + end + -- Strip rank suffix for consistent matching (e.g., "Faerie Fire (Feral)(Rank 4)" -> "Faerie Fire (Feral)") if args.name then args.name = CleveRoids.StripRank(args.name) @@ -3559,6 +3615,15 @@ function CleveRoids.ValidateAura(unit, args, isbuff) -- PERFORMANCE: Cache lowercased search name to avoid repeated string.lower calls -- Support spell ID matching: [mybuff:17941] matches by spell ID instead of name local searchID = args.name and tonumber(args.name) + + -- Rank-specific matching: [mybuff:Mark_of_the_Wild(Rank 9)] only matches that exact rank + if rankNum and not searchID then + local rankSpecificID = GetSpellIDForRank(args.name, rankNum) + if rankSpecificID then + searchID = rankSpecificID + end + end + local searchName = not searchID and args.name and _string_lower(args.name) or nil -- Fast path: Use GetUnitField to search all 48 aura slots in 2 C-to-Lua calls @@ -4036,6 +4101,14 @@ function CleveRoids.ValidateUnitDebuff(unit, args) end if not args.name then return false end + -- Extract rank before stripping (for rank-specific matching) + -- e.g., "Moonfire(Rank 10)" → rankNum = "10", then StripRank → "Moonfire" + local rankNum + do + local _, _, rn = string.find(args.name, "%(Rank%s+(%d+)%)") + rankNum = rn + end + -- Strip rank suffix for consistent matching (e.g., "Faerie Fire (Feral)(Rank 4)" -> "Faerie Fire (Feral)") args.name = CleveRoids.StripRank(args.name) -- Convert underscores to spaces for matching (e.g., "Thunder_Clap" -> "Thunder Clap") @@ -4044,15 +4117,23 @@ function CleveRoids.ValidateUnitDebuff(unit, args) -- Support spell ID matching: [debuff:9904] matches by spell ID instead of name local searchID = tonumber(args.name) + -- Rank-specific matching: [debuff:Moonfire(Rank 10)] only matches that exact rank + if rankNum and not searchID then + local rankSpecificID = GetSpellIDForRank(args.name, rankNum) + if rankSpecificID then + searchID = rankSpecificID + end + end + local found = false local texture, stacks, spellID, remaining local i - -- For non-player units: existence-first, timer-second (matches pfUI's libdebuff pattern) - -- 1. Check if aura actually exists on target via GetUnitField - -- 2. If exists: look up timer data from tracking table, verify caster - -- 3. If not exists: clean up stale tracking entries - -- 4. Fallback: timer-first when GetUnitField unavailable + -- For non-player units: timer-first for personal debuffs, GetUnitField for shared + -- 1. Check tracking table for player's own debuffs (authoritative for personal debuffs) + -- 2. Name-based fallback for custom/Turtle WoW spells not in ID cache + -- 3. Shared debuff check via GetUnitField (fast) or UnitDebuff scan (slow) + -- 4. Stale cleanup: if GetUnitField available and aura not present, clean tracking entries local lib = type(CleveRoids.libdebuff) == "table" and CleveRoids.libdebuff or nil if unit ~= "player" and lib and lib.objects then local guid = CleveRoids.GetGUID(unit) @@ -4066,210 +4147,147 @@ function CleveRoids.ValidateUnitDebuff(unit, args) matchingSpellIDs = GetSpellIDsForName(args.name) or {} end - -- STEP 1: EXISTENCE CHECK via GetUnitField - -- Like pfUI, check actual aura state first — never trust timers alone - local API = CleveRoids.NampowerAPI - local auraPresent -- nil=GetUnitField unavailable, true=on target, false=not on target - local auraSpellId, auraStacks - if API and API.FindUnitAuraInfo then - local searchNameLower = not searchID and args.name and _string_lower(args.name) or nil - local gufResult, gufSpellId, gufStacks, gufSlot = API.FindUnitAuraInfo(unit, searchID, searchNameLower) - if gufResult ~= nil then - auraPresent = gufResult - if gufResult then - auraSpellId = gufSpellId - auraStacks = gufStacks or 0 - end - end - end + local foundSpellId -- Track which spell ID was found (for texture lookup) - if auraPresent then - -- STEP 2a: AURA CONFIRMED ON TARGET — look up timer data - local rec = lib.objects[guid] and lib.objects[guid][auraSpellId] - local isSharedDebuff = lib.IsPersonalDebuff and lib:IsPersonalDebuff(auraSpellId) == false + -- STEP 1: TIMER-FIRST — check tracking table for player's own debuffs + -- Personal debuffs require caster verification; tracking table is authoritative + if matchingSpellIDs and table.getn(matchingSpellIDs) > 0 then + for _, sid in ipairs(matchingSpellIDs) do + local rec = lib.objects[guid] and lib.objects[guid][sid] + local isShared = lib.IsPersonalDebuff and lib:IsPersonalDebuff(sid) == false + if rec and rec.duration and rec.start and (isShared or rec.caster == "player") then + local timeRemaining = rec.duration + rec.start - GetTime() + if timeRemaining > 0 then + found = true + remaining = timeRemaining + stacks = rec.stacks or 0 + foundSpellId = sid - if rec and rec.duration and rec.start and (isSharedDebuff or rec.caster == "player") then - local timeRemaining = rec.duration + rec.start - GetTime() - if timeRemaining > 0 then - found = true - remaining = timeRemaining - stacks = auraStacks - else - -- Timer expired but aura still present (server duration differs) - -- Trust existence, clear stale timer - found = true - remaining = nil - stacks = auraStacks - lib.objects[guid][auraSpellId] = nil - end - - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ff00[Tracking]|r %s (ID:%d): %s", - args.name, auraSpellId, - remaining and string.format("%.1fs left", remaining) or "present (timer expired)") - ) - end - elseif isSharedDebuff then - -- Shared debuff from any caster — existence is sufficient - found = true - stacks = auraStacks - spellID = auraSpellId - remaining = nil - end - -- Personal debuff with no player record → another player's cast, skip - - -- Get texture for found debuff - if found then - for i = 1, 16 do - local _, _, _, sid = UnitDebuff(unit, i) - if sid == auraSpellId then - texture = UnitDebuff(unit, i) - break - end - end - if not texture then - for i = 1, 32 do - local _, _, sid = UnitBuff(unit, i) - if sid == auraSpellId then - texture = UnitBuff(unit, i) - break - end - end - end - end - - elseif auraPresent == false then - -- STEP 2b: AURA NOT ON TARGET — clean up stale tracking entries - if matchingSpellIDs and table.getn(matchingSpellIDs) > 0 then - for _, sid in ipairs(matchingSpellIDs) do - if lib.objects[guid] and lib.objects[guid][sid] then if CleveRoids.debug then - local rec = lib.objects[guid][sid] - local timeLeft = (rec.duration and rec.start) - and (rec.duration + rec.start - GetTime()) or 0 DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff6600[Tracking]|r %s (ID:%d) removed (not on target, timer had %.1fs)", - args.name, sid, timeLeft) + string.format("|cff00ff00[Tracking]|r %s (ID:%d): %.1fs left", + args.name, sid, timeRemaining) ) end - lib.objects[guid][sid] = nil - end - end - end - -- Also clean name-based entries (Turtle WoW custom spells) - if not searchID and lib.objects[guid] then - local cleanupNameLower = _string_lower(args.name) - for sid, rec in pairs(lib.objects[guid]) do - if rec and rec.caster == "player" then - local n = GetSpellRecField and GetSpellRecField(sid, "name") - if n then - n = CleveRoids.StripRank(n) - if _string_lower(n) == cleanupNameLower then - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff6600[Tracking]|r %s (ID:%d) removed (not on target)", - args.name, sid) - ) - end - lib.objects[guid][sid] = nil - break - end - end - end - end - end - -- found stays false - - else - -- STEP 2c: GetUnitField UNAVAILABLE — fall back to timer-first approach - if matchingSpellIDs and table.getn(matchingSpellIDs) > 0 then - for _, sid in ipairs(matchingSpellIDs) do - local rec = lib.objects[guid] and lib.objects[guid][sid] - local isSharedDebuff = lib.IsPersonalDebuff and lib:IsPersonalDebuff(sid) == false - if rec and rec.duration and rec.start and (isSharedDebuff or rec.caster == "player") then - local timeRemaining = rec.duration + rec.start - GetTime() - if timeRemaining > 0 then - found = true - remaining = timeRemaining - stacks = rec.stacks or 0 - - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ff00[Tracking]|r %s (ID:%d): %.1fs left (no verify)", - args.name, sid, timeRemaining) - ) - end - - -- Get texture - for i = 1, 16 do - local _, _, _, debuffSid = UnitDebuff(unit, i) - if debuffSid == sid then - texture = UnitDebuff(unit, i) - break - end - end - if not texture then - for i = 1, 32 do - local _, _, buffSid = UnitBuff(unit, i) - if buffSid == sid then - texture = UnitBuff(unit, i) - break - end - end - end - break - elseif rec then + break + else + -- Timer expired — clean up + if CleveRoids.debug then local expiredTime = GetTime() - (rec.start + rec.duration) - if CleveRoids.debug and expiredTime < 2.0 then + if expiredTime < 2.0 then DEFAULT_CHAT_FRAME:AddMessage( string.format("|cffff6600[Tracking]|r %s (ID:%d) expired %.1fs ago", args.name, sid, expiredTime) ) end - lib.objects[guid][sid] = nil + end + lib.objects[guid][sid] = nil + end + end + end + + if not found and CleveRoids.debugVerbose then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffff0000[Tracking]|r %s not in tracking table (checked %d ranks)", + args.name, table.getn(matchingSpellIDs)) + ) + end + elseif CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffff0000[Tracking]|r Unknown spell: %s", args.name) + ) + end + + -- STEP 2: NAME-BASED FALLBACK for custom/Turtle WoW spells not in ID cache + if not found and (not matchingSpellIDs or table.getn(matchingSpellIDs) == 0) and lib.objects[guid] and not searchID then + local fallbackNameLower = _string_lower(args.name) + for sid, rec in pairs(lib.objects[guid]) do + if rec and rec.caster == "player" then + local n = GetSpellRecField and GetSpellRecField(sid, "name") + if n then + n = CleveRoids.StripRank(n) + if _string_lower(n) == fallbackNameLower then + local timeRemaining = rec.duration + rec.start - GetTime() + if timeRemaining > 0 then + found = true + remaining = timeRemaining + stacks = rec.stacks or 0 + foundSpellId = sid + else + lib.objects[guid][sid] = nil + end + break end end end - - if not found and CleveRoids.debugVerbose then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff0000[Tracking]|r %s not in tracking table (checked %d ranks)", - args.name, table.getn(matchingSpellIDs)) - ) - end - elseif CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff0000[Tracking]|r Unknown spell: %s", args.name) - ) end + end - -- Name-based fallback for custom spells (no GetUnitField) - if not found and (not matchingSpellIDs or table.getn(matchingSpellIDs) == 0) and lib.objects[guid] and not searchID then - local fallbackNameLower = _string_lower(args.name) - for sid, rec in pairs(lib.objects[guid]) do - if rec and rec.caster == "player" then - local n = GetSpellRecField and GetSpellRecField(sid, "name") - if n then - n = CleveRoids.StripRank(n) - if _string_lower(n) == fallbackNameLower then - local timeRemaining = rec.duration + rec.start - GetTime() - if timeRemaining > 0 then - found = true - remaining = timeRemaining - stacks = rec.stacks or 0 - else + -- STEP 3: SHARED DEBUFF CHECK + STALE CLEANUP + -- For shared debuffs (Sunder, Faerie Fire, etc.) from any caster — check actual aura state. + -- Also cleans stale tracking entries when GetUnitField confirms aura is gone. + if not found then + -- Fast path: GetUnitField via FindUnitAuraInfo + local API = CleveRoids.NampowerAPI + local searchNameLower = not searchID and args.name and _string_lower(args.name) or nil + local usedFastPath = false + if API and API.FindUnitAuraInfo then + local gufResult, gufSpellId, gufStacks, gufSlot = API.FindUnitAuraInfo(unit, searchID, searchNameLower) + if gufResult ~= nil then + usedFastPath = true + if gufResult then + -- Step 1 (tracking) already ran and didn't find this debuff. + -- Accept any GetUnitField match as fallback — covers shared debuffs, + -- untracked personal debuffs, and unknown/custom debuffs. + found = true + stacks = gufStacks or 0 + spellID = gufSpellId + foundSpellId = gufSpellId + remaining = nil + elseif lib.objects[guid] then + -- gufResult == false: aura definitively not on target + -- Clean any stale tracking entries + if matchingSpellIDs then + for _, sid in ipairs(matchingSpellIDs) do + if lib.objects[guid][sid] then + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffff6600[Tracking]|r %s (ID:%d) removed (not on target)", + args.name, sid) + ) + end lib.objects[guid][sid] = nil end - break + end + end + -- Also clean name-based entries (Turtle WoW custom spells) + if not searchID then + local cleanupNameLower = _string_lower(args.name) + for sid, rec in pairs(lib.objects[guid]) do + if rec and rec.caster == "player" then + local n = GetSpellRecField and GetSpellRecField(sid, "name") + if n then + n = CleveRoids.StripRank(n) + if _string_lower(n) == cleanupNameLower then + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cffff6600[Tracking]|r %s (ID:%d) removed (not on target)", + args.name, sid) + ) + end + lib.objects[guid][sid] = nil + break + end + end + end end end end end end - -- Fallback: scan actual debuffs for shared debuffs (no GetUnitField) - if not found then + -- Slow path: per-slot UnitDebuff + UnitBuff scan (GetUnitField unavailable) + if not found and not usedFastPath then for i = 1, 16 do local tex, debuffStacks, _, debuffSpellID = UnitDebuff(unit, i) if not tex then break end @@ -4283,15 +4301,13 @@ function CleveRoids.ValidateUnitDebuff(unit, args) matched = baseName and (baseName == args.name or fullName == args.name) end if matched then - local isShared = IsSharedDebuffByIdOrName(lib, debuffSpellID, args.name) - if isShared then - found = true - texture = tex - stacks = debuffStacks or 0 - spellID = debuffSpellID - remaining = nil - break - end + found = true + texture = tex + stacks = debuffStacks or 0 + spellID = debuffSpellID + foundSpellId = debuffSpellID + remaining = nil + break end end end @@ -4311,21 +4327,39 @@ function CleveRoids.ValidateUnitDebuff(unit, args) matched = baseName and (baseName == args.name or fullName == args.name) end if matched then - local isShared = IsSharedDebuffByIdOrName(lib, buffSpellID, args.name) - if isShared then - found = true - texture = tex - stacks = buffStacks or 0 - spellID = buffSpellID - remaining = nil - break - end + found = true + texture = tex + stacks = buffStacks or 0 + spellID = buffSpellID + foundSpellId = buffSpellID + remaining = nil + break end end end end end end + + -- STEP 4: TEXTURE LOOKUP for found debuff (if not already set by slow path) + if found and not texture and foundSpellId then + for i = 1, 16 do + local _, _, _, sid = UnitDebuff(unit, i) + if sid == foundSpellId then + texture = UnitDebuff(unit, i) + break + end + end + if not texture then + for i = 1, 32 do + local _, _, sid = UnitBuff(unit, i) + if sid == foundSpellId then + texture = UnitBuff(unit, i) + break + end + end + end + end -- For player unit, use standard search (player only sees own debuffs on self) elseif unit == "player" then -- Search DEBUFFS first @@ -7863,6 +7897,24 @@ CleveRoids.Keywords = { return not CleveRoids.ValidateNoNextSlamClip() end, + -- Ranged clip window conditionals + -- Checks if there is enough time remaining on the ranged auto-shot timer + -- for the action's cast time to complete without delaying the next auto-shot. + -- Requires SP_SwingTimer addon for ranged timer data. + + -- [norangedclip] - True if casting the action spell NOW will NOT clip ranged auto-shot + -- Usage: /cast [norangedclip] Aimed Shot + -- /cast [norangedclip] Fireball + norangedclip = function(conditionals) + return CleveRoids.ValidateNoRangedClip(conditionals) + end, + + -- [rangedclip] - True if casting the action spell NOW WILL clip ranged auto-shot + -- Usage: /cast [rangedclip] Auto Shot -- Fall back to auto when cast would clip + rangedclip = function(conditionals) + return not CleveRoids.ValidateNoRangedClip(conditionals) + end, + -- Checks if the target uses a specific power type (mana, rage, energy) powertype = function(conditionals) local unit = conditionals.target or "target" diff --git a/Utility.lua b/Utility.lua index 3126c84..287e8ea 100644 --- a/Utility.lua +++ b/Utility.lua @@ -870,42 +870,15 @@ function lib:InitPfUIIntegration() CleveRoidsLibDebuffLearnFrame:UnregisterEvent("CHAT_MSG_SPELL_SELF_DAMAGE") end - -- Unregister Nampower events from the libdebuff frame that pfUI handles. - -- All these handlers early-return when hasPfUIEnhanced, so unregistering - -- avoids wasted event dispatch overhead. - -- Uses pfUI's external hook system for any supplementary processing instead. - local ev = CleveRoidsLibDebuffFrame - if ev then - -- Events whose handlers early-return when hasPfUIEnhanced - ev:UnregisterEvent("SPELL_GO_SELF") - ev:UnregisterEvent("SPELL_GO_OTHER") - ev:UnregisterEvent("AURA_CAST_ON_SELF") - 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) - - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99[libdebuff]|r Unregistered redundant events (pfUI handles via hooks)") - end + -- Delegate event unregistration and hook registration to pfUI compatibility layer. + -- All pfUI hook logic lives in Compatibility/pfUI.lua for easier debugging. + if CleveRoids.Compatibility_pfUI and CleveRoids.Compatibility_pfUI.SetupPfUIEventHooks then + CleveRoids.Compatibility_pfUI.SetupPfUIEventHooks(lib) + else + -- Fallback: call lib method directly (pfUI extension not yet loaded) + lib:RegisterPfUIHooks() end - -- Register pfUI libdebuff hooks for supplementary processing. - -- These fire after pfUI processes each event, avoiding duplicate event listeners. - lib:RegisterPfUIHooks() - if CleveRoids.debug then local v = pfUI.version local tierMsg = lib.hasPfUI76 and " (7.6+ cast tracking)" or "" @@ -947,71 +920,12 @@ function lib:InitPfUIIntegration() return false end --- Register pfUI libdebuff external hooks for supplementary processing. --- Hooks fire after pfUI processes each event, letting us react without --- registering duplicate event listeners. --- --- Available hooks (registered on pfUI global tables): --- pfUI.libdebuff_spell_go_hooks["key"] = fn(spellId, arg1..arg7) --- pfUI.libdebuff_spell_go_other_hooks["key"] = fn(spellId, casterGuid, targetGuid) --- pfUI.libdebuff_spell_start_self_hooks["key"] = fn(spellId, casterGuid, targetGuid, castTime) --- pfUI.libdebuff_spell_start_other_hooks["key"] = fn(spellId, casterGuid, targetGuid, castTime) --- pfUI.libdebuff_spell_failed_other_hooks["key"] = fn(casterGuid, spellId) --- pfUI.libdebuff_spell_cast_hooks["key"] = fn(success, spellId, castType, targetGuid) --- pfUI.libdebuff_aura_cast_on_self_hooks["key"] = fn(spellId, casterGuid, targetGuid) --- pfUI.libdebuff_aura_cast_on_other_hooks["key"] = fn(spellId, casterGuid, targetGuid) --- pfUI.libdebuff_debuff_added_other_hooks["key"] = fn(guid, luaSlot, spellId, stackCount) --- pfUI.libdebuff_debuff_removed_other_hooks["key"] = fn(guid, luaSlot, spellId, stackCount) --- pfUI.libdebuff_unit_health_hooks["key"] = fn(unitToken) --- pfUI.libdebuff_unit_died_hooks["key"] = fn(guid) --- pfUI.libdebuff_player_target_changed_hooks["key"] = fn() --- --- Note: AURA_CAST hooks don't provide durationMs/auraCapStatus, so --- AllCasterAuraTracking and overflow buff tracking remain on their own --- event frame (CleveRoidsAutoAttackFrame in Conditionals.lua). +-- Register pfUI libdebuff hooks (thin wrapper — delegates to Compatibility/pfUI.lua). +-- Hook logic has been moved to Extension.SetupPfUIEventHooks() for easier debugging. +-- This wrapper exists for backwards compatibility if called directly. function lib:RegisterPfUIHooks() - if not pfUI then return end - - local HOOK_KEY = "SuperCleveRoidMacros" - local registered = 0 - - -- UNIT_DIED hook: supplementary cleanup for our private tables - -- (AllCasterAuraTracking, OverflowBuffs) that pfUI doesn't manage. - -- Our UNIT_DIED event handler on the libdebuff frame also does this, - -- but the hook provides a second path in case event ordering shifts. - if type(pfUI.libdebuff_unit_died_hooks) == "table" then - pfUI.libdebuff_unit_died_hooks[HOOK_KEY] = function(guid) - if not guid then return end - guid = CleveRoids.NormalizeGUID(guid) - - -- Clean up AllCasterAuraTracking (our own table, not shared with pfUI) - if CleveRoids.AllCasterAuraTracking and CleveRoids.AllCasterAuraTracking[guid] then - CleveRoids.AllCasterAuraTracking[guid] = nil - end - - -- Clean up OverflowBuffs on player death - local playerGUID = CleveRoids.GetGUID("player") - if playerGUID and guid == playerGUID then - if CleveRoids.OverflowBuffs then - for k in pairs(CleveRoids.OverflowBuffs) do - CleveRoids.OverflowBuffs[k] = nil - end - end - if CleveRoids.AuraCapStatus then - CleveRoids.AuraCapStatus.playerBuffCapped = false - CleveRoids.AuraCapStatus.playerDebuffCapped = false - end - end - end - registered = registered + 1 - end - - lib.pfUIHooksRegistered = registered > 0 - - if CleveRoids.debug and registered > 0 then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff33ff99[libdebuff]|r Registered %d pfUI libdebuff hook(s)", registered) - ) + if CleveRoids.Compatibility_pfUI and CleveRoids.Compatibility_pfUI.SetupPfUIEventHooks then + CleveRoids.Compatibility_pfUI.SetupPfUIEventHooks(lib) end end