diff --git a/Conditionals.lua b/Conditionals.lua index ddf573e..d067623 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -886,69 +886,123 @@ 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 } +-- Structure: [targetGuid][spellName][casterGuid] = { start, duration, spellId } +-- Aligned with pfUI's allAuraCasts multi-caster architecture CleveRoids.AllCasterAuraTracking = {} --- Helper to get time remaining from all-caster tracking +-- Helper: check if a spell is personal using lib:IsPersonalDebuff + name fallback +local function IsPersonalAura(spellId, spellName) + local lib = CleveRoids.libdebuff + if not lib then return true end -- Default personal (safer) + -- Direct ID check + if spellId and lib.IsPersonalDebuff then + return lib:IsPersonalDebuff(spellId) + end + -- Name-based fallback via IsSharedDebuffByIdOrName + if spellName and spellId then + return not IsSharedDebuffByIdOrName(lib, spellId, spellName) + end + return true -- Default personal +end + +-- Helper to get time remaining from all-caster tracking (by spellId) +-- Returns player's entry for personal debuffs, any caster for shared auras. function CleveRoids.GetAllCasterAuraTimeRemaining(targetGuid, spellId) if not targetGuid or not spellId then return nil end local targetData = CleveRoids.AllCasterAuraTracking[targetGuid] if not targetData then return nil end - local auraData = targetData[spellId] - if not auraData or not auraData.start or not auraData.duration then return nil end - local remaining = auraData.duration + auraData.start - GetTime() - if remaining <= 0 then - -- Expired, clean up - targetData[spellId] = nil - return nil + local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") + if not spellName then return nil end + + local casters = targetData[spellName] + if not casters then return nil end + + local now = GetTime() + local playerGuid = CleveRoids.GetGUID("player") + + -- Always check player's own entry first + if playerGuid and casters[playerGuid] then + local auraData = casters[playerGuid] + if auraData.start and auraData.duration then + local remaining = auraData.duration + auraData.start - now + if remaining > 0 then return remaining end + end end - return remaining + + -- Personal debuff and player has no active entry → don't use other players' data + if IsPersonalAura(spellId, spellName) then return nil end + + -- Shared aura: return any active caster's entry + for _, auraData in pairs(casters) do + if auraData.start and auraData.duration then + local remaining = auraData.duration + auraData.start - now + if remaining > 0 then return remaining end + end + end + return nil end --- Helper to find aura by name across all spell IDs for a target +-- Helper to find aura by name (or spell ID string) for a target +-- Returns player's entry for personal debuffs, any caster for shared auras. function CleveRoids.FindAllCasterAuraByName(targetGuid, searchName) if not targetGuid or not searchName then return nil, nil end local targetData = CleveRoids.AllCasterAuraTracking[targetGuid] if not targetData then return nil, nil end - local now = GetTime() - - -- Direct spell ID lookup: O(1) hash table access instead of iterating all entries + -- Resolve spell ID to name for direct lookup local searchID = tonumber(searchName) if searchID then - local auraData = targetData[searchID] - if auraData and auraData.start and auraData.duration then - local remaining = auraData.duration + auraData.start - now - if remaining > 0 then - return remaining, auraData.casterGuid - else - targetData[searchID] = nil - end - end - return nil, nil + local resolvedName = GetSpellRecField and GetSpellRecField(searchID, "name") + if not resolvedName then return nil, nil end + searchName = resolvedName end - -- Name-based search (existing behavior) - local searchLower = string.lower(searchName) + local now = GetTime() - for spellId, auraData in pairs(targetData) do + -- Try exact match first (O(1) hash lookup) + local casters = targetData[searchName] + + -- Case-insensitive fallback + if not casters then + local searchLower = string.lower(searchName) + for spellName, c in pairs(targetData) do + local baseName = string.gsub(spellName, "%s*%(%s*Rank%s+%d+%s*%)", "") + if string.lower(baseName) == searchLower then + casters = c + break + end + end + end + + if not casters then return nil, nil end + + local playerGuid = CleveRoids.GetGUID("player") + + -- Always check player's own entry first + if playerGuid and casters[playerGuid] then + local auraData = casters[playerGuid] if auraData.start and auraData.duration then local remaining = auraData.duration + auraData.start - now - if remaining > 0 then - -- Get spell name and compare - local spellName = GetSpellRecField(spellId, "name") - if spellName then - -- Strip rank and compare lowercase - local baseName = string.gsub(spellName, "%s*%(%s*Rank%s+%d+%s*%)", "") - if string.lower(baseName) == searchLower then - return remaining, auraData.casterGuid - end - end - else - -- Expired, clean up - targetData[spellId] = nil - end + if remaining > 0 then return remaining, playerGuid end + end + end + + -- Get a spellId from any entry to check personal vs shared + local anySpellId = nil + for _, aData in pairs(casters) do + anySpellId = aData.spellId + break + end + + -- Personal debuff and player has no active entry → don't use other players' data + if IsPersonalAura(anySpellId, searchName) then return nil, nil end + + -- Shared aura: return any active caster's entry + for cGuid, auraData in pairs(casters) do + if auraData.start and auraData.duration then + local remaining = auraData.duration + auraData.start - now + if remaining > 0 then return remaining, cGuid end end end return nil, nil @@ -1186,29 +1240,37 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu -- Store aura duration for all-caster tracking (even without auraCapStatus) if spellId and durationMs and durationMs > 0 then - if not CleveRoids.AllCasterAuraTracking[targetGuid] then - CleveRoids.AllCasterAuraTracking[targetGuid] = {} - end - -- PERFORMANCE: Reuse existing entry table when possible - local existing = CleveRoids.AllCasterAuraTracking[targetGuid][spellId] - if existing then - existing.start = now - existing.duration = durationMs / 1000 - existing.casterGuid = casterGuid - else - CleveRoids.AllCasterAuraTracking[targetGuid][spellId] = { - start = now, - duration = durationMs / 1000, -- Convert ms to seconds - casterGuid = casterGuid, - } + local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") + if spellName then + if not CleveRoids.AllCasterAuraTracking[targetGuid] then + CleveRoids.AllCasterAuraTracking[targetGuid] = {} + end + if not CleveRoids.AllCasterAuraTracking[targetGuid][spellName] then + CleveRoids.AllCasterAuraTracking[targetGuid][spellName] = {} + end + local casterKey = casterGuid or "unknown" + -- PERFORMANCE: Reuse existing entry table when possible + local existing = CleveRoids.AllCasterAuraTracking[targetGuid][spellName][casterKey] + if existing then + existing.start = now + existing.duration = durationMs / 1000 + existing.spellId = spellId + else + CleveRoids.AllCasterAuraTracking[targetGuid][spellName][casterKey] = { + start = now, + duration = durationMs / 1000, + spellId = spellId, + } + end end -- Debug output when enabled if CleveRoids.debug then - local spellName = GetSpellRecField(spellId, "name") or "Unknown" + local debugName = spellName or GetSpellRecField(spellId, "name") or "Unknown" DEFAULT_CHAT_FRAME:AddMessage(string.format( - "|cff00ffff[AuraTrack]|r %s (ID:%d) on %s, dur=%.1fs", - spellName, spellId, string.sub(tostring(targetGuid), 1, 16), durationMs / 1000 + "|cff00ffff[AuraTrack]|r %s (ID:%d) on %s by %s, dur=%.1fs", + debugName, spellId, string.sub(tostring(targetGuid), 1, 16), + string.sub(tostring(casterGuid), 1, 16), durationMs / 1000 )) end @@ -1261,30 +1323,41 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu data = nextGuid and CleveRoids.AuraCapStatus.targetCapStatus[nextGuid] end - -- Cleanup old aura tracking entries - local tGuid, spells = next(CleveRoids.AllCasterAuraTracking) + -- Cleanup old aura tracking entries (3-level: targetGuid → spellName → casterGuid) + local tGuid, spellNames = next(CleveRoids.AllCasterAuraTracking) while tGuid do local nextTGuid = next(CleveRoids.AllCasterAuraTracking, tGuid) - local hasActive = false - local sid, auraData = next(spells) - while sid do - local nextSid = next(spells, sid) - if auraData.start and auraData.duration then - local remaining = auraData.duration + auraData.start - now - if remaining <= 0 then - spells[sid] = nil - else - hasActive = true + local hasActiveTarget = false + local sName, casters = next(spellNames) + while sName do + local nextSName = next(spellNames, sName) + local hasActiveCaster = false + local cGuid, auraData = next(casters) + while cGuid do + local nextCGuid = next(casters, cGuid) + if auraData.start and auraData.duration then + if auraData.duration + auraData.start - now <= 0 then + casters[cGuid] = nil + else + hasActiveCaster = true + end end + cGuid = nextCGuid + auraData = nextCGuid and casters[nextCGuid] end - sid = nextSid - auraData = nextSid and spells[nextSid] + if hasActiveCaster then + hasActiveTarget = true + else + spellNames[sName] = nil + end + sName = nextSName + casters = nextSName and spellNames[nextSName] end - if not hasActive then + if not hasActiveTarget then CleveRoids.AllCasterAuraTracking[tGuid] = nil end tGuid = nextTGuid - spells = nextTGuid and CleveRoids.AllCasterAuraTracking[nextTGuid] + spellNames = nextTGuid and CleveRoids.AllCasterAuraTracking[nextTGuid] end end @@ -1338,25 +1411,28 @@ autoAttackFrame:SetScript("OnEvent", function() if spellId and spellId > 0 and durationMs and durationMs > 0 then local playerGUID = CleveRoids.GetGUID("player") - if playerGUID then + local durSpellName = GetSpellRecField and GetSpellRecField(spellId, "name") + if playerGUID and durSpellName then if not CleveRoids.AllCasterAuraTracking[playerGUID] then CleveRoids.AllCasterAuraTracking[playerGUID] = {} end + if not CleveRoids.AllCasterAuraTracking[playerGUID][durSpellName] then + CleveRoids.AllCasterAuraTracking[playerGUID][durSpellName] = {} + end local durationSec = durationMs / 1000 local now = GetTime() local expirationSec = expirationTimeMs and expirationTimeMs / 1000 local startTime = expirationSec and (expirationSec - durationSec) or now - CleveRoids.AllCasterAuraTracking[playerGUID][spellId] = { + CleveRoids.AllCasterAuraTracking[playerGUID][durSpellName][playerGUID] = { start = startTime, duration = durationSec, - casterGuid = playerGUID, + spellId = spellId, } if CleveRoids.debug then - local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") or "Unknown" DEFAULT_CHAT_FRAME:AddMessage(string.format( "|cff88ff88[AuraDurUpdate]|r %s (slot:%d, ID:%d) dur=%.1fs", - spellName, auraSlot, spellId, durationSec + durSpellName, auraSlot, spellId, durationSec )) end end @@ -1386,9 +1462,12 @@ autoAttackFrame:SetScript("OnEvent", function() local state = arg7 if state == 2 then return end -- Stack change, not full removal if guid and spellId and CleveRoids.AllCasterAuraTracking[guid] then - CleveRoids.AllCasterAuraTracking[guid][spellId] = nil - if not next(CleveRoids.AllCasterAuraTracking[guid]) then - CleveRoids.AllCasterAuraTracking[guid] = nil + local removedName = GetSpellRecField and GetSpellRecField(spellId, "name") + if removedName and CleveRoids.AllCasterAuraTracking[guid][removedName] then + CleveRoids.AllCasterAuraTracking[guid][removedName] = nil + if not next(CleveRoids.AllCasterAuraTracking[guid]) then + CleveRoids.AllCasterAuraTracking[guid] = nil + end end end end @@ -3456,11 +3535,22 @@ function CleveRoids.ValidateAura(unit, args, isbuff) if gufSlot <= 32 then found = true if isPlayer then - -- GetUnitField auraApplications may not work for player token - -- without SuperWoW; use standard API for reliable stacks - stacks = GetPlayerBuffApplications(gufSlot - 1) or gufStacks or 0 - -- GetPlayerAuraDuration (Nampower) is primary: GetPlayerBuffTimeLeft - -- returns 0 for auras found via GetUnitField fast path + -- Match pfUI pattern: always go through GetPlayerBuff() + -- to get the bid before calling standard buff APIs. + -- GetPlayerBuff validates the slot and returns the bid + -- that GetPlayerBuffApplications/GetPlayerBuffTimeLeft expect. + local bid = GetPlayerBuff(gufSlot - 1, "HELPFUL") + if bid >= 0 then + stacks = GetPlayerBuffApplications(bid) + remaining = GetPlayerBuffTimeLeft(bid) + end + -- GetUnitField stacks as fallback when bid invalid or + -- GetPlayerBuffApplications returned 0 + if not stacks or stacks == 0 then + stacks = gufStacks or 0 + end + -- GetPlayerAuraDuration (Nampower) overrides standard + -- timeleft when available (more accurate for fast path) if _G.GetPlayerAuraDuration then local _, durationMs, expirationTimeMs = _G.GetPlayerAuraDuration(gufSlot - 1) if expirationTimeMs and expirationTimeMs > 0 then @@ -3470,10 +3560,6 @@ function CleveRoids.ValidateAura(unit, args, isbuff) remaining = 0 -- No duration (permanent aura) end end - -- Fallback to standard API - if remaining == nil then - remaining = GetPlayerBuffTimeLeft(gufSlot - 1) - end else stacks = gufStacks or 0 end @@ -3482,9 +3568,14 @@ function CleveRoids.ValidateAura(unit, args, isbuff) -- For debuff checks: accept any slot (debuffs overflow to buff slots) found = true if isPlayer then - stacks = GetPlayerBuffApplications(gufSlot - 1) or gufStacks or 0 - -- GetPlayerAuraDuration (Nampower) is primary: GetPlayerBuffTimeLeft - -- returns 0 for auras found via GetUnitField fast path + local bid = GetPlayerBuff(gufSlot - 1, "HARMFUL") + if bid >= 0 then + stacks = GetPlayerBuffApplications(bid) + remaining = GetPlayerBuffTimeLeft(bid) + end + if not stacks or stacks == 0 then + stacks = gufStacks or 0 + end if _G.GetPlayerAuraDuration then local _, durationMs, expirationTimeMs = _G.GetPlayerAuraDuration(gufSlot - 1) if expirationTimeMs and expirationTimeMs > 0 then @@ -3494,10 +3585,6 @@ function CleveRoids.ValidateAura(unit, args, isbuff) remaining = 0 -- No duration (permanent aura) end end - -- Fallback to standard API - if remaining == nil then - remaining = GetPlayerBuffTimeLeft(gufSlot - 1) - end else stacks = gufStacks or 0 end diff --git a/Core.lua b/Core.lua index 9e6644d..686f2e7 100644 --- a/Core.lua +++ b/Core.lua @@ -6909,7 +6909,7 @@ SlashCmdList["CLEVEROID"] = function(msg) CleveRoids.Print("|cffffaa00Tracked Auras:|r") local trackingCount = 0 local now = GetTime() - for targetGuid, spells in pairs(CleveRoids.AllCasterAuraTracking or {}) do + for targetGuid, spellNames in pairs(CleveRoids.AllCasterAuraTracking or {}) do local unitName = nil -- Try to find unit name for this GUID (use pcall to handle invalid units like "focus") for _, testUnit in ipairs({"target", "mouseover", "party1", "party2", "party3", "party4"}) do @@ -6923,14 +6923,16 @@ SlashCmdList["CLEVEROID"] = function(msg) end end - for spellId, auraData in pairs(spells) do - if auraData.start and auraData.duration then - local remaining = auraData.duration + auraData.start - now - if remaining > 0 then - local spellName = GetSpellRecField(spellId, "name") or ("ID:" .. spellId) - local display = unitName or (string.sub(targetGuid, 1, 16) .. "...") - CleveRoids.Print(string.format(" %s on %s: %.1fs left", spellName, display, remaining)) - trackingCount = trackingCount + 1 + for spellName, casters in pairs(spellNames) do + for casterGuid, auraData in pairs(casters) do + if auraData.start and auraData.duration then + local remaining = auraData.duration + auraData.start - now + if remaining > 0 then + local display = unitName or (string.sub(targetGuid, 1, 16) .. "...") + CleveRoids.Print(string.format(" %s on %s: %.1fs left (caster: %s)", + spellName, display, remaining, string.sub(tostring(casterGuid), 1, 16))) + trackingCount = trackingCount + 1 + end end end end @@ -6948,8 +6950,10 @@ SlashCmdList["CLEVEROID"] = function(msg) local targetData = CleveRoids.AllCasterAuraTracking[targetGuid] if targetData then local count = 0 - for _ in pairs(targetData) do count = count + 1 end - CleveRoids.Print(" Tracked spells on target: " .. count) + for _, casters in pairs(targetData) do + for _ in pairs(casters) do count = count + 1 end + end + CleveRoids.Print(" Tracked aura entries on target: " .. count) else CleveRoids.Print(" |cffff9900No tracking data for this target|r") end diff --git a/Extensions/OverflowBuffFrame.lua b/Extensions/OverflowBuffFrame.lua index b9b66ce..b054b04 100644 --- a/Extensions/OverflowBuffFrame.lua +++ b/Extensions/OverflowBuffFrame.lua @@ -292,18 +292,29 @@ local function GetTargetOverflowBuffs() local now = GetTime() - for spellId, auraData in pairs(trackingData[targetGuid]) do - -- v3.0+: Skip hidden auras (not real overflow) - if _G.IsAuraHidden and _G.IsAuraHidden(spellId) == 1 then - -- Hidden aura, skip - elseif auraData.start and auraData.duration then - local remaining = auraData.duration + auraData.start - now - if remaining > 0 then - local isVisible = auraData._testEntry == nil and visibleSpellIds[spellId] + for spellName, casters in pairs(trackingData[targetGuid]) do + -- Find best entry across all casters for this spell + local bestEntry = nil + local bestRemaining = 0 + for casterGuid, auraData in pairs(casters) do + if auraData.spellId and auraData.start and auraData.duration then + local remaining = auraData.duration + auraData.start - now + if remaining > bestRemaining then + bestRemaining = remaining + bestEntry = auraData + end + end + end + + if bestEntry and bestRemaining > 0 then + local spellId = bestEntry.spellId + -- v3.0+: Skip hidden auras (not real overflow) + if not (_G.IsAuraHidden and _G.IsAuraHidden(spellId) == 1) then + local isVisible = bestEntry._testEntry == nil and visibleSpellIds[spellId] if not isVisible then table.insert(results, { spellId = spellId, - remaining = remaining, + remaining = bestRemaining, source = "target", }) end @@ -507,9 +518,14 @@ 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 + for spellName, casters in pairs(targetData) do + for casterGuid, entry in pairs(casters) do + if entry._testEntry then + casters[casterGuid] = nil + end + end + if not next(casters) then + targetData[spellName] = nil end end if not next(targetData) then @@ -538,10 +554,14 @@ local function InjectTestTargetData() 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] = { + local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") or ("TestSpell" .. spellId) + if not CleveRoids.AllCasterAuraTracking[targetGuid][spellName] then + CleveRoids.AllCasterAuraTracking[targetGuid][spellName] = {} + end + CleveRoids.AllCasterAuraTracking[targetGuid][spellName]["test"] = { start = testStartTime, duration = dur, - casterGuid = "test", + spellId = spellId, _testEntry = true, } end diff --git a/Utility.lua b/Utility.lua index aac782a..863490f 100644 --- a/Utility.lua +++ b/Utility.lua @@ -5548,11 +5548,13 @@ ev:SetScript("OnEvent", function() 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 + -- Also clean AllCasterAuraTracking (keyed by spellName) + if spellName and CleveRoids.AllCasterAuraTracking[guid] then + if CleveRoids.AllCasterAuraTracking[guid][spellName] then + CleveRoids.AllCasterAuraTracking[guid][spellName] = nil + if not next(CleveRoids.AllCasterAuraTracking[guid]) then + CleveRoids.AllCasterAuraTracking[guid] = nil + end end end