Merge pull request #120 from jrc13245/overflowtest

Merge Overflowtest
This commit is contained in:
Torio
2026-02-18 18:10:09 -05:00
committed by GitHub
11 changed files with 2075 additions and 187 deletions
+2 -2
View File
@@ -124,7 +124,7 @@ function Extension.HookPfUILibdebuff()
-- 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 or (pfUI.libdebuff_slot_ownership ~= nil)
local hasPfUI76 = CleveRoids.hasPfUI76
-- pfUI 7.6+ handles all durations internally - no hooks needed
if hasPfUI76 then
@@ -351,7 +351,7 @@ function Extension.SyncComboDurationToPfUI(guid, spellID, duration)
end
-- pfUI 7.6+ handles all durations internally
if CleveRoids.hasPfUI76 or (pfUI.libdebuff_slot_ownership ~= nil) then
if CleveRoids.hasPfUI76 then
return
end
+380 -25
View File
@@ -149,6 +149,33 @@ function CleveRoids.InvalidateSpellNameCache()
_spellNameToIDsBuilt = false
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.
local function IsSharedDebuffByIdOrName(lib, spellID, debuffName)
if not lib then return false end
-- Direct ID check (fast path)
if lib.IsPersonalDebuff and lib:IsPersonalDebuff(spellID) == false then
return true
end
-- Name-based fallback: if ANY spell ID for this name is in sharedDebuffs, treat as shared.
-- This handles custom server spell IDs (e.g., Turtle WoW Judgement variants) that have
-- the same debuff name but different IDs from the hardcoded vanilla entries.
if debuffName then
local knownIDs = GetSpellIDsForName(debuffName)
if knownIDs then
for _, kid in ipairs(knownIDs) do
if lib.sharedDebuffs and lib.sharedDebuffs[kid] then
-- Also register this new spell ID so future checks are fast
lib.sharedDebuffs[spellID] = lib.sharedDebuffs[kid]
return true
end
end
end
end
return false
end
-- PERFORMANCE: Equipment cache for HasGearEquipped (avoids 19-slot scan per call)
-- Invalidated on UNIT_INVENTORY_CHANGED via CleveRoids.InvalidateEquipmentCache()
-- Enhanced with Nampower v2.18+ GetEquippedItems when available
@@ -849,6 +876,14 @@ CleveRoids.AuraCapStatus = {
lastCleanupTime = 0,
}
-- Overflow buff tracking: buffs applied while buff-capped (v2.34+)
-- GetUnitField "aura" returns 48 slots: [1-32] = buff slots, [33-48] = debuff slots.
-- Beyond these 48 client slots, the server can hold additional overflow buffs with
-- no aura slot at all -- invisible to GetPlayerBuff and GetPlayerAuraDuration.
-- Populated from AURA_CAST_ON_SELF when auraCapStatus indicates buff bar full.
-- Format: [spellId] = { timestamp = GetTime(), durationSec = durationMs/1000 }
CleveRoids.OverflowBuffs = {}
-- All-caster aura duration tracking (from AURA_CAST events)
-- Tracks buff/debuff durations from ANY caster, not just player
-- Structure: [targetGuid][spellId] = { start, duration, casterGuid }
@@ -994,10 +1029,98 @@ local function OnAuraCastSelf(spellId, casterGuid, targetGuid, effect, effectAur
local now = GetTime()
CleveRoids.AuraCapStatus.playerLastUpdate = now
local buffCapped = HasHitFlag(auraCapStatus, AURA_CAP_BUFF_FULL)
-- Check buff bar full (bit 1)
CleveRoids.AuraCapStatus.playerBuffCapped = HasHitFlag(auraCapStatus, AURA_CAP_BUFF_FULL)
CleveRoids.AuraCapStatus.playerBuffCapped = buffCapped
-- Check debuff bar full (bit 2)
CleveRoids.AuraCapStatus.playerDebuffCapped = HasHitFlag(auraCapStatus, AURA_CAP_DEBUFF_FULL)
-- Determine if this aura is a buff (not debuff) — debuffs land in GetPlayerAuraDuration
-- slots 32-47, so checking those slots identifies them regardless of cap status.
local isBuffNotDebuff = false
if spellId and spellId > 0 and effectAuraName and effectAuraName > 0 then
local isDebuff = false
if _G.GetPlayerAuraDuration then
for slot = 32, 47 do
local sid = _G.GetPlayerAuraDuration(slot)
if sid and sid == spellId then
isDebuff = true
break
end
end
end
isBuffNotDebuff = not isDebuff
-- Track overflow buffs: if buff bar is full, this buff has no client aura slot.
-- Store spellId + duration so /cancelaura and [mybuff] can find it.
if buffCapped and isBuffNotDebuff then
local entry = CleveRoids.OverflowBuffs[spellId]
if not entry then
entry = {}
CleveRoids.OverflowBuffs[spellId] = entry
end
entry.timestamp = now
entry.durationSec = durationMs and (durationMs / 1000) or 0
end
end
if not buffCapped and next(CleveRoids.OverflowBuffs) then
-- No longer buff-capped: some overflow buffs may have gotten real slots.
-- Only remove entries that now appear in a visible aura slot.
-- The server does NOT auto-migrate overflow buffs into freed slots,
-- so we can't blindly clear everything.
if _G.GetPlayerAuraDuration then
local visibleSpells = {}
for slot = 0, 31 do
local sid = _G.GetPlayerAuraDuration(slot)
if sid and sid > 0 then
visibleSpells[sid] = true
end
end
for k in pairs(CleveRoids.OverflowBuffs) do
if visibleSpells[k] then
CleveRoids.OverflowBuffs[k] = nil
end
end
end
-- Also prune expired entries
for k, entry in pairs(CleveRoids.OverflowBuffs) do
if entry.durationSec and entry.durationSec > 0 then
local elapsed = now - (entry.timestamp or 0)
if elapsed > entry.durationSec then
CleveRoids.OverflowBuffs[k] = nil
end
end
end
end
-- NEW: Populate ownBuffCasts and allBuffAuras for all player buffs (not just overflow)
-- Use the isBuffNotDebuff result determined above (avoids redundant slot scanning)
local lib = CleveRoids.libdebuff
if isBuffNotDebuff and spellId and durationMs and durationMs > 0 and lib and not lib.hasPfUIEnhanced then
local spellName = SpellInfo and SpellInfo(spellId)
if spellName then
local _, playerGuidRaw = UnitExists("player")
local playerGuid = playerGuidRaw and CleveRoids.NormalizeGUID(playerGuidRaw)
if playerGuid then
lib.ownBuffCasts[playerGuid] = lib.ownBuffCasts[playerGuid] or {}
lib.ownBuffCasts[playerGuid][spellName] = {
startTime = now,
duration = durationMs / 1000,
spellId = spellId,
casterGuid = casterGuid,
}
lib.allBuffAuras[playerGuid] = lib.allBuffAuras[playerGuid] or {}
lib.allBuffAuras[playerGuid][spellName] = lib.allBuffAuras[playerGuid][spellName] or {}
lib.allBuffAuras[playerGuid][spellName][casterGuid or "unknown"] = {
startTime = now,
duration = durationMs / 1000,
rank = 0,
}
end
end
end
end
local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAuraName,
@@ -1033,6 +1156,25 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu
spellName, spellId, string.sub(tostring(targetGuid), 1, 16), durationMs / 1000
))
end
-- NEW: Store in pendingBuffCasts for BUFF_ADDED_OTHER to confirm as buff
-- (AURA_CAST_ON_OTHER fires for both buffs and debuffs; BUFF_ADDED_OTHER confirms buff)
local lib = CleveRoids.libdebuff
if lib and not lib.hasPfUIEnhanced then
local spellNameForPending = SpellInfo and SpellInfo(spellId)
if spellNameForPending then
local normTargetGuid = CleveRoids.NormalizeGUID(targetGuid)
if normTargetGuid then
lib.pendingBuffCasts[normTargetGuid] = lib.pendingBuffCasts[normTargetGuid] or {}
lib.pendingBuffCasts[normTargetGuid][spellId] = {
casterGuid = CleveRoids.NormalizeGUID(casterGuid),
duration = durationMs / 1000,
spellName = spellNameForPending,
time = now,
}
end
end
end
end
-- Store cap status for this target GUID (if available)
@@ -1663,6 +1805,58 @@ end
function CleveRoids.CancelAura(auraName)
local ix = 0
auraName = string.lower(string.gsub(auraName, "_"," "))
-- v2.34+ path: cancel by spell ID (works for buff-capped overflow auras too)
local API = CleveRoids.NampowerAPI
if API and API.features.hasCancelPlayerAuraSpellId and CleveRoids.hasSuperwow then
-- First scan visible buffs via GetPlayerBuff (fast, covers normal case)
while true do
local aura_ix = GetPlayerBuff(ix, "HELPFUL")
ix = ix + 1
if aura_ix == -1 then break end
local bid = GetPlayerBuffID(aura_ix)
bid = (bid < -1) and (bid + 65536) or bid
if string.lower(SpellInfo(bid)) == auraName then
_G.CancelPlayerAuraSpellId(bid, 1)
return true
end
end
-- Not found in visible buffs - scan all 32 raw aura slots
-- GetPlayerAuraDuration reads unit data fields directly (same 32 slots but bypasses UI filtering)
if API.features.hasGetPlayerAuraDuration and _G.GetPlayerAuraDuration then
for slot = 0, 31 do
local spellId = _G.GetPlayerAuraDuration(slot)
if spellId and spellId > 0 then
local name = SpellInfo(spellId)
if name and string.lower(name) == auraName then
_G.CancelPlayerAuraSpellId(spellId, 1)
return true
end
end
end
end
-- Final fallback: check overflow buff tracking (buffs applied while buff-capped
-- that have NO client aura slot - tracked via AURA_CAST_ON_SELF events)
for spellId, entry in pairs(CleveRoids.OverflowBuffs) do
-- Skip expired overflow entries
local elapsed = GetTime() - (entry.timestamp or 0)
if entry.durationSec and entry.durationSec > 0 and elapsed > entry.durationSec then
CleveRoids.OverflowBuffs[spellId] = nil
else
local name = SpellInfo(spellId)
if name and string.lower(name) == auraName then
_G.CancelPlayerAuraSpellId(spellId, 1)
CleveRoids.OverflowBuffs[spellId] = nil
return true
end
end
end
return false
end
-- Legacy path for older Nampower versions
while true do
local aura_ix = GetPlayerBuff(ix,"HELPFUL")
ix = ix + 1
@@ -2451,11 +2645,16 @@ end
--- Falls back to basic position checking if MonkeySpeed isn't available
--- @return boolean True if player is moving
function CleveRoids.IsPlayerMoving()
-- If MonkeySpeed is available, use it for accurate detection
-- If MonkeySpeed is available, use it for accurate speed-based detection
if CleveRoids.HasMonkeySpeed() then
return (MonkeySpeed.m_fSpeed or 0) > 0
end
-- Nampower 2.36+ provides direct movement flag query (catches jumping, falling, pitching)
if CleveRoids.NampowerAPI.features.hasPlayerIsMoving then
return PlayerIsMoving() == 1
end
-- Fallback: use continuously tracked position history from OnUpdate
-- Core.lua tracks position at ~100 Hz with 4-sample circular buffer (matches MonkeySpeed's 0.01s).
-- Movement is detected if ANY consecutive pair of samples shows movement.
@@ -2465,25 +2664,18 @@ function CleveRoids.IsPlayerMoving()
local headIdx = CleveRoids._posHistoryIndex or 0
if history and count >= 2 then
-- Iterate consecutive pairs in circular buffer order (oldest to newest)
-- Head is the most recent entry, walk backwards through the ring
local samplesAvail = count
for i = 1, samplesAvail - 1 do
-- prevIdx and currIdx walk from oldest to newest
local currOffset = samplesAvail - i -- e.g., 3,2,1,0 for count=4
local prevOffset = samplesAvail - i + 1 -- e.g., 4,3,2,1
local currIdx = math.mod((headIdx - currOffset - 1), 4) + 1
local prevIdx = math.mod((headIdx - prevOffset - 1), 4) + 1
local prev = history[prevIdx]
local curr = history[currIdx]
-- Compare the most recent pair: headIdx (newest) vs. the slot before it.
-- Use (headIdx - 2 + 4) mod 4 to safely wrap backwards in the 1-based ring.
-- This avoids Lua 5.0 math.mod returning negative values for negative inputs.
local prevIdx = math.mod((headIdx - 2 + 4), 4) + 1
local curr = history[headIdx]
local prev = history[prevIdx]
if curr and prev then
local dx = curr.x - prev.x
local dy = curr.y - prev.y
local dist = math.sqrt(dx * dx + dy * dy)
if dist > 0.001 then
return true -- Any movement in recent history = moving
end
return math.sqrt(dx * dx + dy * dy) > 0.001
end
return false -- No movement in any recent sample = stopped
return false
end
-- Legacy fallback for code that might not have history yet
@@ -3281,6 +3473,112 @@ function CleveRoids.ValidateAura(unit, args, isbuff)
end
end
-- Overflow buff fallback: player buffs in server slots 33-48 (no client slot)
-- Check AURA_CAST_ON_SELF tracked overflow data for presence + duration
if not found and isPlayer and isbuff and next(CleveRoids.OverflowBuffs) then
local now = GetTime()
for spellId, entry in pairs(CleveRoids.OverflowBuffs) do
-- Prune expired entries
local elapsed = now - (entry.timestamp or 0)
if entry.durationSec and entry.durationSec > 0 and elapsed > entry.durationSec then
CleveRoids.OverflowBuffs[spellId] = nil
else
local match = false
if searchID then
match = (spellId == searchID)
elseif searchName then
local lowerName = GetLowercaseSpellName(spellId)
match = (lowerName and lowerName == searchName)
end
if match then
found = true
stacks = 0
-- Compute remaining time from apply timestamp + duration
if entry.durationSec and entry.durationSec > 0 then
remaining = entry.durationSec - elapsed
if remaining < 0 then remaining = 0 end
else
remaining = -1 -- Unknown duration (permanent buff)
end
break
end
end
end
end
-- allBuffAuras fallback for player buff timing: when slow path found the buff but
-- returned no remaining time, check lib.allBuffAuras for cached AURA_CAST timing.
if found and remaining == nil and isPlayer and isbuff and searchName then
local lib = type(CleveRoids.libdebuff) == "table" and CleveRoids.libdebuff or nil
if lib and lib.allBuffAuras then
local _, playerGuidRaw = UnitExists("player")
local playerGuid = CleveRoids.NormalizeGUID(playerGuidRaw)
if playerGuid and lib.allBuffAuras[playerGuid] then
-- Try exact name match first
local casters = lib.allBuffAuras[playerGuid][args.name]
-- Try lowercase match if exact didn't work
if not casters then
for bName, c in pairs(lib.allBuffAuras[playerGuid]) do
if _string_lower(bName) == searchName then
casters = c
break
end
end
end
if casters then
for _, cData in pairs(casters) do
local elapsed = GetTime() - (cData.startTime or 0)
remaining = cData.duration > 0 and (cData.duration - elapsed) or -1
break
end
end
end
end
end
-- Non-player overflow fallback: buff exists in server slots 33-48 (no client slot)
-- AllCasterAuraTracking already has data from AURA_CAST_ON_OTHER for all aura applications.
-- If the normal scan didn't find the buff, check there for presence + duration.
-- Guard: verify the spell isn't a visible debuff on the target (AllCasterAuraTracking
-- stores both buffs and debuffs, so without this check [buff:DebuffName] could false-positive).
if not found and not isPlayer and isbuff and (searchID or searchName) then
local _, targetGuid = UnitExists(unit)
if targetGuid then
-- Check if the spell is in a visible debuff slot — if so, it's a debuff, not a buff
local isDebuff = false
local di = 1
while true do
local dtex, _, _, dspellId = UnitDebuff(unit, di)
if not dtex then break end
if dspellId then
if searchID then
if dspellId == searchID then
isDebuff = true
break
end
elseif searchName then
local lowerName = GetLowercaseSpellName(dspellId)
if lowerName and lowerName == searchName then
isDebuff = true
break
end
end
end
di = di + 1
end
if not isDebuff then
local trackRemaining = CleveRoids.FindAllCasterAuraByName(targetGuid,
searchID and tostring(searchID) or args.name)
if trackRemaining then
found = true
stacks = 0
remaining = trackRemaining
end
end
end
end
local ops = CleveRoids.operators
local cmp = CleveRoids.comparators
@@ -3291,6 +3589,46 @@ function CleveRoids.ValidateAura(unit, args, isbuff)
-- (showing ~1000s instead of actual remaining time). Skip it for buff time checks
-- and rely on all-caster tracking from AURA_CAST events instead.
-- Fast path: Check lib.allBuffAuras (spellName-indexed, O(1) lookup)
-- More efficient than FindAllCasterAuraByName which does ID→name translation
if nonPlayerAuraTimeRemaining == nil and isbuff then
local lib = type(CleveRoids.libdebuff) == "table" and CleveRoids.libdebuff or nil
if lib and lib.allBuffAuras then
local _, targetGuid = UnitExists(unit)
targetGuid = targetGuid and CleveRoids.NormalizeGUID(targetGuid)
if targetGuid then
local buffEntries = lib.allBuffAuras[targetGuid]
if buffEntries then
-- Try exact name match first
local casters = buffEntries[args.name]
-- Try lowercase match if exact didn't work
if not casters and searchName then
for bName, c in pairs(buffEntries) do
if _string_lower(bName) == searchName then
casters = c
break
end
end
end
if casters then
for _, cData in pairs(casters) do
local elapsed = GetTime() - (cData.startTime or 0)
local rem = cData.duration > 0 and (cData.duration - elapsed) or -1
if rem == nil or rem > 0 or cData.duration <= 0 then
nonPlayerAuraTimeRemaining = rem
if not found then
found = true
stacks = 0
end
break
end
end
end
end
end
end
end
-- Second try: All-caster tracking from AURA_CAST events (works for any caster)
-- Only use if libdebuff didn't find it (libdebuff has more accurate timing for player casts)
if nonPlayerAuraTimeRemaining == nil then
@@ -3506,7 +3844,7 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
if gufResult ~= nil then
-- GetUnitField was available and searched
if gufResult then
local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(gufSpellId) == false
local isShared = IsSharedDebuffByIdOrName(lib, gufSpellId, args.name)
if isShared then
found = true
stacks = gufStacks or 0
@@ -3540,7 +3878,8 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
if matched then
-- IMPORTANT: Only use fallback for SHARED debuffs
-- Personal debuffs must come from tracking table (caster check)
local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(debuffSpellID) == false
-- Uses name-based fallback for custom spell IDs (e.g., Turtle WoW Judgements)
local isShared = IsSharedDebuffByIdOrName(lib, debuffSpellID, args.name)
if isShared then
found = true
texture = tex
@@ -3570,7 +3909,8 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
end
if matched then
-- IMPORTANT: Only use fallback for SHARED debuffs
local isShared = lib and lib.IsPersonalDebuff and lib:IsPersonalDebuff(buffSpellID) == false
-- Uses name-based fallback for custom spell IDs (e.g., Turtle WoW Judgements)
local isShared = IsSharedDebuffByIdOrName(lib, buffSpellID, args.name)
if isShared then
found = true
texture = tex
@@ -4347,6 +4687,7 @@ CleveRoids.CCTypesLossOfControl = {
[1] = true, -- charm
[2] = true, -- disoriented
[5] = true, -- fear
[7] = true, -- root (Entangling Roots, Frost Nova, etc.)
[10] = true, -- sleep
[12] = true, -- stun (Cheap Shot, Kidney Shot, etc.)
[13] = true, -- freeze
@@ -6219,14 +6560,28 @@ CleveRoids.Keywords = {
end, conditionals, "nopet")
end,
-- [swimming] - Player is currently swimming (Nampower v2.36+)
swimming = function(conditionals)
-- Check if "Aquatic Form" is in the reactive list and usable
return CleveRoids.IsReactiveUsable("Aquatic Form")
if not CleveRoids.NampowerAPI.features.hasPlayerIsSwimming then
if not CleveRoids._swimmingErrorShown then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [swimming] conditional requires Nampower v2.36.0 or newer.", 1, 0.5, 0.5)
CleveRoids._swimmingErrorShown = true
end
return false
end
return PlayerIsSwimming() == 1
end,
-- [noswimming] - Player is NOT swimming (Nampower v2.36+)
noswimming = function(conditionals)
-- Check if "Aquatic Form" is NOT usable
return not CleveRoids.IsReactiveUsable("Aquatic Form")
if not CleveRoids.NampowerAPI.features.hasPlayerIsSwimming then
if not CleveRoids._swimmingErrorShown then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The [swimming] conditional requires Nampower v2.36.0 or newer.", 1, 0.5, 0.5)
CleveRoids._swimmingErrorShown = true
end
return false
end
return PlayerIsSwimming() ~= 1
end,
distance = function(conditionals)
+145 -37
View File
@@ -490,6 +490,10 @@ frame:SetScript("OnEvent", function()
if type(CleveRoidMacros.refresh) ~= "number" then
CleveRoidMacros.refresh = 5
end
if type(CleveRoidMacros.macrocheck) ~= "number" then
CleveRoidMacros.macrocheck = 1 -- enabled by default
end
end)
-- Queues a full update of all action bars.
@@ -805,10 +809,10 @@ function CleveRoids.TestForActiveAction(actions)
if action.sequence then
newSequence = action.sequence
newActiveAction = CleveRoids.GetCurrentSequenceAction(newSequence)
-- Check if current step is null/pass/noop - if so, skip this sequence for icon
-- Check if current step is a stop step - if so, skip this sequence for icon
if newActiveAction and newActiveAction.action then
local stepLower = string.lower(CleveRoids.Trim(newActiveAction.action))
if stepLower == "null" or stepLower == "pass" or stepLower == "noop" then
if stepLower == "nil" or stepLower == "null" or stepLower == "pass" or stepLower == "noop" then
hasActive = false
newActiveAction = nil
newSequence = nil
@@ -1877,17 +1881,11 @@ function CleveRoids.ParseMsg(msg)
-- With a condition block present, build out the conditionals table
-- optional spam/cancel flags (apply only when we actually have a [] block)
-- ! prefix with explicit conditionals: store as a flag for execution-time anti-toggle,
-- rather than injecting an implicit conditional that gets AND'd with user conditions.
-- The anti-toggle check happens in DoWithConditionals before CastSpellByName.
if noSpam and noSpam ~= "" then
local spamCond = CleveRoids.GetSpammableConditional(action)
if spamCond then
conditionals[spamCond] = { action }
-- Also create _groups entry so Multi() finds it when combined with explicit conditionals
if not conditionals._groups then
conditionals._groups = {}
end
conditionals._groups[spamCond] = { { values = { action }, operator = "OR" } }
end
conditionals.noSpam = true
end
if cancelAura and cancelAura ~= "" then
conditionals.cancelaura = action
@@ -2372,7 +2370,7 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo
conditionals.target = origTarget
return false
end
-- Set target to the found GUID for soft-casting via SuperWoW
-- Set target to the found GUID for soft-casting via SuperWoW or Nampower v2.37+
conditionals.target = scanResult
-- Don't need to retarget since we're using GUID directly
needRetarget = false
@@ -2404,7 +2402,7 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo
end
end
if conditionals.target ~= nil and targetBeforeAction and not (CleveRoids.hasSuperwow and action == CastSpellByName) then
if conditionals.target ~= nil and targetBeforeAction and not (action == CastSpellByName and (CleveRoids.hasSuperwow or CleveRoids.hasCastSpellByNameUnitToken)) then
if not UnitIsUnit("target", conditionals.target) then
if SpellIsTargeting() then
SpellStopCasting()
@@ -2477,11 +2475,33 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo
end
end
if action == CastSpellByName then
-- Special case: !Attack should use AttackTarget() which doesn't toggle
-- This is more reliable than CastSpellByName("Attack") which toggles on/off
-- ! prefix anti-toggle: prevent CastSpellByName from toggling off active spells.
-- For Attack/AutoShot/Shoot, use the non-toggling AttackTarget() API.
-- For self-buffs/shapeshifts, skip if already active on the player.
if conditionals.noSpam then
local spamType = CleveRoids.spamConditions[msg]
if spamType == "checkchanneled" then
-- Attack/AutoShot/Shoot: use non-toggling API
if msg == CleveRoids.Localized.Attack then
AttackTarget()
if needRetarget then TargetLastTarget() end
conditionals.target = origTarget
return true
end
-- AutoShot/Shoot: checkchanneled prevents toggle (already handled below)
else
-- Self-buff/shapeshift: skip if already active on player
if CleveRoids.ValidatePlayerBuff(msg) then
if needRetarget then TargetLastTarget() end
conditionals.target = origTarget
return false
end
end
end
-- Legacy path: !Attack without explicit conditionals (checkchanneled injected)
if msg == CleveRoids.Localized.Attack and conditionals.checkchanneled then
AttackTarget()
elseif CleveRoids.hasSuperwow and conditionals.target then
elseif (CleveRoids.hasSuperwow or CleveRoids.hasCastSpellByNameUnitToken) and conditionals.target then
-- Let Nampower DLL handle queuing natively via its CastSpellByName hook
CastSpellByName(castMsg, conditionals.target)
else
@@ -2581,6 +2601,23 @@ function CleveRoids.DoTarget(msg)
-- Save original target GUID for potential restoration (SuperWoW returns GUID as 2nd value)
local _, originalTargetGuid = UnitExists("target")
-- Handle [multiscan:priority] - use ResolveMultiscanTarget for enemy scanning
-- ResolveMultiscanTarget handles its own target save/restore internally
if conditionals.multiscan then
local scanResult = CleveRoids.ResolveMultiscanTarget(conditionals, conditionals.target)
if scanResult then
TargetUnit(scanResult)
return true
end
-- No match found - restore original target if scanning changed it
if originalTargetGuid and UnitExists(originalTargetGuid) then
TargetUnit(originalTargetGuid)
elseif not originalTargetGuid then
ClearTarget()
end
return false
end
-- Track if an explicit target was specified via @unit syntax
local explicitTarget = conditionals.target
@@ -3181,16 +3218,15 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand)
end
end
-- Item not found via v2.18 lookup
-- Item not found via v2.18 lookup - fall through to legacy path
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cffff8800[EquipLog] Item '" .. tostring(msg) .. "' not found via v2.18 FindPlayerItemSlot|r")
CleveRoids.Print("|cffff8800[EquipLog] Item '" .. tostring(msg) .. "' not found via v2.18 FindPlayerItemSlot, trying legacy fallback|r")
-- Debug: Try direct FindPlayerItemSlot call
if FindPlayerItemSlot then
local bag, slot = FindPlayerItemSlot(msg)
CleveRoids.Print("|cff888888[EquipLog] Direct FindPlayerItemSlot('" .. msg .. "') = bag:" .. tostring(bag) .. " slot:" .. tostring(slot) .. "|r")
end
end
return false
end
-- Fallback for older Nampower versions
@@ -3294,13 +3330,25 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand)
if EquipItemByName then
local ok = pcall(EquipItemByName, msg, invslot)
if ok then
-- Invalidate cache entry if it exists
if CleveRoids.Items then
CleveRoids.Items[msg] = nil
CleveRoids.Items[string_lower(msg)] = nil
-- Verify the item actually landed in the target slot
-- EquipItemByName may silently no-op for same-named items in paired slots
-- (e.g., dual-wielding Scimitar: MH copy found first, "equipped" to same slot)
local newLink = GetInventoryItemLink("player", invslot)
if newLink then
local _, _, newName = string_find(newLink, "|h%[(.-)%]|h")
if newName and string_lower(newName) == string_lower(msg) then
if CleveRoids.Items then
CleveRoids.Items[msg] = nil
CleveRoids.Items[string_lower(msg)] = nil
end
InvalidateDisplacedItem()
return true
end
end
-- Verification failed - EquipItemByName didn't place item in target slot
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cffff8800[EquipLog] EquipItemByName('" .. msg .. "', " .. invslot .. ") succeeded but item not in target slot, trying fallback|r")
end
InvalidateDisplacedItem()
return true
end
end
@@ -3336,12 +3384,23 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand)
if item.name and EquipItemByName and item.name ~= msg then
local ok = pcall(EquipItemByName, item.name, invslot)
if ok then
if CleveRoids.Items then
CleveRoids.Items[item.name] = nil
CleveRoids.Items[string_lower(item.name)] = nil
-- Verify the item actually landed in the target slot (same guard as above)
local newLink = GetInventoryItemLink("player", invslot)
if newLink then
local _, _, newName = string_find(newLink, "|h%[(.-)%]|h")
if newName and string_lower(newName) == string_lower(item.name) then
if CleveRoids.Items then
CleveRoids.Items[item.name] = nil
CleveRoids.Items[string_lower(item.name)] = nil
end
InvalidateDisplacedItem()
return true
end
end
-- Verification failed - fall through to manual cursor-based equip
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cffff8800[EquipLog] EquipItemByName('" .. item.name .. "', " .. invslot .. ") succeeded but item not in target slot, trying manual fallback|r")
end
InvalidateDisplacedItem()
return true
end
end
@@ -3634,15 +3693,15 @@ function CleveRoids.DoCastSequence(sequence)
))
end
-- Check for "null" step - does nothing, allows macro to fall through
-- Usage: /castsequence reset=target Sunder Armor, null
-- Check for stop steps - does nothing, allows macro to fall through
-- Sequence stays on this step until a reset condition fires (target/combat/time/modifier)
-- Usage: /castsequence reset=target Sunder Armor, nil
local actionLower = string.lower(CleveRoids.Trim(active.action))
if actionLower == "null" or actionLower == "pass" or actionLower == "noop" then
-- Update sequence state (so per-target tracking works)
if actionLower == "nil" or actionLower == "null" or actionLower == "pass" or actionLower == "noop" then
sequence.status = 0
sequence.lastUpdate = GetTime()
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[Sequence]|r Hit null step, falling through")
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[Sequence]|r Hit stop step, falling through")
end
-- Don't cast anything, just return - allows macro to continue to next line
return
@@ -3840,7 +3899,7 @@ function CleveRoids.OnUpdate(self)
-- Maintain legacy vars for any code that uses them directly
if CleveRoids._posHistoryCount >= 2 then
local prevIdx = math.mod((idx - 2), 4) + 1
local prevIdx = math.mod((idx - 2 + 4), 4) + 1
CleveRoids._previousPlayerPos = history[prevIdx]
CleveRoids._currentPlayerPos = entry
end
@@ -5149,6 +5208,18 @@ function CleveRoids.Frame:SPELLCAST_CHANNEL_START()
end
end
-- v2.38+: Override channelDuration with the accurate server-provided value stored by
-- SPELL_START_SELF (which fires before this event). This beats the tooltip scan or
-- UNIT_CASTEVENT cast_time fallback, because the server already applied all modifiers.
if CleveRoids._v238ChannelDuration then
if not spellId or not CleveRoids._v238ChannelSpellId
or CleveRoids._v238ChannelSpellId == spellId then
CleveRoids.channelDuration = CleveRoids._v238ChannelDuration
end
CleveRoids._v238ChannelDuration = nil
CleveRoids._v238ChannelSpellId = nil
end
-- Force immediate action update
CleveRoids.TestForAllActiveActions()
end
@@ -5785,11 +5856,13 @@ SlashCmdList["CLEVEROID"] = function(msg)
CleveRoids.Print("Current Settings:")
DEFAULT_CHAT_FRAME:AddMessage("realtime (force fast updates, CPU intensive) = " .. CleveRoidMacros.realtime .. " (Default: 0)")
DEFAULT_CHAT_FRAME:AddMessage("refresh (updates per second) = " .. CleveRoidMacros.refresh .. " (Default: 5)")
DEFAULT_CHAT_FRAME:AddMessage("macrocheck (syntax checker) = " .. CleveRoidMacros.macrocheck .. " (Default: 1)")
DEFAULT_CHAT_FRAME:AddMessage("debug (show learning messages) = " .. (CleveRoids.debug and "1" or "0") .. " (Default: 0)")
DEFAULT_CHAT_FRAME:AddMessage(" ")
CleveRoids.Print("Available Commands:")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid realtime 0 or 1 - Force realtime updates")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid refresh X - Set refresh rate (1-10 updates/sec)")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid macrocheck 0 or 1 - Enable/disable macro syntax checker")
if CleveRoids.hasSuperwow then
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid learn <spellID> <duration> - Manually set spell duration")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid forget <spellID|all> - Forget learned duration(s)")
@@ -5824,6 +5897,8 @@ SlashCmdList["CLEVEROID"] = function(msg)
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid tankdebug - Debug tank targeting conditionals')
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00All-Caster Aura Tracking:|r")
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid auradebug - Debug buff/debuff time tracking from all casters')
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Overflow Buff Frame:|r")
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid overflowtest - Toggle overflow buff frame test mode')
return
end
@@ -5854,6 +5929,23 @@ SlashCmdList["CLEVEROID"] = function(msg)
return
end
-- macrocheck
if cmd == "macrocheck" then
local num = tonumber(val)
if num == 0 or num == 1 then
CleveRoidMacros.macrocheck = num
if num == 1 then
CleveRoids.Print("Macro syntax checker |cff00ff00enabled|r")
else
CleveRoids.Print("Macro syntax checker |cffff0000disabled|r (reload UI to take effect)")
end
else
CleveRoids.Print("Usage: /cleveroid macrocheck 0 or 1 - Enable/disable macro syntax checker (Default: 1)")
CleveRoids.Print("Current macrocheck = " .. tostring(CleveRoidMacros.macrocheck))
end
return
end
-- learn (manual set duration)
if cmd == "learn" then
if not CleveRoids.hasSuperwow then
@@ -6559,6 +6651,22 @@ SlashCmdList["CLEVEROID"] = function(msg)
return
end
-- overflowtest (toggle overflow buff frame test mode)
if cmd == "overflowtest" then
if CleveRoids.ToggleOverflowTest then
CleveRoids.ToggleOverflowTest()
else
CleveRoids.Print("|cffff0000OverflowBuffFrame extension not loaded|r")
end
return
end
-- Delegate to extension console handlers (chain pattern)
if CleveRoids.HandleConsoleCommand then
CleveRoids.HandleConsoleCommand(msg)
return
end
-- Unknown command fallback
CleveRoids.Print("Usage:")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid - Show current settings")
+361 -6
View File
@@ -29,11 +29,21 @@ local pendingValidation = false
local lastSelectedMacro = nil
local updateFrame = nil
local hooked = false
local measureFs = nil -- hidden FontString for measuring text width (wrap detection)
-- Cache for current errors (avoids re-validation on every frame)
local currentErrors = nil
local nameHighlight = nil -- Yellow backdrop behind macro name when name has errors
-- Whitelist GUI state
local whitelistButton = nil
local whitelistPopup = nil
local WHITELIST_ROW_POOL_SIZE = 8
local whitelistRows = {} -- pre-allocated label+remove rows
-- Forward declarations (defined later, referenced by whitelist GUI)
local RequestValidation
-- ============================================================================
-- Error Panel (Option A)
-- ============================================================================
@@ -45,7 +55,7 @@ local function CreateErrorPanel()
-- Anchor to the scroll frame (text edit area) instead of the full MacroFrame,
-- which can extend far below the visible UI on extended macro clients
local scrollRef = MacroFrameScrollFrame or MacroFrame
panel:SetPoint("TOPLEFT", scrollRef, "BOTTOMLEFT", 0, -27)
panel:SetPoint("TOPLEFT", scrollRef, "BOTTOMLEFT", -20, -27)
panel:SetPoint("TOPRIGHT", scrollRef, "BOTTOMRIGHT", 0, -27)
panel:SetHeight(1)
panel:SetFrameStrata("DIALOG")
@@ -186,18 +196,61 @@ local function UpdateLineHighlights(errors)
local editWidth = MacroFrameText:GetWidth()
if editWidth < 10 then editWidth = 260 end -- fallback
-- Create measurement FontString lazily (same font as EditBox, hidden)
if not measureFs then
measureFs = (MacroFrame or UIParent):CreateFontString(nil, "OVERLAY")
measureFs:Hide()
end
local fontPath, fontSize, fontFlags = MacroFrameText:GetFont()
if fontPath then
measureFs:SetFont(fontPath, fontSize, fontFlags)
end
-- Split macro text into logical lines for wrap calculation
local text = MacroFrameText:GetText() or ""
local logicalLines = {}
local lineStart = 1
while true do
local nlPos = string.find(text, "\n", lineStart, true)
if nlPos then
table.insert(logicalLines, string.sub(text, lineStart, nlPos - 1))
lineStart = nlPos + 1
else
table.insert(logicalLines, string.sub(text, lineStart))
break
end
end
-- Calculate visual line count for each logical line
local visualCounts = {}
for i, line in ipairs(logicalLines) do
if line == "" or editWidth <= 0 then
visualCounts[i] = 1
else
measureFs:SetText(line)
local textWidth = measureFs:GetStringWidth()
visualCounts[i] = math.max(1, math.ceil(textWidth / editWidth))
end
end
local highlightIdx = 1
for lineNum, _ in pairs(errorLines) do
if highlightIdx > HIGHLIGHT_POOL_SIZE then break end
local tex = lineHighlights[highlightIdx]
-- Position: top text inset + (lineNum-1) lines down
local yOffset = -topInset - ((lineNum - 1) * lineHeight)
-- Y offset: sum visual lines of all preceding logical lines
local yLines = 0
for i = 1, lineNum - 1 do
yLines = yLines + (visualCounts[i] or 1)
end
local yOffset = -topInset - (yLines * lineHeight)
local highlightHeight = (visualCounts[lineNum] or 1) * lineHeight
tex:ClearAllPoints()
tex:SetPoint("TOPLEFT", MacroFrameText, "TOPLEFT", -2, yOffset)
tex:SetWidth(editWidth + 4)
tex:SetHeight(lineHeight)
tex:SetHeight(highlightHeight)
tex:Show()
highlightIdx = highlightIdx + 1
@@ -246,6 +299,300 @@ local function UpdateNameHighlight(hasNameErrors)
end
end
-- ============================================================================
-- Command Whitelist GUI
-- ============================================================================
local function RefreshWhitelistDisplay()
if not whitelistPopup then return end
local commands = CleveRoids.GetWhitelistedCommandsList()
local count = table.getn(commands)
-- Update rows
for i = 1, WHITELIST_ROW_POOL_SIZE do
local row = whitelistRows[i]
if row then
if i <= count then
row.label:SetText(commands[i])
row.frame:Show()
else
row.label:SetText("")
row.frame:Hide()
end
end
end
-- Show overflow indicator
local overflowText = whitelistPopup.overflowText
if count > WHITELIST_ROW_POOL_SIZE then
overflowText:SetText("|cff888888... and " .. (count - WHITELIST_ROW_POOL_SIZE) .. " more|r")
overflowText:Show()
else
overflowText:SetText("")
overflowText:Hide()
end
-- Dynamic height: title(20) + gap(8) + editbox(18) + rows(18 each) + padding
local rowsShown = count
if rowsShown > WHITELIST_ROW_POOL_SIZE then rowsShown = WHITELIST_ROW_POOL_SIZE end
local height = 8 + 20 + 8 + 18 + 2 + (rowsShown * 18)
if count > WHITELIST_ROW_POOL_SIZE then
height = height + 14
end
height = height + 8 -- bottom padding
whitelistPopup:SetHeight(height)
-- Re-trigger error checking so results update in real time
RequestValidation()
end
local function CreateWhitelistPopup()
if whitelistPopup then return end
local popup = CreateFrame("Frame", "CleveRoidsWhitelistPopup", UIParent)
popup:SetWidth(220)
popup:SetHeight(120)
popup:SetPoint("CENTER", UIParent, "CENTER", 0, 100)
popup:SetFrameStrata("FULLSCREEN_DIALOG")
popup:SetMovable(true)
popup:EnableMouse(true)
-- pfUI pixel-perfect flat style
popup:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8x8",
edgeFile = "Interface\\Buttons\\WHITE8x8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 0, right = 0, top = 0, bottom = 0 },
})
popup:SetBackdropColor(0.08, 0.08, 0.1, 0.95)
popup:SetBackdropBorderColor(0.2, 0.2, 0.2, 1)
popup:Hide()
-- Drag handling
popup:RegisterForDrag("LeftButton")
popup:SetScript("OnDragStart", function()
this:StartMoving()
end)
popup:SetScript("OnDragStop", function()
this:StopMovingOrSizing()
end)
-- Title
local title = popup:CreateFontString(nil, "OVERLAY")
title:SetFont("Fonts\\FRIZQT__.TTF", 11, "OUTLINE")
title:SetPoint("TOPLEFT", popup, "TOPLEFT", 8, -8)
title:SetText("Whitelisted Commands")
title:SetTextColor(0.9, 0.8, 0.5, 1)
-- Close button (flat X)
local closeBtn = CreateFrame("Button", nil, popup)
closeBtn:SetWidth(16)
closeBtn:SetHeight(16)
closeBtn:SetPoint("TOPRIGHT", popup, "TOPRIGHT", -4, -4)
local closeLbl = closeBtn:CreateFontString(nil, "OVERLAY")
closeLbl:SetFont("Fonts\\FRIZQT__.TTF", 12, "OUTLINE")
closeLbl:SetPoint("CENTER", 0, 0)
closeLbl:SetText("x")
closeLbl:SetTextColor(0.6, 0.6, 0.6, 1)
closeBtn:SetScript("OnEnter", function()
closeLbl:SetTextColor(1, 0.3, 0.3, 1)
end)
closeBtn:SetScript("OnLeave", function()
closeLbl:SetTextColor(0.6, 0.6, 0.6, 1)
end)
closeBtn:SetScript("OnClick", function()
popup:Hide()
end)
-- EditBox (flat style)
local editBox = CreateFrame("EditBox", "CleveRoidsWhitelistEditBox", popup)
editBox:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8)
editBox:SetWidth(148)
editBox:SetHeight(18)
editBox:SetAutoFocus(false)
editBox:SetMaxLetters(60)
editBox:SetFont("Fonts\\FRIZQT__.TTF", 10, "")
editBox:SetTextColor(1, 1, 1, 1)
editBox:SetTextInsets(4, 4, 0, 0)
editBox:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8x8",
edgeFile = "Interface\\Buttons\\WHITE8x8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 0, right = 0, top = 0, bottom = 0 },
})
editBox:SetBackdropColor(0.05, 0.05, 0.07, 0.9)
editBox:SetBackdropBorderColor(0.3, 0.3, 0.35, 1)
-- Add button (flat style)
local addBtn = CreateFrame("Button", nil, popup)
addBtn:SetPoint("LEFT", editBox, "RIGHT", 4, 0)
addBtn:SetWidth(40)
addBtn:SetHeight(18)
addBtn:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8x8",
edgeFile = "Interface\\Buttons\\WHITE8x8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 0, right = 0, top = 0, bottom = 0 },
})
addBtn:SetBackdropColor(0.15, 0.15, 0.15, 0.9)
addBtn:SetBackdropBorderColor(0.3, 0.3, 0.35, 1)
local addLbl = addBtn:CreateFontString(nil, "OVERLAY")
addLbl:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE")
addLbl:SetPoint("CENTER", 0, 0)
addLbl:SetText("Add")
addLbl:SetTextColor(0.8, 0.8, 0.8, 1)
addBtn:SetScript("OnEnter", function()
this:SetBackdropColor(0.25, 0.25, 0.25, 0.95)
this:SetBackdropBorderColor(0.45, 0.45, 0.5, 1)
addLbl:SetTextColor(1, 1, 1, 1)
end)
addBtn:SetScript("OnLeave", function()
this:SetBackdropColor(0.15, 0.15, 0.15, 0.9)
this:SetBackdropBorderColor(0.3, 0.3, 0.35, 1)
addLbl:SetTextColor(0.8, 0.8, 0.8, 1)
end)
local function AddCommand()
local text = editBox:GetText()
if not text or CleveRoids.Trim(text) == "" then return end
text = CleveRoids.Trim(text)
CleveRoids.AddWhitelistedCommand(text)
editBox:SetText("")
editBox:ClearFocus()
RefreshWhitelistDisplay()
end
addBtn:SetScript("OnClick", AddCommand)
editBox:SetScript("OnEnterPressed", function()
AddCommand()
end)
editBox:SetScript("OnEscapePressed", function()
this:ClearFocus()
end)
-- Anchor for rows: below the editbox row
local rowAnchor = editBox
-- Pre-allocate row pool
for i = 1, WHITELIST_ROW_POOL_SIZE do
local rowFrame = CreateFrame("Frame", nil, popup)
rowFrame:SetHeight(16)
rowFrame:SetPoint("TOPLEFT", rowAnchor, "BOTTOMLEFT", 0, -2)
rowFrame:SetPoint("RIGHT", popup, "RIGHT", -8, 0)
rowFrame:Hide()
local label = rowFrame:CreateFontString(nil, "OVERLAY")
label:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE")
label:SetPoint("LEFT", rowFrame, "LEFT", 2, 0)
label:SetJustifyH("LEFT")
label:SetTextColor(0.7, 0.7, 0.7, 1)
-- Flat X remove button
local removeBtn = CreateFrame("Button", nil, rowFrame)
removeBtn:SetWidth(14)
removeBtn:SetHeight(14)
removeBtn:SetPoint("RIGHT", rowFrame, "RIGHT", -1, 0)
local removeLbl = removeBtn:CreateFontString(nil, "OVERLAY")
removeLbl:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE")
removeLbl:SetPoint("CENTER", 0, 0)
removeLbl:SetText("x")
removeLbl:SetTextColor(0.5, 0.5, 0.5, 1)
removeBtn.removeLbl = removeLbl
removeBtn.rowIndex = i
removeBtn:SetScript("OnEnter", function()
this.removeLbl:SetTextColor(1, 0.3, 0.3, 1)
end)
removeBtn:SetScript("OnLeave", function()
this.removeLbl:SetTextColor(0.5, 0.5, 0.5, 1)
end)
removeBtn:SetScript("OnClick", function()
local idx = this.rowIndex
local row = whitelistRows[idx]
if row and row.label then
local cmd = row.label:GetText()
if cmd and cmd ~= "" then
CleveRoids.RemoveWhitelistedCommand(cmd)
RefreshWhitelistDisplay()
end
end
end)
whitelistRows[i] = { frame = rowFrame, label = label, removeBtn = removeBtn }
rowAnchor = rowFrame
end
-- Overflow text
local overflowText = popup:CreateFontString(nil, "OVERLAY")
overflowText:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE")
overflowText:SetPoint("TOPLEFT", whitelistRows[WHITELIST_ROW_POOL_SIZE].frame, "BOTTOMLEFT", 2, -2)
overflowText:SetJustifyH("LEFT")
overflowText:SetTextColor(0.5, 0.5, 0.5, 1)
overflowText:Hide()
popup.overflowText = overflowText
whitelistPopup = popup
end
local function CreateWhitelistButton()
if whitelistButton then return end
if not MacroFrame then return end
local btn = CreateFrame("Button", "CleveRoidsWhitelistButton", MacroFrame)
btn:SetWidth(80)
btn:SetHeight(18)
btn:SetPoint("BOTTOMRIGHT", MacroFrame, "BOTTOMRIGHT", 8, 52)
btn:SetFrameStrata("DIALOG")
-- pfUI pixel-perfect flat style
btn:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8x8",
edgeFile = "Interface\\Buttons\\WHITE8x8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 0, right = 0, top = 0, bottom = 0 },
})
btn:SetBackdropColor(0.1, 0.1, 0.1, 0.85)
btn:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
local label = btn:CreateFontString(nil, "OVERLAY")
label:SetFont("Fonts\\FRIZQT__.TTF", 10, "OUTLINE")
label:SetPoint("CENTER", btn, "CENTER", 0, 0)
label:SetText("Whitelist")
label:SetTextColor(0.8, 0.8, 0.8, 1)
btn.label = label
-- Hover highlight
btn:SetScript("OnEnter", function()
this:SetBackdropColor(0.2, 0.2, 0.2, 0.95)
this:SetBackdropBorderColor(0.45, 0.45, 0.5, 1)
this.label:SetTextColor(1, 1, 1, 1)
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:SetText("Command Whitelist")
GameTooltip:AddLine("Add third-party addon commands so the", 1, 1, 1, true)
GameTooltip:AddLine("error checker stops flagging them.", 1, 1, 1, true)
GameTooltip:Show()
end)
btn:SetScript("OnLeave", function()
this:SetBackdropColor(0.1, 0.1, 0.1, 0.85)
this:SetBackdropBorderColor(0.3, 0.3, 0.3, 1)
this.label:SetTextColor(0.8, 0.8, 0.8, 1)
GameTooltip:Hide()
end)
btn:SetScript("OnClick", function()
CreateWhitelistPopup()
if whitelistPopup:IsVisible() then
whitelistPopup:Hide()
else
RefreshWhitelistDisplay()
whitelistPopup:Show()
end
end)
whitelistButton = btn
end
-- ============================================================================
-- Validation & Debounce
-- ============================================================================
@@ -382,7 +729,7 @@ local function RunValidation()
UpdateNameHighlight(hasNameErrors)
end
local function RequestValidation()
RequestValidation = function()
lastKeystroke = GetTime()
pendingValidation = true
end
@@ -563,6 +910,10 @@ local function ClearAll()
if nameHighlight then
nameHighlight:Hide()
end
if whitelistPopup then
whitelistPopup:Hide()
end
end
-- ============================================================================
@@ -591,8 +942,9 @@ local function InstallHooks()
if origOnShow then
origOnShow()
end
-- Create panel lazily on first show
-- Create UI lazily on first show
CreateErrorPanel()
CreateWhitelistButton()
-- Reset state and validate
lastSelectedMacro = MacroFrame.selectedMacro
RunValidation()
@@ -633,6 +985,9 @@ function Extension.OnAddonLoaded()
end
function Extension.OnLoad()
-- Skip if macro checker is disabled
if CleveRoidMacros and CleveRoidMacros.macrocheck == 0 then return end
-- Skip if SuperMacro is loaded (detected at load time)
if SuperMacroFrame ~= nil then return end
+635
View File
@@ -0,0 +1,635 @@
--[[
OverflowBuffFrame Extension
Two frames displaying server-side overflow buffs (no client aura slot) for
the player and the current target (if a group/party member).
Player data: CleveRoids.OverflowBuffs (AURA_CAST_ON_SELF when buff-capped).
Target data: CleveRoids.AllCasterAuraTracking filtered by UnitBuff visibility.
Each frame shows 2 rows of 8 icons = 16 slots.
Author: Mewtiny
License: MIT License
]]
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
-- ============================================================================
-- Constants
-- ============================================================================
local ICON_SIZE = 24
local ICON_SPACING = 2
local ICONS_PER_ROW = 8
local NUM_ROWS = 2
local ICONS_PER_FRAME = ICONS_PER_ROW * NUM_ROWS -- 16
local ROW_SPACING = 16 -- vertical gap between rows (icon + duration text)
local DURATION_FONT_SIZE = 10
local DURATION_OFFSET_Y = -3
local UPDATE_INTERVAL = 0.2
local LABEL_HEIGHT = 12
-- ============================================================================
-- State
-- ============================================================================
local playerFrame = nil
local targetFrame = nil
local playerIcons = {} -- [1..16]
local targetIcons = {} -- [1..16]
local playerLabelFs = nil
local targetLabelFs = nil
local updateFrame = nil
local updateElapsed = 0
local testMode = false
local dirty = false -- Flag for deferred rebuild (avoids event ordering race)
local lastPlayerCount = 0
local lastTargetCount = 0
-- ============================================================================
-- Duration Formatting (matches pfUI api.lua:1324 style)
-- ============================================================================
local function FormatDuration(seconds)
if not seconds or seconds <= 0 then return "" end
if seconds >= 100 then
return math.floor(seconds / 60 + 0.5) .. "m"
elseif seconds > 5 then
return math.floor(seconds + 0.5) .. "s"
else
return string.format("%.1f", seconds)
end
end
-- Forward declarations (needed by OnClick closures in CreateIconButton)
local RebuildFrame
-- ============================================================================
-- Icon Button Creation
-- ============================================================================
local function CreateIconButton(parent, index, iconTable)
local btn = CreateFrame("Button", nil, parent)
btn:SetWidth(ICON_SIZE)
btn:SetHeight(ICON_SIZE)
btn:RegisterForClicks("RightButtonUp")
-- Dark 1px border backdrop (pfUI style)
btn:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8x8",
edgeFile = "Interface\\Buttons\\WHITE8x8",
tile = false, tileSize = 0, edgeSize = 1,
insets = { left = 0, right = 0, top = 0, bottom = 0 },
})
btn:SetBackdropColor(0, 0, 0, 0.6)
btn:SetBackdropBorderColor(0.1, 0.1, 0.1, 0.9)
-- Icon texture (inset by 1px for border)
local icon = btn:CreateTexture(nil, "ARTWORK")
icon:SetPoint("TOPLEFT", btn, "TOPLEFT", 1, -1)
icon:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -1, 1)
icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
icon:Hide()
-- Duration text below icon
local dur = btn:CreateFontString(nil, "OVERLAY")
dur:SetFont("Fonts\\FRIZQT__.TTF", DURATION_FONT_SIZE, "OUTLINE")
dur:SetPoint("TOP", btn, "BOTTOM", 0, DURATION_OFFSET_Y)
dur:SetJustifyH("CENTER")
dur:SetTextColor(1, 1, 1, 1)
dur:SetText("")
-- Right-click to cancel player overflow buffs
btn:SetScript("OnClick", function()
local data = iconTable[index]
if not data or not data.spellId then return end
if data.source ~= "player" then return end
-- CancelPlayerAuraSpellId(spellId, ignoreMissing)
-- ignoreMissing=1 is required for overflow buffs that have no client aura slot
if CancelPlayerAuraSpellId then
CancelPlayerAuraSpellId(data.spellId, 1)
end
-- Remove from overflow tracking
if CleveRoids.OverflowBuffs then
CleveRoids.OverflowBuffs[data.spellId] = nil
end
-- Hide tooltip and rebuild
GameTooltip:Hide()
RebuildFrame()
end)
-- Tooltip
btn:SetScript("OnEnter", function()
local data = iconTable[index]
if not data or not data.spellId then return end
GameTooltip:SetOwner(btn, "ANCHOR_BOTTOMLEFT")
local spellName = SpellInfo and SpellInfo(data.spellId) or ("Spell " .. data.spellId)
if spellName then
local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "")
GameTooltip:AddLine(baseName, 1, 1, 1)
end
if data.remaining and data.remaining > 0 then
GameTooltip:AddLine(FormatDuration(data.remaining) .. " remaining", 0.7, 0.7, 0.7)
end
if data.source == "player" then
GameTooltip:AddLine("Right-click to cancel", 0.8, 0.8, 0.5)
else
GameTooltip:AddLine("Overflow buff (target)", 0.5, 1.0, 0.8)
end
GameTooltip:Show()
end)
btn:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
btn:Hide()
return {
button = btn,
icon = icon,
duration = dur,
spellId = nil,
remaining = nil,
source = nil,
}
end
-- ============================================================================
-- Frame Creation
-- ============================================================================
-- Compute frame dimensions
local FRAME_WIDTH = ICONS_PER_ROW * (ICON_SIZE + ICON_SPACING) - ICON_SPACING
local FRAME_HEIGHT = LABEL_HEIGHT + NUM_ROWS * (ICON_SIZE + ROW_SPACING)
local function CreateBuffFrame(name, savedPosKey, defaultY, labelColor, iconTable)
local frame = CreateFrame("Frame", name, UIParent)
frame:SetWidth(FRAME_WIDTH)
frame:SetHeight(FRAME_HEIGHT)
frame:SetPoint("TOP", UIParent, "TOP", 0, defaultY)
frame:SetFrameStrata("MEDIUM")
frame:SetMovable(true)
frame:EnableMouse(false)
frame:Hide()
-- Section label
local label = frame:CreateFontString(nil, "OVERLAY")
label:SetFont("Fonts\\FRIZQT__.TTF", 9, "OUTLINE")
label:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
label:SetTextColor(labelColor[1], labelColor[2], labelColor[3], 0.8)
label:SetText("")
-- Create 16 icon slots in 2 rows of 8
for i = 1, ICONS_PER_FRAME do
iconTable[i] = CreateIconButton(frame, i, iconTable)
local row = math.floor((i - 1) / ICONS_PER_ROW)
local col = math.mod(i - 1, ICONS_PER_ROW)
local xOff = col * (ICON_SIZE + ICON_SPACING)
local yOff = -(LABEL_HEIGHT + row * (ICON_SIZE + ROW_SPACING))
iconTable[i].button:SetPoint("TOPLEFT", frame, "TOPLEFT", xOff, yOff)
end
-- Shift+drag to move
frame:SetScript("OnMouseDown", function()
if IsShiftKeyDown() then
frame:StartMoving()
end
end)
frame:SetScript("OnMouseUp", function()
frame:StopMovingOrSizing()
local _, _, _, x, y = frame:GetPoint(1)
CleveRoidMacros = CleveRoidMacros or {}
CleveRoidMacros[savedPosKey] = { x = x, y = y }
end)
-- Restore saved position
CleveRoidMacros = CleveRoidMacros or {}
if CleveRoidMacros[savedPosKey] then
frame:ClearAllPoints()
frame:SetPoint("TOP", UIParent, "TOP",
CleveRoidMacros[savedPosKey].x or 0,
CleveRoidMacros[savedPosKey].y or defaultY)
end
return frame, label
end
-- ============================================================================
-- Data Collection
-- ============================================================================
local function GetPlayerOverflowBuffs()
local results = {}
local overflowBuffs = CleveRoids.OverflowBuffs
if not overflowBuffs then return results end
local now = GetTime()
for spellId, entry in pairs(overflowBuffs) do
if entry.timestamp and entry.durationSec then
local remaining = entry.durationSec - (now - entry.timestamp)
if remaining > 0 then
table.insert(results, {
spellId = spellId,
remaining = remaining,
source = "player",
})
end
end
end
table.sort(results, function(a, b) return a.remaining > b.remaining end)
return results
end
local function GetTargetOverflowBuffs()
local results = {}
if not UnitExists("target") then return results end
if not testMode and not (UnitInParty("target") or UnitInRaid("target")) then return results end
local _, targetGuid = UnitExists("target")
if not targetGuid then return results end
local trackingData = CleveRoids.AllCasterAuraTracking
if not trackingData or not trackingData[targetGuid] then return results end
-- Build set of visible buff textures on target
local visibleBuffs = {}
for i = 1, 32 do
local texture = UnitBuff("target", i)
if not texture then break end
visibleBuffs[texture] = true
end
local now = GetTime()
local lib = CleveRoids.libdebuff
for spellId, auraData in pairs(trackingData[targetGuid]) do
if auraData.start and auraData.duration then
local remaining = auraData.duration + auraData.start - now
if remaining > 0 then
local isVisible = false
if not auraData._testEntry then
local spellIcon = lib and lib:GetCachedIcon(spellId)
isVisible = spellIcon and visibleBuffs[spellIcon]
end
if not isVisible then
table.insert(results, {
spellId = spellId,
remaining = remaining,
source = "target",
})
end
end
end
end
table.sort(results, function(a, b) return a.remaining > b.remaining end)
return results
end
-- ============================================================================
-- Layout
-- ============================================================================
local function PopulateFrame(frame, label, labelText, iconTable, buffs, countRef)
local count = table.getn(buffs)
if count > ICONS_PER_FRAME then count = ICONS_PER_FRAME end
local lib = CleveRoids.libdebuff
for i = 1, count do
local data = iconTable[i]
local buff = buffs[i]
local texture = lib and lib:GetCachedIcon(buff.spellId)
if texture then
data.icon:SetTexture(texture)
else
data.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
end
data.icon:Show()
data.spellId = buff.spellId
data.remaining = buff.remaining
data.source = buff.source
data.duration:SetText(FormatDuration(buff.remaining))
data.button:EnableMouse(true)
data.button:Show()
end
-- Hide unused
for i = count + 1, ICONS_PER_FRAME do
local data = iconTable[i]
data.button:Hide()
data.icon:Hide()
data.duration:SetText("")
data.spellId = nil
data.remaining = nil
data.source = nil
end
if count > 0 then
label:SetText(labelText)
frame:EnableMouse(true)
frame:Show()
else
label:SetText("")
frame:EnableMouse(false)
frame:Hide()
end
return count
end
RebuildFrame = function()
if not playerFrame then return end
local pBuffs = GetPlayerOverflowBuffs()
lastPlayerCount = PopulateFrame(playerFrame, playerLabelFs, "Overflow (You)", playerIcons, pBuffs)
local tBuffs = GetTargetOverflowBuffs()
local targetName = UnitExists("target") and UnitName("target") or "Target"
lastTargetCount = PopulateFrame(targetFrame, targetLabelFs, "Overflow (" .. targetName .. ")", targetIcons, tBuffs)
end
-- Refresh only duration text (no full rebuild)
local function RefreshDurations()
local anyExpired = false
local now = GetTime()
-- Player durations
if playerFrame and playerFrame:IsVisible() then
for i = 1, lastPlayerCount do
local data = playerIcons[i]
if data.spellId then
local entry = CleveRoids.OverflowBuffs and CleveRoids.OverflowBuffs[data.spellId]
if entry and entry.timestamp and entry.durationSec then
local remaining = entry.durationSec - (now - entry.timestamp)
if remaining > 0 then
data.remaining = remaining
data.duration:SetText(FormatDuration(remaining))
else
anyExpired = true
end
else
anyExpired = true
end
end
end
end
-- Target durations
if targetFrame and targetFrame:IsVisible() then
local targetGuid = nil
if UnitExists("target") then
local _, guid = UnitExists("target")
targetGuid = guid
end
for i = 1, lastTargetCount do
local data = targetIcons[i]
if data.spellId and targetGuid then
local remaining = CleveRoids.GetAllCasterAuraTimeRemaining(targetGuid, data.spellId) or 0
if remaining > 0 then
data.remaining = remaining
data.duration:SetText(FormatDuration(remaining))
else
anyExpired = true
end
else
anyExpired = true
end
end
end
if anyExpired then
RebuildFrame()
end
end
-- ============================================================================
-- Test Mode
-- ============================================================================
local TEST_PLAYER_SPELL_IDS = {
1126, -- Mark of the Wild
1243, -- Power Word: Fortitude
1461, -- Arcane Intellect
14752, -- Divine Spirit
976, -- Shadow Protection
6307, -- Blood Pact
20217, -- Blessing of Kings
19740, -- Blessing of Might
21849, -- Gift of the Wild
25898, -- Greater Blessing of Kings
25899, -- Greater Blessing of Sanctuary
25890, -- Greater Blessing of Light
10938, -- Greater Power Word: Fortitude
10157, -- Greater Arcane Intellect
20911, -- Blessing of Sanctuary
25782, -- Greater Blessing of Might
}
local TEST_TARGET_SPELL_IDS = {
467, -- Thorns
10060, -- Power Infusion
17007, -- Leader of the Pack
24932, -- Leader of the Pack aura
19506, -- Trueshot Aura
8936, -- Regrowth
774, -- Rejuvenation
139, -- Renew
1022, -- Blessing of Protection
6346, -- Fear Ward
1044, -- Blessing of Freedom
10958, -- Shadow Protection (rank)
27681, -- Prayer of Spirit
21562, -- Prayer of Fortitude
20914, -- Blessing of Sanctuary (rank)
25916, -- Greater Blessing of Wisdom
}
local testTargetGuid = nil
local testStartTime = 0 -- when test mode was enabled (for duration calc)
-- Remove test entries from a specific GUID in AllCasterAuraTracking
local function ClearTestTargetData(guid)
if not guid or not CleveRoids.AllCasterAuraTracking then return end
local targetData = CleveRoids.AllCasterAuraTracking[guid]
if not targetData then return end
for spellId, entry in pairs(targetData) do
if entry._testEntry then
targetData[spellId] = nil
end
end
if not next(targetData) then
CleveRoids.AllCasterAuraTracking[guid] = nil
end
end
-- Inject test entries for the current target
local function InjectTestTargetData()
if not UnitExists("target") then return end
local _, targetGuid = UnitExists("target")
if not targetGuid then return end
-- Clean up old test target if it changed
if testTargetGuid and testTargetGuid ~= targetGuid then
ClearTestTargetData(testTargetGuid)
end
testTargetGuid = targetGuid
CleveRoids.AllCasterAuraTracking = CleveRoids.AllCasterAuraTracking or {}
if not CleveRoids.AllCasterAuraTracking[targetGuid] then
CleveRoids.AllCasterAuraTracking[targetGuid] = {}
end
local targetDurations = { 8, 15, 25, 40, 55, 90, 150, 240, 400, 500, 700, 1000, 1300, 1600, 2000, 3000 }
for i = 1, table.getn(TEST_TARGET_SPELL_IDS) do
local spellId = TEST_TARGET_SPELL_IDS[i]
local dur = targetDurations[i] or 60
CleveRoids.AllCasterAuraTracking[targetGuid][spellId] = {
start = testStartTime,
duration = dur,
casterGuid = "test",
_testEntry = true,
}
end
end
local function EnableTestMode()
testMode = true
testStartTime = GetTime()
CleveRoids.OverflowBuffs = CleveRoids.OverflowBuffs or {}
local playerDurations = { 5, 10, 30, 45, 60, 120, 180, 300, 450, 600, 900, 1200, 1500, 1800, 2400, 3600 }
for i = 1, table.getn(TEST_PLAYER_SPELL_IDS) do
local spellId = TEST_PLAYER_SPELL_IDS[i]
local dur = playerDurations[i] or 60
CleveRoids.OverflowBuffs[spellId] = {
timestamp = testStartTime,
durationSec = dur,
_testEntry = true,
}
end
-- Inject target test data if we have a target
InjectTestTargetData()
CleveRoids.Print("|cff00ff00Overflow buff frame test mode enabled|r")
CleveRoids.Print(" Shift+drag to reposition the frames")
RebuildFrame()
end
local function DisableTestMode()
testMode = false
if CleveRoids.OverflowBuffs then
for spellId, entry in pairs(CleveRoids.OverflowBuffs) do
if entry._testEntry then
CleveRoids.OverflowBuffs[spellId] = nil
end
end
end
ClearTestTargetData(testTargetGuid)
testTargetGuid = nil
CleveRoids.Print("|cffff9900Overflow buff frame test mode disabled|r")
RebuildFrame()
end
local function ToggleTestMode()
if testMode then
DisableTestMode()
else
EnableTestMode()
end
end
CleveRoids.ToggleOverflowTest = ToggleTestMode
-- ============================================================================
-- OnUpdate Handler
-- ============================================================================
local function OnUpdate()
updateElapsed = updateElapsed + arg1
-- Check dirty flag every frame for responsive rebuilds after AURA_CAST events.
-- By the time OnUpdate fires, the core handler has already updated OverflowBuffs.
if dirty then
dirty = false
updateElapsed = 0
RebuildFrame()
return
end
if updateElapsed < UPDATE_INTERVAL then return end
updateElapsed = 0
RefreshDurations()
end
-- ============================================================================
-- Extension Registration
-- ============================================================================
local ext = CleveRoids.RegisterExtension("OverflowBuffFrame")
ext.OnTargetChanged = function()
-- Re-inject test data for the new target during test mode
if testMode then
InjectTestTargetData()
end
RebuildFrame()
end
ext.OnAuraCast = function()
-- Defer rebuild to OnUpdate: the core handler that populates OverflowBuffs
-- registers for AURA_CAST_ON_SELF later (PLAYER_ENTERING_WORLD) than this
-- extension (ADDON_LOADED), so it fires AFTER us. A direct RebuildFrame()
-- here would read stale data.
dirty = true
end
ext.OnGroupChanged = function()
RebuildFrame()
end
ext.OnPlayerDead = function()
if CleveRoids.OverflowBuffs then
if testMode then
for spellId, entry in pairs(CleveRoids.OverflowBuffs) do
if not entry._testEntry then
CleveRoids.OverflowBuffs[spellId] = nil
end
end
else
CleveRoids.OverflowBuffs = {}
end
end
RebuildFrame()
end
ext.OnLoad = function()
playerFrame, playerLabelFs = CreateBuffFrame(
"CleveRoidsOverflowPlayer", "overflowPlayerPos", -100,
{ 0.4, 0.7, 1.0 }, playerIcons)
targetFrame, targetLabelFs = CreateBuffFrame(
"CleveRoidsOverflowTarget", "overflowTargetPos", -170,
{ 0.4, 1.0, 0.7 }, targetIcons)
ext.RegisterEvent("PLAYER_TARGET_CHANGED", "OnTargetChanged")
ext.RegisterEvent("PARTY_MEMBERS_CHANGED", "OnGroupChanged")
ext.RegisterEvent("RAID_ROSTER_UPDATE", "OnGroupChanged")
ext.RegisterEvent("PLAYER_DEAD", "OnPlayerDead")
if CleveRoids.hasNampower then
ext.RegisterEvent("AURA_CAST_ON_SELF", "OnAuraCast")
end
-- Shared OnUpdate frame
updateFrame = CreateFrame("Frame")
updateFrame:SetScript("OnUpdate", OnUpdate)
RebuildFrame()
end
_G["CleveRoids"] = CleveRoids
+6 -1
View File
@@ -42,7 +42,7 @@ CleveRoids.spell_tracking = {}
-- Format: [casterGuid] = {spellID, spellName, icon, startTime, duration, endTime}
CleveRoids.castTracking = {}
-- pfUI 7.6+ with Nampower 2.27.2+ detected (GUID-based cast tracking available)
-- pfUI 7.6+ with Nampower 2.31.0+ detected (GUID-based cast tracking available)
CleveRoids.hasPfUI76 = false
-- Combo point tracking (initialized early for /cast hook)
@@ -123,6 +123,8 @@ CleveRoids.dynamicCmds = {
["/equip12"] = true,
["/equip13"] = true,
["/equip14"] = true,
["/applymain"] = true,
["/applyoff"] = true,
}
-- Equipment swap queue system
@@ -161,6 +163,7 @@ CleveRoids.ignoreKeywords = {
action = true,
ignoretooltip = true,
cancelaura = true,
noSpam = true, -- ! prefix flag: prevent toggle-off at execution time
_operators = true, -- Metadata for AND/OR operator tracking
_groups = true, -- Grouped conditional values for AND/OR evaluation
multiscan = true, -- Processed before Keywords loop (target resolution)
@@ -321,6 +324,8 @@ initFrame:SetScript("OnEvent", function()
CleveRoids.hasGetUnitData = API.features.hasGetUnitData
CleveRoids.hasGetSpellModifiers = API.features.hasGetSpellModifiers
CleveRoids.hasEnhancedSpellFunctions = API.features.hasEnhancedSpellFunctions
-- v2.37+: CastSpellByName supports unit token strings as 2nd param
CleveRoids.hasCastSpellByNameUnitToken = API.features.hasCastSpellByNameUnitToken
end
-- Initialize the API
+62 -2
View File
@@ -422,6 +422,59 @@ local VALID_COMMANDS = {
["/yes"] = true,
}
-- ============================================================================
-- Command Whitelist API (for third-party addon commands)
-- ============================================================================
local function GetWhitelistedCommands()
if not CleveRoidMacros then CleveRoidMacros = {} end
if not CleveRoidMacros.whitelistedCommands then
CleveRoidMacros.whitelistedCommands = {}
end
return CleveRoidMacros.whitelistedCommands
end
function CleveRoids.AddWhitelistedCommand(cmd)
if not cmd or cmd == "" then return end
cmd = string.lower(cmd)
-- Auto-prepend "/" if missing
if string.sub(cmd, 1, 1) ~= "/" then
cmd = "/" .. cmd
end
local whitelist = GetWhitelistedCommands()
whitelist[cmd] = true
end
function CleveRoids.RemoveWhitelistedCommand(cmd)
if not cmd or cmd == "" then return end
cmd = string.lower(cmd)
if string.sub(cmd, 1, 1) ~= "/" then
cmd = "/" .. cmd
end
local whitelist = GetWhitelistedCommands()
whitelist[cmd] = nil
end
function CleveRoids.IsWhitelistedCommand(cmd)
if not cmd or cmd == "" then return false end
cmd = string.lower(cmd)
if string.sub(cmd, 1, 1) ~= "/" then
cmd = "/" .. cmd
end
local whitelist = GetWhitelistedCommands()
return whitelist[cmd] == true
end
function CleveRoids.GetWhitelistedCommandsList()
local whitelist = GetWhitelistedCommands()
local list = {}
for cmd, _ in pairs(whitelist) do
table.insert(list, cmd)
end
table.sort(list)
return list
end
-- Commands that can have conditionals without actions
-- e.g., /petattack [harm] or /target [exists,hp:<=20]
local COMMANDS_NO_ACTION_NEEDED = {
@@ -679,7 +732,7 @@ local function validateLine(line, lineNum)
local _, _, cmd = safeStringFind(line, "^(/[a-z]+%d*)")
if cmd then
local lowerCmd = string.lower(cmd)
if not VALID_COMMANDS[lowerCmd] then
if not VALID_COMMANDS[lowerCmd] and not CleveRoids.IsWhitelistedCommand(lowerCmd) then
table.insert(localErrors, {
type = ERROR_TYPES.INVALID_COMMAND,
line = lineNum,
@@ -1102,6 +1155,11 @@ end
-- Slash command
SLASH_MACROCHECK1 = "/macrocheck"
SlashCmdList.MACROCHECK = function(msg)
if CleveRoidMacros and CleveRoidMacros.macrocheck == 0 then
CleveRoids.Print("Macro syntax checker is disabled. Enable with /cleveroid macrocheck 1")
return
end
local success, result = pcall(function()
msg = safeTrim(msg or "")
@@ -1117,4 +1175,6 @@ SlashCmdList.MACROCHECK = function(msg)
end
end
CleveRoids.Print("Macro syntax checker loaded. Use /macrocheck [macroname] or /macrocheck all")
if not CleveRoidMacros or CleveRoidMacros.macrocheck ~= 0 then
CleveRoids.Print("Macro syntax checker loaded. Use /macrocheck [macroname] or /macrocheck all")
end
+137 -1
View File
@@ -85,7 +85,43 @@
- Reads from NampowerSettings addon when available
- Falls back to CVars when addon not present
Current version: v2.33.0
Aura Cancel Functions (v2.34+):
- CancelPlayerAuraSlot(auraSlot) - Cancel buff/debuff by raw 0-based aura slot
- CancelPlayerAuraSpellId(spellId, [ignoreMissing]) - Cancel buff/debuff by spell ID
Pass ignoreMissing=1 to skip aura-slot presence check (for server-side
overflow buffs that have no client aura slot in the 48-slot array)
GUID String Fix & Talent Helper (v2.35+):
- GetUnitData/GetUnitField now return GUID-type fields (charm, summon, charmedBy,
summonedBy, createdBy, target, persuaded, channelObject) as hex strings
(e.g., "0x0600000012345678") instead of raw numbers to avoid Lua 64-bit precision loss
- LearnTalentRank(talentPage, talentIndex, rank) - Learn a specific talent rank directly
Player State Functions (v2.36+):
- PlayerIsMoving() - Returns 1 if player is moving, nil if stationary
- PlayerIsRooted() - Returns 1 if player is rooted, nil if not
- PlayerIsSwimming() - Returns 1 if player is swimming, nil if not
Unit Token Cast & Mouseover (v2.37+):
- CastSpellByName(spellName, unitToken) - 2nd param now accepts unit token strings
(e.g. "mouseover", "party1", "focus") in addition to GUIDs and 1 (self).
Enables soft-casting on unit tokens without changing the player's visible target.
- SetMouseoverUnit(unitToken) - Programmatically set the mouseover unit.
Previously only available via SuperWoW; now also provided by Nampower.
Spell Duration & Enhanced SPELL_START (v2.38+):
- GetSpellDuration(spellId, [ignoreModifiers]) - Returns duration in ms.
For channeling spells: channel duration. For others: first aura effect duration.
Pass ignoreModifiers=1 to get base duration (ignores talents/buffs).
Returns 0 if spell has no duration, nil if spellId invalid.
- SPELL_START_SELF / SPELL_START_OTHER now include two new parameters:
arg7 = duration (ms) - channel duration for channeling spells, 0 otherwise
arg8 = spellType - 0=Normal, 1=Channeling, 2=Autorepeating
Use spellType to distinguish channeling spells (SPELL_START_OTHER carries both
cast-time and channeling spells since there is no separate SPELL_CHANNEL_START
packet for other players).
Current version: v2.38.0
]]
local _G = _G or getfenv(0)
@@ -242,6 +278,27 @@ API.VERSION_REQUIREMENTS = {
-- v2.33+ - UPDATE_DURATION spellId parameter and GetCastInfo guid fix
["DurationEventSpellId"] = { 2, 33, 0 },
["GetCastInfoGuidFix"] = { 2, 33, 0 },
-- v2.34+ - Aura cancel functions
["CancelPlayerAuraSlot"] = { 2, 34, 0, "CancelPlayerAuraSlot" },
["CancelPlayerAuraSpellId"] = { 2, 34, 0, "CancelPlayerAuraSpellId" },
-- v2.35+ - GUID string fix and talent helper
["GuidStringFormat"] = { 2, 35, 0 }, -- GetUnitData/GetUnitField return GUID fields as hex strings
["LearnTalentRank"] = { 2, 35, 0, "LearnTalentRank" },
-- v2.36+ - Player state functions
["PlayerIsMoving"] = { 2, 36, 0, "PlayerIsMoving" },
["PlayerIsRooted"] = { 2, 36, 0, "PlayerIsRooted" },
["PlayerIsSwimming"] = { 2, 36, 0, "PlayerIsSwimming" },
-- v2.37+ - Unit token cast support and SetMouseoverUnit
["CastSpellByNameUnitToken"]= { 2, 37, 0 }, -- CastSpellByName accepts unit token strings as 2nd param
["SetMouseoverUnit"] = { 2, 37, 0, "SetMouseoverUnit" },
-- v2.38+ - GetSpellDuration and enhanced SPELL_START parameters
["GetSpellDuration"] = { 2, 38, 0, "GetSpellDuration" },
["SpellStartSpellType"] = { 2, 38, 0 }, -- duration + spellType params added to SPELL_START events
}
-- Check if a specific feature is available
@@ -382,6 +439,27 @@ local function InitializeFeatures()
f.hasDurationEventSpellId = API.HasFeature("DurationEventSpellId")
f.hasGetCastInfoGuidFix = API.HasFeature("GetCastInfoGuidFix")
-- v2.34+ Aura cancel functions
f.hasCancelPlayerAuraSlot = API.HasFeature("CancelPlayerAuraSlot")
f.hasCancelPlayerAuraSpellId = API.HasFeature("CancelPlayerAuraSpellId")
-- v2.35+ GUID string format and talent helper
f.hasGuidStringFormat = API.HasFeature("GuidStringFormat")
f.hasLearnTalentRank = API.HasFeature("LearnTalentRank")
-- v2.36+ Player state functions
f.hasPlayerIsMoving = API.HasFeature("PlayerIsMoving")
f.hasPlayerIsRooted = API.HasFeature("PlayerIsRooted")
f.hasPlayerIsSwimming = API.HasFeature("PlayerIsSwimming")
-- v2.37+ Unit token cast support and SetMouseoverUnit
f.hasCastSpellByNameUnitToken = API.HasFeature("CastSpellByNameUnitToken")
f.hasSetMouseoverUnit = API.HasFeature("SetMouseoverUnit")
-- v2.38+ GetSpellDuration and enhanced SPELL_START parameters
f.hasGetSpellDuration = API.HasFeature("GetSpellDuration")
f.hasSpellStartSpellType = API.HasFeature("SpellStartSpellType")
-- Runtime detection for enhanced spell functions (verify by testing)
if f.hasEnhancedSpellFunctions and GetSpellTexture then
local success, result = pcall(function()
@@ -2982,5 +3060,63 @@ function API.GetSpellPower(mode)
return _G.GetSpellPower(mode)
end
-- Get duration of a spell in milliseconds (v2.38+)
-- For channeling spells: returns the channel duration.
-- For non-channeling spells: returns the first aura effect duration.
-- spellId: spell ID to look up
-- ignoreModifiers: pass 1 to ignore talent/buff modifiers (returns base duration)
-- Returns: duration in ms (0 if spell has no duration), or nil if spellId invalid
function API.GetSpellDuration(spellId, ignoreModifiers)
if not API.features.hasGetSpellDuration or not _G.GetSpellDuration then
return nil
end
return _G.GetSpellDuration(spellId, ignoreModifiers)
end
--------------------------------------------------------------------------------
-- AURA CANCEL FUNCTIONS (v2.34+)
--------------------------------------------------------------------------------
-- Cancel a player aura by raw 0-based aura slot (v2.34+)
-- auraSlot: 0-31 for buffs, 32-47 for debuffs
function API.CancelPlayerAuraSlot(auraSlot)
if not API.features.hasCancelPlayerAuraSlot or not _G.CancelPlayerAuraSlot then
return false
end
_G.CancelPlayerAuraSlot(auraSlot)
return true
end
-- Cancel a player aura by spell ID (v2.34+)
-- spellId: the spell ID to cancel
-- ignoreMissing: pass 1 to skip aura-slot presence check (useful for buff-capped hidden auras)
function API.CancelPlayerAuraSpellId(spellId, ignoreMissing)
if not API.features.hasCancelPlayerAuraSpellId or not _G.CancelPlayerAuraSpellId then
return false
end
_G.CancelPlayerAuraSpellId(spellId, ignoreMissing)
return true
end
--------------------------------------------------------------------------------
-- TALENT HELPER (v2.35+)
--------------------------------------------------------------------------------
-- Learn a specific talent rank directly (v2.35+)
-- talentPage: 1-3 (talent tab)
-- talentIndex: 1-32 (talent position within tab)
-- rank: 1-5 (rank to learn)
function API.LearnTalentRank(talentPage, talentIndex, rank)
if not API.features.hasLearnTalentRank or not _G.LearnTalentRank then
return false
end
if not talentPage or not talentIndex or not rank then return false end
if talentPage < 1 or talentPage > 3 then return false end
if talentIndex < 1 or talentIndex > 32 then return false end
if rank < 1 or rank > 5 then return false end
_G.LearnTalentRank(talentPage, talentIndex, rank)
return true
end
-- Expose API globally for other addons
_G.CleveRoidsNampowerAPI = API
+80 -96
View File
@@ -154,63 +154,56 @@ These always evaluate against the player. Cannot be redirected with `@unit`.
| Conditional | Description | No | Multi | Example | NP | Addon |
|-------------|-------------|:--:|:-----:|---------|:--:|-------|
| ***State & Modifiers*** | | | | | | |
| `mod` | Modifier key pressed | ✅ | ✅ | `[mod:alt/ctrl]` | | |
| `combat` | In combat (player default, or unit) | ✅ | ✅ | `[combat]` `[combat:target]` | | |
| `form` / `stance` | Shapeshift form or stance index | ✅ | ✅ | `[form:1/3]` `[stance:2]` | | |
| `stealth` | In stealth (Rogue/Druid) | ✅ | — | `[stealth]` | | |
| `group` | Player is in group type | ✅ | ✅ | `[group:party/raid]` | | |
| `resting` | In rest area | ✅ | — | `[resting]` | | |
| `swimming` | Swimming (Druid only) | ✅ | — | `[swimming]` | | |
| `moving` | Moving / speed % | ✅ | ✅ | `[moving]` `[moving:>100&<200]` | | MonkeySpeed (speed %) |
| `zone` | Current zone name | ✅ | ✅ | `[zone:Ironforge/Stormwind]` | | |
| ***Resources*** | | | | | | |
| `myhp` | Player HP % | — | ✅ | `[myhp:<30]` `[myhp:>20&<50]` | | |
| `myrawhp` | Player raw HP value | — | ✅ | `[myrawhp:>1000]` | | |
| `myhplost` | Player HP deficit (max - current) | — | ✅ | `[myhplost:>500]` | | |
| `mypower` | Player mana/rage/energy % | — | ✅ | `[mypower:>50]` | | |
| `myrawpower` | Player raw power value | — | ✅ | `[myrawpower:>=40]` | | |
| `mypowerlost` | Player power deficit | — | ✅ | `[mypowerlost:>200]` | | |
| `druidmana` | Druid mana while shapeshifted | — | ✅ | `[druidmana:>=500]` | | |
| `combo` | Combo points | ✅ | ✅ | `[combo:>=4]` | | |
| `stat` | Player stat value | ✅ | ✅ | `[stat:agi>100]` `[stat:ap>1000]` | | |
| `mylevel` | Player level | — | ✅ | `[mylevel:=60]` | | |
| ***Player Auras*** | | | | | | |
| `mybuff` | Player has buff (with time/stacks) | ✅ | ✅ | `[mybuff:Thorns<5]` `[nomybuff:MotW/GotW]` | | |
| `mydebuff` | Player has debuff (with time/stacks) | ✅ | ✅ | `[mydebuff:Curse_of_Agony]` | | |
| `mybuffcount` | Player buff slot count (32 max) | ✅ | ✅ | `[mybuffcount:>15]` `[nomybuffcount:>28]` | | |
| `mybuffcapped` | Player buff bar full (32) | ✅ | — | `[nomybuffcapped]` | v2.20 | |
| `mydebuffcapped` | Player debuff bar full (16) | ✅ | — | `[nomydebuffcapped]` | v2.20 | |
| `mycc` | Player has CC effect | ✅ | ✅ | `[mycc:stun/silence]` `[nomycc]` | | |
| ***Spells & Cooldowns*** | | | | | | |
| `cooldown` | Spell/item CD remaining (ignores GCD) | ✅ | ✅ | `[cooldown:Sprint<5]` | | |
| `cdgcd` | Spell/item CD remaining (includes GCD) | ✅ | ✅ | `[cdgcd:Sprint>0]` | | |
| `gcd` | GCD is active / time remaining | ✅ | ✅ | `[gcd]` `[gcd:<1]` | | |
| `usable` | Spell/item is usable now | ✅ | ✅ | `[usable:Overpower]` | | |
| `reactive` | Reactive ability available (on bar) | ✅ | ✅ | `[reactive:Overpower]` | | |
| `known` | Spell/talent known (with rank) | ✅ | ✅ | `[known:Berserk]` `[known:Frostbolt>#2]` | | |
| `selfcasting` | Player is casting or channeling | ✅ | ✅ | `[selfcasting]` `[noselfcasting:Hearthstone]` | | |
| `checkcasting` | True if NOT casting (specific spell) | — | ✅ | `[checkcasting]` `[checkcasting:Frostbolt]` | | |
| `channeled` | Player is channeling | ✅ | ✅ | `[channeled]` `[channeled:Arcane_Missiles]` | | |
| `checkcasting` | True if NOT casting (specific spell) | — | ✅ | `[checkcasting]` `[checkcasting:Frostbolt]` | | |
| `checkchanneled` | True if NOT channeling (specific spell) | — | ✅ | `[checkchanneled]` `[checkchanneled:Evocation]` | | |
| `spellcasttime` | Spell cast time from tooltip (real-time) | ✅ | ✅ | `[spellcasttime:>1.5]` `[spellcasttime:Frostbolt<2]` | | |
| `queuedspell` | Spell is queued | ✅ | ✅ | `[queuedspell]` `[queuedspell:Fireball]` | v2.12 | |
| `onswingpending` | On-swing spell pending | ✅ | — | `[onswingpending]` | v2.12 | |
| ***Equipment*** | | | | | | |
| `combat` | In combat (player default, or unit) | ✅ | ✅ | `[combat]` `[combat:target]` | | |
| `combo` | Combo points | ✅ | ✅ | `[combo:>=4]` | | |
| `cooldown` | Spell/item CD remaining (ignores GCD) | ✅ | ✅ | `[cooldown:Sprint<5]` | | |
| `druidmana` | Druid mana while shapeshifted | — | ✅ | `[druidmana:>=500]` | | |
| `equipped` | Item or weapon type equipped | ✅ | ✅ | `[equipped:Daggers/Swords]` | | |
| `mhimbue` | Main hand has temporary imbue | ✅ | ✅ | `[mhimbue:Instant_Poison<300]` `[mhimbue:>#5]` | | |
| `ohimbue` | Off hand has temporary imbue | ✅ | ✅ | `[noohimbue]` `[ohimbue:Crippling_Poison]` | | |
| `pet` | Has active pet (with family) | ✅ | ✅ | `[pet]` `[pet:Cat/Wolf]` | | |
| ***Melee Tracking*** | | | | | | |
| `lastswing` | Player melee swing type/timing | ✅ | ✅ | `[lastswing:dodge]` `[lastswing:<2]` | v2.24 | |
| `incominghit` | Incoming attack type/timing | ✅ | ✅ | `[incominghit:crushing]` `[noincominghit:crit]` | v2.24 | |
| `swingtimer` | Swing timer % elapsed | ✅ | ✅ | `[swingtimer:>80]` | | SP_SwingTimer |
| `stimer` | Alias for swingtimer | ✅ | ✅ | `[stimer:<15]` | | SP_SwingTimer |
| `slamclip` / `noslamclip` | Slam now will/won't clip auto-attack | ✅ | — | `[noslamclip]` | | SP_SwingTimer |
| `nextslamclip` / `nonextslamclip` | Instant now will/won't cause next Slam to clip | ✅ | — | `[nonextslamclip]` | | SP_SwingTimer |
| ***Target Existence*** | | | | | | |
| `form` / `stance` | Shapeshift form or stance index | ✅ | ✅ | `[form:1/3]` `[stance:2]` | | |
| `gcd` | GCD is active / time remaining | ✅ | ✅ | `[gcd]` `[gcd:<1]` | | |
| `group` | Player is in group type | ✅ | ✅ | `[group:party/raid]` | | |
| `hastarget` | Player has a target | — | — | `[hastarget]` | | |
| `incominghit` | Incoming attack type/timing | ✅ | ✅ | `[incominghit:crushing]` `[noincominghit:crit]` | v2.24 | |
| `known` | Spell/talent known (with rank) | ✅ | ✅ | `[known:Berserk]` `[known:Frostbolt>#2]` | | |
| `lastswing` | Player melee swing type/timing | ✅ | ✅ | `[lastswing:dodge]` `[lastswing:<2]` | v2.24 | |
| `mhimbue` | Main hand has temporary imbue | ✅ | ✅ | `[mhimbue:Instant_Poison<300]` `[mhimbue:>#5]` | | |
| `mod` | Modifier key pressed | ✅ | ✅ | `[mod:alt/ctrl]` | | |
| `moving` | Moving / speed % | ✅ | ✅ | `[moving]` `[moving:>100&<200]` | | MonkeySpeed (speed %) |
| `mybuff` | Player has buff (with time/stacks) | ✅ | ✅ | `[mybuff:Thorns<5]` `[nomybuff:MotW/GotW]` | | |
| `mybuffcapped` | Player buff bar full (32) | ✅ | — | `[nomybuffcapped]` | v2.20 | |
| `mybuffcount` | Player buff slot count (32 max) | ✅ | ✅ | `[mybuffcount:>15]` `[nomybuffcount:>28]` | | |
| `mycc` | Player has CC effect | ✅ | ✅ | `[mycc:stun/silence]` `[nomycc]` | | |
| `mydebuff` | Player has debuff (with time/stacks) | ✅ | ✅ | `[mydebuff:Curse_of_Agony]` | | |
| `mydebuffcapped` | Player debuff bar full (16) | ✅ | — | `[nomydebuffcapped]` | v2.20 | |
| `myhp` | Player HP % | — | ✅ | `[myhp:<30]` `[myhp:>20&<50]` | | |
| `myhplost` | Player HP deficit (max - current) | — | ✅ | `[myhplost:>500]` | | |
| `mylevel` | Player level | — | ✅ | `[mylevel:=60]` | | |
| `mypower` | Player mana/rage/energy % | — | ✅ | `[mypower:>50]` | | |
| `mypowerlost` | Player power deficit | — | ✅ | `[mypowerlost:>200]` | | |
| `myrawhp` | Player raw HP value | — | ✅ | `[myrawhp:>1000]` | | |
| `myrawpower` | Player raw power value | — | ✅ | `[myrawpower:>=40]` | | |
| `nextslamclip` / `nonextslamclip` | Instant now will/won't cause next Slam to clip | ✅ | — | `[nonextslamclip]` | | SP_SwingTimer |
| `notarget` | Player has no target | — | — | `[notarget]` | | |
| `ohimbue` | Off hand has temporary imbue | ✅ | ✅ | `[noohimbue]` `[ohimbue:Crippling_Poison]` | | |
| `onswingpending` | On-swing spell pending | ✅ | — | `[onswingpending]` | v2.12 | |
| `pet` | Has active pet (with family) | ✅ | ✅ | `[pet]` `[pet:Cat/Wolf]` | | |
| `queuedspell` | Spell is queued | ✅ | ✅ | `[queuedspell]` `[queuedspell:Fireball]` | v2.12 | |
| `reactive` | Reactive ability available (on bar) | ✅ | ✅ | `[reactive:Overpower]` | | |
| `resting` | In rest area | ✅ | — | `[resting]` | | |
| `selfcasting` | Player is casting or channeling | ✅ | ✅ | `[selfcasting]` `[noselfcasting:Hearthstone]` | | |
| `slamclip` / `noslamclip` | Slam now will/won't clip auto-attack | ✅ | — | `[noslamclip]` | | SP_SwingTimer |
| `spellcasttime` | Spell cast time from tooltip (real-time) | ✅ | ✅ | `[spellcasttime:>1.5]` `[spellcasttime:Frostbolt<2]` | | |
| `stat` | Player stat value | ✅ | ✅ | `[stat:agi>100]` `[stat:ap>1000]` | | |
| `stealth` | In stealth (Rogue/Druid) | ✅ | — | `[stealth]` | | |
| `stimer` | Alias for swingtimer | ✅ | ✅ | `[stimer:<15]` | | SP_SwingTimer |
| `swimming` | Swimming (Druid only) | ✅ | — | `[swimming]` | | |
| `swingtimer` | Swing timer % elapsed | ✅ | ✅ | `[swingtimer:>80]` | | SP_SwingTimer |
| `usable` | Spell/item is usable now | ✅ | ✅ | `[usable:Overpower]` | | |
| `zone` | Current zone name | ✅ | ✅ | `[zone:Ironforge/Stormwind]` | | |
### Target Conditionals (default @target)
@@ -218,62 +211,53 @@ These default to checking the current target. Most can be redirected with `@unit
| Conditional | Description | No | Multi | Example | NP | Addon |
|-------------|-------------|:--:|:-----:|---------|:--:|-------|
| ***Existence & Disposition*** | | | | | | |
| `exists` | Unit exists | ✅ | — | `[@mouseover,exists]` | | |
| `alive` | Unit is alive | ✅ | — | `[alive]` | | |
| `dead` | Unit is dead | ✅ | — | `[dead]` | | |
| `help` | Unit is friendly | ✅ | — | `[help]` | | |
| `harm` | Unit is hostile | ✅ | — | `[harm]` | | |
| `isplayer` | Unit is a player | ✅ | — | `[isplayer]` | | |
| `isnpc` | Unit is an NPC | ✅ | — | `[isnpc]` | | |
| ***Resources*** | | | | | | |
| `hp` | Target HP % | — | ✅ | `[hp:<20]` `[hp:>30&<70]` | | |
| `rawhp` | Target raw HP value | — | ✅ | `[rawhp:>5000]` | | |
| `hplost` | Target HP deficit (max - current) | — | ✅ | `[hplost:>1000]` | | |
| `power` | Target mana/rage/energy % | — | ✅ | `[power:<30]` | | |
| `rawpower` | Target raw power value | — | ✅ | `[rawpower:>500]` | | |
| `powerlost` | Target power deficit | — | ✅ | `[powerlost:>100]` | | |
| `powertype` | Target power type | ✅ | ✅ | `[powertype:mana/rage]` | | |
| `level` | Target level (skull = 63) | — | ✅ | `[level:>60]` | | |
| ***Classification*** | | | | | | |
| `class` | Target class (players only) | ✅ | ✅ | `[class:Warrior/Priest]` | | |
| `type` | Creature type | ✅ | ✅ | `[type:Undead/Beast]` | | |
| `name` | Exact name match (case-insensitive) | ✅ | ✅ | `[name:Onyxia]` `[noname:Critter/Dummy]` | | |
| ***Target Auras*** | | | | | | |
| `behind` | Behind target / count behind | ✅ | ✅ | `[behind]` `[behind:>=2]` | | UnitXP_SP3 |
| `buff` | Target has buff (with time/stacks) | ✅ | ✅ | `[buff:Shield>#3]` `[nobuff:Renew/Rejuv]` | | |
| `debuff` | Target has debuff (with time/stacks) | ✅ | ✅ | `[debuff:Moonfire<4]` `[nodebuff:Rake]` | | |
| `buffcapped` | Target buff bar full (32) | ✅ | — | `[nobuffcapped]` | v2.20 | |
| `debuffcapped` | Target debuff bar full | ✅ | — | `[nodebuffcapped]` | v2.20 | |
| `cc` | Target has CC effect | ✅ | ✅ | `[cc:stun/fear]` `[nocc:polymorph]` | | |
| `cursive` | GUID-based debuff tracking (time remaining) | ✅ | ✅ | `[cursive:Rake<3]` `[nocursive:Rip]` | | Cursive |
| ***Casting*** | | | | | | |
| `casting` | Target is casting (specific spell) | ✅ | ✅ | `[casting]` `[casting:Heal]` | | |
| `casttime` | Target cast time remaining (seconds) | ✅ | ✅ | `[casttime:<0.5]` | | |
| `cc` | Target has CC effect | ✅ | ✅ | `[cc:stun/fear]` `[nocc:polymorph]` | | |
| `channeltime` | Target channel time remaining (seconds) | ✅ | ✅ | `[channeltime:<0.5]` | | |
| ***Relationship*** | | | | | | |
| `party` | Unit is in your party | ✅ | ✅ | `[party]` `[party:focus]` | | |
| `raid` | Unit is in your raid | ✅ | ✅ | `[raid]` `[raid:mouseover]` | | |
| `member` | Target is in party or raid | ✅ | — | `[member]` | | |
| `targeting` | Unit is targeting you or a tank | ✅ | ✅ | `[targeting:player]` `[notargeting:tank]` | | pfUI (for tank) |
| `istank` | Unit is marked as tank | ✅ | — | `[istank]` `[@focus,istank]` | | pfUI |
| `tag` | Target is tapped by anyone | ✅ | — | `[tag]` `[notag]` | | |
| `mytag` | Target is tapped by you | ✅ | — | `[mytag]` `[nomytag]` | | |
| `othertag` | Target is tapped by someone else | ✅ | — | `[othertag]` `[noothertag]` | | |
| ***Range & Position*** | | | | | | |
| `class` | Target class (players only) | ✅ | ✅ | `[class:Warrior/Priest]` | | |
| `cursive` | GUID-based debuff tracking (time remaining) | ✅ | ✅ | `[cursive:Rake<3]` `[nocursive:Rip]` | | Cursive |
| `dead` | Unit is dead | ✅ | — | `[dead]` | | |
| `debuff` | Target has debuff (with time/stacks) | ✅ | ✅ | `[debuff:Moonfire<4]` `[nodebuff:Rake]` | | |
| `debuffcapped` | Target debuff bar full | ✅ | — | `[nodebuffcapped]` | v2.20 | |
| `distance` | Distance in yards | ✅ | ✅ | `[distance:<40]` | | UnitXP_SP3 |
| `meleerange` | In melee range / count in melee | ✅ | ✅ | `[meleerange]` `[meleerange:>1]` | | UnitXP_SP3 |
| `inrange` | In spell range / count in range | ✅ | ✅ | `[inrange:Charge]` `[inrange:Multi-Shot>1]` | | UnitXP_SP3 (count) |
| `outrange` | Out of spell range / count out | ✅ | ✅ | `[outrange:Charge]` `[outrange:Charge>0]` | | UnitXP_SP3 (count) |
| `insight` | In line of sight / count in LoS | ✅ | ✅ | `[insight]` `[insight:>0]` | | UnitXP_SP3 |
| `behind` | Behind target / count behind | ✅ | ✅ | `[behind]` `[behind:>=2]` | | UnitXP_SP3 |
| `multiscan` | Scan enemies by priority, soft-cast | — | ✅ | `[multiscan:nearest]` `[multiscan:skull]` | | UnitXP_SP3 |
| ***Immunity*** | | | | | | |
| `exists` | Unit exists | ✅ | — | `[@mouseover,exists]` | | |
| `harm` | Unit is hostile | ✅ | — | `[harm]` | | |
| `help` | Unit is friendly | ✅ | — | `[help]` | | |
| `hp` | Target HP % | — | ✅ | `[hp:<20]` `[hp:>30&<70]` | | |
| `hplost` | Target HP deficit (max - current) | — | ✅ | `[hplost:>1000]` | | |
| `immune` | Target is immune (school or CC type) | ✅ | ✅ | `[noimmune:fire]` `[noimmune:stun]` | | |
| `inrange` | In spell range / count in range | ✅ | ✅ | `[inrange:Charge]` `[inrange:Multi-Shot>1]` | | UnitXP_SP3 (count) |
| `insight` | In line of sight / count in LoS | ✅ | ✅ | `[insight]` `[insight:>0]` | | UnitXP_SP3 |
| `isnpc` | Unit is an NPC | ✅ | — | `[isnpc]` | | |
| `isplayer` | Unit is a player | ✅ | — | `[isplayer]` | | |
| `istank` | Unit is marked as tank | ✅ | — | `[istank]` `[@focus,istank]` | | pfUI |
| `level` | Target level (skull = 63) | — | ✅ | `[level:>60]` | | |
| `meleerange` | In melee range / count in melee | ✅ | ✅ | `[meleerange]` `[meleerange:>1]` | | UnitXP_SP3 |
| `member` | Target is in party or raid | ✅ | — | `[member]` | | |
| `multiscan` | Scan enemies by priority, soft-cast | — | ✅ | `[multiscan:nearest]` `[multiscan:skull]` | | UnitXP_SP3 |
| `mytag` | Target is tapped by you | ✅ | — | `[mytag]` `[nomytag]` | | |
| `name` | Exact name match (case-insensitive) | ✅ | ✅ | `[name:Onyxia]` `[noname:Critter/Dummy]` | | |
| `othertag` | Target is tapped by someone else | ✅ | — | `[othertag]` `[noothertag]` | | |
| `outrange` | Out of spell range / count out | ✅ | ✅ | `[outrange:Charge]` `[outrange:Charge>0]` | | UnitXP_SP3 (count) |
| `party` | Unit is in your party | ✅ | ✅ | `[party]` `[party:focus]` | | |
| `power` | Target mana/rage/energy % | — | ✅ | `[power:<30]` | | |
| `powerlost` | Target power deficit | — | ✅ | `[powerlost:>100]` | | |
| `powertype` | Target power type | ✅ | ✅ | `[powertype:mana/rage]` | | |
| `raid` | Unit is in your raid | ✅ | ✅ | `[raid]` `[raid:mouseover]` | | |
| `rawhp` | Target raw HP value | — | ✅ | `[rawhp:>5000]` | | |
| `rawpower` | Target raw power value | — | ✅ | `[rawpower:>500]` | | |
| `resisted` | Last spell was resisted | ✅ | ✅ | `[resisted]` `[resisted:full/partial]` | | |
| ***Addon Integrations*** | | | | | | |
| `tag` | Target is tapped by anyone | ✅ | — | `[tag]` `[notag]` | | |
| `targeting` | Unit is targeting you or a tank | ✅ | ✅ | `[targeting:player]` `[notargeting:tank]` | | pfUI (for tank) |
| `threat` | Threat % on target (100 = pull) | ✅ | ✅ | `[threat:>80]` | | TWThreat |
| `ttk` | Time to kill target (seconds) | ✅ | ✅ | `[ttk:<10]` | | TimeToKill |
| `tte` | Time to execute threshold (seconds) | ✅ | ✅ | `[tte:<5]` | | TimeToKill |
| `ttk` | Time to kill target (seconds) | ✅ | ✅ | `[ttk:<10]` | | TimeToKill |
| `type` | Creature type | ✅ | ✅ | `[type:Undead/Beast]` | | |
### Multi-Unit Count Mode
+2 -1
View File
@@ -2,7 +2,7 @@
## Author: DWG, _brain, Torio, yani9o, Avitasia
## Title: [Super]CleveRoidMacros
## Notes: /cleveroid for settings
## Version: 2.0
## Version: 2.1
## OptionalDeps: ClassicFocus, FocusFrame, pfUI, SuperMacro, Bongos_ActionBar, Cursive, XPerl, LunaUnitFrames, DragonflightReloaded, -Dragonflight3
## SavedVariables: CleveRoidMacros, CleveRoids_LearnedDurations, CleveRoids_AuraTextures, CleveRoids_ImmunityData, CleveRoids_ComboDurations, CleveRoids_SpellSchools
Localization.lua
@@ -43,4 +43,5 @@ Extensions\Mouseover\Cursive.lua
Extensions\Tooltip\Generic.lua
Extensions\CursiveCustomSpells.lua
Extensions\OverflowBuffFrame.lua
+265 -16
View File
@@ -328,7 +328,8 @@ do
local isUpdatingMouseover = false
local function apply(unit)
if CleveRoids.hasSuperwow and _G.SetMouseoverUnit then
if _G.SetMouseoverUnit then
-- Available via SuperWoW or Nampower v2.37+
-- Set flag so UPDATE_MOUSEOVER_UNIT handler knows we triggered this
CleveRoids.__mo.selfTriggered = true
-- Use empty string instead of nil to properly clear mouseover.
@@ -658,7 +659,7 @@ lib.guidToName = lib.guidToName or {}
-- duration calculation, and miss/dodge/parry/resist/immune detection.
-- Enhanced tables (populated by pfUI or standalone Nampower handlers)
lib.ownDebuffs = lib.ownDebuffs or {} -- [targetGUID][spellName] = {startTime, duration, texture, rank, slot}
lib.ownDebuffs = lib.ownDebuffs or {} -- [targetGUID][spellName] = {startTime, duration, texture, rank, spellId}
lib.ownSlots = lib.ownSlots or {} -- [targetGUID][slot] = spellName (LEGACY - empty when pfUI 7.6+ is active)
lib.allSlots = lib.allSlots or {} -- [targetGUID][slot] = {spellName, casterGuid, isOurs} (LEGACY - empty when pfUI 7.6+ is active)
lib.slotOwnership = lib.slotOwnership or {} -- [targetGUID][auraSlot] = {casterGuid, spellName, spellId, isOurs} (pfUI 7.6+ GetUnitField edition)
@@ -667,6 +668,16 @@ lib.pendingCasts = lib.pendingCasts or {} -- [targetGUID][spellName] = {caster
lib.recentMisses = lib.recentMisses or {} -- [targetGUID][spellName] = {time, spellId, targetName, reason} for miss/dodge/parry detection
lib.iconCache = lib.iconCache or {} -- [spellId] = texture (shared with pfUI 7.6 or standalone)
-- Buff tracking tables (parallel to debuff tables, standalone Nampower mode only)
-- Buff tables are NOT linked to pfUI — pfUI has no public buff tracking tables.
-- They remain empty when hasPfUIEnhanced=true since all buff event handlers are gated by it.
lib.ownBuffCasts = lib.ownBuffCasts or {} -- [targetGUID][buffName] = {startTime, duration, spellId, casterGuid}
-- Player's own buff casts on self or others (AURA_CAST events)
lib.allBuffAuras = lib.allBuffAuras or {} -- [targetGUID][buffName][casterGuid] = {startTime, duration, rank}
-- All buffs from all casters on any unit (confirmed by BUFF_ADDED events)
lib.pendingBuffCasts = lib.pendingBuffCasts or {} -- [targetGUID][spellId] = {casterGuid, duration, spellName, time}
-- Temp storage from AURA_CAST_ON_OTHER, consumed by BUFF_ADDED_OTHER
-- Flag indicating whether enhanced pfUI tracking is available
lib.hasPfUIEnhanced = false
lib.hasStandaloneNampower = false
@@ -702,7 +713,7 @@ function lib:HasEnhancedPfUILibdebuff()
end
-- Verify Nampower version based on pfUI version:
-- - pfUI 7.6+ (GetUnitField edition): requires Nampower v2.27.2+
-- - pfUI 7.6+ (GetUnitField edition): requires Nampower v2.37.0+
-- - pfUI 7.4.3 to 7.5.x (legacy): requires Nampower v2.26+
if not GetNampowerVersion then return false end
local npMajor, npMinor, npPatch = GetNampowerVersion()
@@ -712,10 +723,9 @@ function lib:HasEnhancedPfUILibdebuff()
local isPfUI76 = pfUI.libdebuff_slot_ownership ~= nil
if isPfUI76 then
-- pfUI 7.6+ requires Nampower v2.27.2+
-- pfUI 7.6+ requires Nampower v2.37.0+
if npMajor < 2 then return false end
if npMajor == 2 and npMinor < 27 then return false end
if npMajor == 2 and npMinor == 27 and npPatch < 2 then return false end
if npMajor == 2 and npMinor < 37 then return false end
else
-- Legacy pfUI 7.4.3-7.5.x requires Nampower v2.26+
if npMajor < 2 or (npMajor == 2 and npMinor < 26) then return false end
@@ -733,7 +743,7 @@ function lib:HasEnhancedPfUILibdebuff()
end
-- Check if pfUI v7.6+ with enhanced cast tracking is available
-- pfUI 7.6 requires Nampower v2.27.2+ and exposes additional tables
-- pfUI 7.6+ requires Nampower v2.37.0+ and exposes additional tables
function lib:HasPfUI76()
if not pfUI then return false end
@@ -744,13 +754,12 @@ function lib:HasPfUI76()
if v.major < 7 then return false end
if v.major == 7 and (v.minor or 0) < 6 then return false end
-- Verify Nampower v2.27.2+ (pfUI 7.6 hard requirement)
-- Verify Nampower v2.38.0+ (pfUI 7.6+ hard requirement, bumped from 2.37 in pfUI update 2026-02-18)
if not GetNampowerVersion then return false end
local npMajor, npMinor, npPatch = GetNampowerVersion()
npPatch = npPatch or 0
if npMajor < 2 then return false end
if npMajor == 2 and npMinor < 27 then return false end
if npMajor == 2 and npMinor == 27 and npPatch < 2 then return false end
if npMajor == 2 and npMinor < 38 then return false end
-- Verify the new tables exist
if not pfUI.libdebuff_casts then return false end
@@ -1184,6 +1193,58 @@ function lib:CleanupStaleTrackingData()
end
end
end
-- Clean ownBuffCasts (player-cast buffs on targets)
if lib.ownBuffCasts then
for guid, buffs in pairs(lib.ownBuffCasts) do
for buffName, data in pairs(buffs) do
local elapsed = now - (data.startTime or 0)
local dur = data.duration or 0
-- Remove if expired (duration > 0 and past end time) or stale (no duration and old)
if (dur > 0 and elapsed > dur) or (dur <= 0 and elapsed > staleTime) then
buffs[buffName] = nil
end
end
if not next(buffs) then
lib.ownBuffCasts[guid] = nil
end
end
end
-- Clean allBuffAuras (all-caster buff tracking)
if lib.allBuffAuras then
for guid, buffs in pairs(lib.allBuffAuras) do
for buffName, casters in pairs(buffs) do
for cGuid, data in pairs(casters) do
local elapsed = now - (data.startTime or 0)
local dur = data.duration or 0
if (dur > 0 and elapsed > dur) or (dur <= 0 and elapsed > staleTime) then
casters[cGuid] = nil
end
end
if not next(casters) then
buffs[buffName] = nil
end
end
if not next(buffs) then
lib.allBuffAuras[guid] = nil
end
end
end
-- Clean pendingBuffCasts (short-lived correlation data, 2 second TTL)
if lib.pendingBuffCasts then
for guid, spells in pairs(lib.pendingBuffCasts) do
for spellId, data in pairs(spells) do
if (now - (data.time or 0)) > 2 then
spells[spellId] = nil
end
end
if not next(spells) then
lib.pendingBuffCasts[guid] = nil
end
end
end
end
-- Get the caster GUID for a debuff on a target
@@ -2996,10 +3057,16 @@ delayedTrackingFrame:SetScript("OnUpdate", function()
if not lib.detectedJudgementDebuffIDs[debuffSpellID] then
lib.detectedJudgementDebuffIDs[debuffSpellID] = true
lib.judgementSpells[debuffSpellID] = true -- Add to refresh list
-- Also register as shared debuff so [debuff]/[nodebuff] fallback detection works
-- for custom Turtle WoW spell IDs not in the hardcoded sharedDebuffs table
if not lib.sharedDebuffs[debuffSpellID] then
lib.sharedDebuffs[debuffSpellID] = 10 -- Standard judgement duration
CleveRoids.InvalidateSpellNameCache()
end
if debug then
DEFAULT_CHAT_FRAME:AddMessage(
_string_format("|cff00ffff[Judgement Detect]|r Found debuff %s (ID:%d) from cast (ID:%d) - added to refresh list",
_string_format("|cff00ffff[Judgement Detect]|r Found debuff %s (ID:%d) from cast (ID:%d) - added to refresh + shared list",
debuffName, debuffSpellID, pending.castSpellID)
)
end
@@ -3685,6 +3752,13 @@ if CleveRoids.hasNampower then
ev:RegisterEvent("DEBUFF_REMOVED_OTHER")
ev:RegisterEvent("UNIT_DIED") -- Instant cleanup on target death
-- BUFF_ADDED/REMOVED events require v2.30+ for auraSlot and state args
if npMajor > 2 or (npMajor == 2 and npMinor >= 30) then
ev:RegisterEvent("BUFF_ADDED_OTHER")
ev:RegisterEvent("BUFF_REMOVED_SELF")
ev:RegisterEvent("BUFF_REMOVED_OTHER")
end
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[Nampower]|r Registered UNIT_DIED for instant cleanup (v2.26+)")
end
@@ -4043,7 +4117,13 @@ ev:SetScript("OnEvent", function()
})
-- If this is a Judgement spell cast by a Paladin, schedule a scan to find the actual debuff ID
if CleveRoids.playerClass == "PALADIN" and lib.judgementSpells[spellID] then
-- Check by ID (known judgement debuffs) OR by name (Turtle WoW custom cast spell IDs)
local isJudgementCast = lib.judgementSpells[spellID]
if not isJudgementCast and CleveRoids.playerClass == "PALADIN" then
local castName = _SpellInfo(spellID)
isJudgementCast = castName and _string_find(castName, "^Judgement")
end
if CleveRoids.playerClass == "PALADIN" and isJudgementCast then
table.insert(lib.pendingJudgements, {
timestamp = GetTime(),
castSpellID = spellID,
@@ -4370,19 +4450,38 @@ ev:SetScript("OnEvent", function()
-- and these handlers return early (pfUI handles it).
elseif event == "SPELL_START_SELF" or event == "SPELL_START_OTHER" then
-- v2.38+: For player's own channeling spells, pre-store the accurate server channel duration
-- BEFORE the pfUI early-return so SPELLCAST_CHANNEL_START (which fires later) can use it.
-- arg8 == 1 means spellType=Channeling; arg7 is the channel duration in ms.
if event == "SPELL_START_SELF" and arg8 == 1 and arg7 and arg7 > 0 then
CleveRoids._v238ChannelDuration = arg7 / 1000
CleveRoids._v238ChannelSpellId = arg2
end
-- pfUI 7.6 manages castTracking via its own SPELL_START handler
if lib.hasPfUI76 then return end
local spellId = arg2
local casterGuid = arg3
local castTime = arg6 -- milliseconds
local castTime = arg6 -- cast time in milliseconds
-- v2.38+: arg7 = channel duration (ms, channeling spells only), arg8 = spellType (0=Normal,1=Channeling,2=Autorepeating)
local channelDuration = arg7
local spellType = arg8
if not casterGuid or not spellId then return end
local spellName = SpellInfo and SpellInfo(spellId)
local icon = lib:GetCachedIcon(spellId)
local now = GetTime()
local durationSec = castTime and (castTime / 1000) or 0
-- For channeling spells (v2.38+: spellType==1), use the channel duration.
-- For normal cast-time spells, use castTime. Fall back to castTime if params unavailable.
local durationMs
if spellType == 1 and channelDuration and channelDuration > 0 then
durationMs = channelDuration
else
durationMs = castTime or 0
end
local durationSec = durationMs / 1000
CleveRoids.castTracking[casterGuid] = {
spellID = spellId,
@@ -4394,9 +4493,10 @@ ev:SetScript("OnEvent", function()
}
if CleveRoids.debug then
local typeStr = spellType == 1 and "channel" or (spellType == 2 and "auto" or "cast")
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ccff[SPELL_START]|r %s (ID:%d) cast by %s - %.1fs",
spellName or "Unknown", spellId, casterGuid, durationSec)
string.format("|cff00ccff[SPELL_START]|r %s (ID:%d) cast by %s - %.1fs [%s]",
spellName or "Unknown", spellId, casterGuid, durationSec, typeStr)
)
end
@@ -4876,6 +4976,125 @@ ev:SetScript("OnEvent", function()
end
end
-- NAMPOWER v2.30+ BUFF_ADDED_OTHER - Confirm pending AURA_CAST_ON_OTHER data as buff
elseif event == "BUFF_ADDED_OTHER" then
if lib.hasPfUIEnhanced then return end
local guid = CleveRoids.NormalizeGUID(arg1)
local spellId = arg3
local state = arg7 -- 0=added, 1=removed, 2=modified
if not guid or not spellId then return end
-- Consume pending AURA_CAST data for this buff
local pending = lib.pendingBuffCasts[guid] and lib.pendingBuffCasts[guid][spellId]
if pending then
local spellName = pending.spellName or (SpellInfo and SpellInfo(spellId))
if spellName then
local casterGuid = pending.casterGuid
local duration = pending.duration
local now = GetTime()
-- allBuffAuras: confirmed buff from any caster
lib.allBuffAuras[guid] = lib.allBuffAuras[guid] or {}
lib.allBuffAuras[guid][spellName] = lib.allBuffAuras[guid][spellName] or {}
lib.allBuffAuras[guid][spellName][casterGuid or "unknown"] = {
startTime = now,
duration = duration or 0,
rank = 0,
}
-- ownBuffCasts: only if player is the caster
local _, playerGuidRaw = UnitExists("player")
local playerGuid = playerGuidRaw and CleveRoids.NormalizeGUID(playerGuidRaw)
if playerGuid and casterGuid == playerGuid then
lib.ownBuffCasts[guid] = lib.ownBuffCasts[guid] or {}
lib.ownBuffCasts[guid][spellName] = {
startTime = now,
duration = duration or 0,
spellId = spellId,
casterGuid = casterGuid,
}
end
end
lib.pendingBuffCasts[guid][spellId] = nil
end
-- NAMPOWER v2.30+ BUFF_REMOVED_SELF - Player's own buffs removed
elseif event == "BUFF_REMOVED_SELF" then
if lib.hasPfUIEnhanced then return end
local spellId = arg3
local state = arg7
if not spellId then return end
if state == 2 then return end -- Stack decrease only, not full removal
local spellName = SpellInfo and SpellInfo(spellId)
local _, playerGuidRaw = UnitExists("player")
local playerGuid = playerGuidRaw and CleveRoids.NormalizeGUID(playerGuidRaw)
if spellName and playerGuid then
if lib.ownBuffCasts[playerGuid] then
lib.ownBuffCasts[playerGuid][spellName] = nil
if not next(lib.ownBuffCasts[playerGuid]) then
lib.ownBuffCasts[playerGuid] = nil
end
end
if lib.allBuffAuras[playerGuid] then
lib.allBuffAuras[playerGuid][spellName] = nil
if not next(lib.allBuffAuras[playerGuid]) then
lib.allBuffAuras[playerGuid] = nil
end
end
end
-- Also prune from OverflowBuffs (existing system)
if spellId and CleveRoids.OverflowBuffs and CleveRoids.OverflowBuffs[spellId] then
CleveRoids.OverflowBuffs[spellId] = nil
end
-- NAMPOWER v2.30+ BUFF_REMOVED_OTHER - Buffs removed from other units
elseif event == "BUFF_REMOVED_OTHER" then
if lib.hasPfUIEnhanced then return end
local guid = CleveRoids.NormalizeGUID(arg1)
local spellId = arg3
local state = arg7
if not guid or not spellId then return end
if state == 2 then return end -- Stack decrease only, not full removal
local spellName = SpellInfo and SpellInfo(spellId)
if spellName then
if lib.allBuffAuras[guid] then
lib.allBuffAuras[guid][spellName] = nil
if not next(lib.allBuffAuras[guid]) then
lib.allBuffAuras[guid] = nil
end
end
if lib.ownBuffCasts[guid] then
lib.ownBuffCasts[guid][spellName] = nil
if not next(lib.ownBuffCasts[guid]) then
lib.ownBuffCasts[guid] = nil
end
end
end
-- Clean pending correlation data
if lib.pendingBuffCasts[guid] then
lib.pendingBuffCasts[guid][spellId] = nil
if not next(lib.pendingBuffCasts[guid]) then
lib.pendingBuffCasts[guid] = nil
end
end
-- Also clean AllCasterAuraTracking (existing system)
if CleveRoids.AllCasterAuraTracking[guid] then
CleveRoids.AllCasterAuraTracking[guid][spellId] = nil
if not next(CleveRoids.AllCasterAuraTracking[guid]) then
CleveRoids.AllCasterAuraTracking[guid] = nil
end
end
-- NAMPOWER v2.26+ UNIT_DIED - Instant cleanup on target death
elseif event == "UNIT_DIED" then
local guid = arg1
@@ -4908,11 +5127,41 @@ ev:SetScript("OnEvent", function()
lib.recentMisses[guid] = nil
end
-- Clean up buff tracking tables
if lib.ownBuffCasts[guid] then
lib.ownBuffCasts[guid] = nil
end
if lib.allBuffAuras[guid] then
lib.allBuffAuras[guid] = nil
end
if lib.pendingBuffCasts[guid] then
lib.pendingBuffCasts[guid] = nil
end
-- Clean up cast tracking for this unit (they can't be casting if dead)
if not lib.hasPfUI76 and CleveRoids.castTracking[guid] then
CleveRoids.castTracking[guid] = nil
end
-- Clean up overflow buff tracking (death removes all buffs)
local _, playerGUID = UnitExists("player")
if playerGUID and guid == playerGUID then
-- Player died: clear all overflow buff entries and reset cap status
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
-- Clean up all-caster aura tracking for the dead unit
if CleveRoids.AllCasterAuraTracking and CleveRoids.AllCasterAuraTracking[guid] then
CleveRoids.AllCasterAuraTracking[guid] = nil
end
-- Clean up GUID to name mapping (after 5 seconds to allow final lookups)
-- Actually, keep it for a bit in case we need it for immunity detection
-- lib.guidToName[guid] = nil