diff --git a/Conditionals.lua b/Conditionals.lua index e1ca333..f053a89 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -877,9 +877,9 @@ CleveRoids.AuraCapStatus = { } -- Overflow buff tracking: buffs applied while buff-capped (v2.34+) --- The server maintains up to 48 buff slots (32 visible + 16 invisible overflow). --- Buffs in slots 33-48 are mechanically active but have no client aura slot, --- so GetPlayerBuff/GetPlayerAuraDuration can't see them. +-- 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 = {} @@ -1036,12 +1036,10 @@ local function OnAuraCastSelf(spellId, casterGuid, targetGuid, effect, effectAur -- Check debuff bar full (bit 2) CleveRoids.AuraCapStatus.playerDebuffCapped = HasHitFlag(auraCapStatus, AURA_CAP_DEBUFF_FULL) - -- Track overflow buffs: if buff bar is full, this aura may land in server slots 33-48 - -- (no client aura slot). Store spellId + duration so /cancelaura and [mybuff] can find it. - -- But first verify it's actually a buff — debuffs applied while buff-capped go into - -- debuff slots (32-47), not buff overflow. Player debuffs never overflow into buff slots. - if buffCapped and spellId and spellId > 0 and effectAuraName and effectAuraName > 0 then - -- Check debuff slots (32-47) — if the spellId is there, it's a debuff, not overflow + -- 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 @@ -1052,8 +1050,11 @@ local function OnAuraCastSelf(spellId, casterGuid, targetGuid, effect, effectAur end end end + isBuffNotDebuff = not isDebuff - if not isDebuff then + -- 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 = {} @@ -1062,7 +1063,9 @@ local function OnAuraCastSelf(spellId, casterGuid, targetGuid, effect, effectAur entry.timestamp = now entry.durationSec = durationMs and (durationMs / 1000) or 0 end - elseif not buffCapped and next(CleveRoids.OverflowBuffs) then + 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, @@ -1091,6 +1094,33 @@ local function OnAuraCastSelf(spellId, casterGuid, targetGuid, effect, effectAur 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, @@ -1126,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) @@ -3464,6 +3513,36 @@ function CleveRoids.ValidateAura(unit, args, isbuff) 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. @@ -3517,6 +3596,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 diff --git a/Core.lua b/Core.lua index d023924..ba19a77 100644 --- a/Core.lua +++ b/Core.lua @@ -2370,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 @@ -2402,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() @@ -2501,7 +2501,7 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo -- 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 diff --git a/Extensions/OverflowBuffFrame.lua b/Extensions/OverflowBuffFrame.lua index cd0418b..e921c92 100644 --- a/Extensions/OverflowBuffFrame.lua +++ b/Extensions/OverflowBuffFrame.lua @@ -1,7 +1,9 @@ --[[ OverflowBuffFrame Extension - Two frames displaying overflow buffs (slots 33-48) for the player - and the current target (if a group/party member). + 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 @@ -98,7 +100,7 @@ local function CreateIconButton(parent, index, iconTable) if data.source ~= "player" then return end -- CancelPlayerAuraSpellId(spellId, ignoreMissing) - -- ignoreMissing=1 is required for overflow buffs in invisible slots 33-48 + -- ignoreMissing=1 is required for overflow buffs that have no client aura slot if CancelPlayerAuraSpellId then CancelPlayerAuraSpellId(data.spellId, 1) end diff --git a/Init.lua b/Init.lua index e6feef2..bf62857 100644 --- a/Init.lua +++ b/Init.lua @@ -324,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 diff --git a/NampowerAPI.lua b/NampowerAPI.lua index d993693..9b178fc 100644 --- a/NampowerAPI.lua +++ b/NampowerAPI.lua @@ -88,8 +88,8 @@ 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 overflow buffs in - server slots 33-48 that have no client aura slot) + 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, @@ -102,7 +102,14 @@ - PlayerIsRooted() - Returns 1 if player is rooted, nil if not - PlayerIsSwimming() - Returns 1 if player is swimming, nil if not - Current version: v2.36.0 + 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. + + Current version: v2.37.0 ]] local _G = _G or getfenv(0) @@ -272,6 +279,10 @@ API.VERSION_REQUIREMENTS = { ["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" }, } -- Check if a specific feature is available @@ -425,6 +436,10 @@ local function InitializeFeatures() 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") + -- Runtime detection for enhanced spell functions (verify by testing) if f.hasEnhancedSpellFunctions and GetSpellTexture then local success, result = pcall(function() diff --git a/Utility.lua b/Utility.lua index 2f4c612..0e7001c 100644 --- a/Utility.lua +++ b/Utility.lua @@ -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.31.0+ + -- - 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,9 +723,9 @@ function lib:HasEnhancedPfUILibdebuff() local isPfUI76 = pfUI.libdebuff_slot_ownership ~= nil if isPfUI76 then - -- pfUI 7.6+ requires Nampower v2.31.0+ + -- pfUI 7.6+ requires Nampower v2.37.0+ if npMajor < 2 then return false end - if npMajor == 2 and npMinor < 31 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 @@ -732,7 +743,7 @@ function lib:HasEnhancedPfUILibdebuff() end -- Check if pfUI v7.6+ with enhanced cast tracking is available --- pfUI 7.6 requires Nampower v2.31.0+ 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 @@ -743,12 +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.31.0+ (pfUI 7.6 hard requirement) + -- Verify Nampower v2.37.0+ (pfUI 7.6+ hard requirement) 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 < 31 then return false end + if npMajor == 2 and npMinor < 37 then return false end -- Verify the new tables exist if not pfUI.libdebuff_casts then return false end @@ -1182,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 @@ -3689,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 @@ -4886,6 +4956,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 @@ -4918,6 +5107,17 @@ 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