fix debuff and buff tracking

This commit is contained in:
Jrc13245
2026-03-06 18:55:39 -05:00
parent 2f698403e5
commit 34190ccbd8
4 changed files with 242 additions and 129 deletions
+186 -99
View File
@@ -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
+15 -11
View File
@@ -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
+34 -14
View File
@@ -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
+7 -5
View File
@@ -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