Files
pfUI/libs/libdebuff.lua
T
2026-06-22 15:11:33 -05:00

2098 lines
77 KiB
Lua

-- load pfUI environment
setfenv(1, pfUI:GetEnvironment())
--[[ libdebuff - GetUnitField Edition ]]--
-- A pfUI library that detects and saves all ongoing debuffs of players, NPCs and enemies.
--
-- MAJOR REWRITE: Now uses GetUnitField for slot mapping instead of manual shifting.
-- Key insight: GetUnitField returns STABLE aura slots (33-48) that DON'T shift when
-- debuffs expire. Only the display slots (UnitDebuff returns 1,2,3...) are compacted.
--
-- This eliminates ~400 lines of error-prone shift logic while maintaining full
-- multi-caster tracking support.
--
-- libdebuff:UnitDebuff(unit, id)
-- Returns debuff informations on the given effect of the specified unit.
-- name, rank, texture, stacks, dtype, duration, timeleft, caster
-- return instantly when another libdebuff is already active
if pfUI.api.libdebuff then return end
-- fix a typo (missing $) in ruRU capture index
if GetLocale() == "ruRU" then
SPELLREFLECTSELFOTHER = gsub(SPELLREFLECTSELFOTHER, "%%2s", "%%2%$s")
end
local libdebuff = CreateFrame("Frame", "pfdebuffsScanner", UIParent)
local _, class = UnitClass("player")
local lastspell
-- Nampower Support
local hasNampower = false
-- Set hasNampower immediately for functionality
if GetNampowerVersion then
local major, minor, patch = GetNampowerVersion()
patch = patch or 0
-- Minimum required version: 3.0.0 (UnitGUID support)
if major > 3 or (major == 3 and minor > 0) or (major == 3 and minor == 0 and patch >= 0) then
hasNampower = true
end
end
-- Nampower startup check: show version info and ensure CVars are set.
-- Runs on first OnUpdate after PLAYER_ENTERING_WORLD to give Nampower time to initialize.
local nampowerCheckFrame = CreateFrame("Frame")
nampowerCheckFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
nampowerCheckFrame:SetScript("OnEvent", function()
-- Defer to next frame so Nampower is fully initialized
this:SetScript("OnUpdate", function()
this:SetScript("OnUpdate", nil)
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
if GetNampowerVersion then
local major, minor, patch = GetNampowerVersion()
patch = patch or 0
local versionString = major .. "." .. minor .. "." .. patch
if major > 3 or (major == 3 and minor > 0) or (major == 3 and minor == 0 and patch >= 0) then
DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99[libdebuff]|r Nampower v" .. versionString .. " detected - GetUnitField mode enabled!")
if SetCVar and GetCVar then
local cvarsToEnable = {
"NP_EnableSpellStartEvents",
"NP_EnableSpellGoEvents",
"NP_EnableAuraCastEvents",
"NP_EnableAutoAttackEvents",
"NP_EnableSpellHealEvents",
}
local enabledCount = 0
local alreadyEnabledCount = 0
local failedCount = 0
for _, cvar in ipairs(cvarsToEnable) do
local success, currentValue = pcall(GetCVar, cvar)
if success and currentValue then
if currentValue == "1" then
alreadyEnabledCount = alreadyEnabledCount + 1
else
local setSuccess = pcall(SetCVar, cvar, "1")
if setSuccess then enabledCount = enabledCount + 1
else failedCount = failedCount + 1 end
end
else
failedCount = failedCount + 1
end
end
if enabledCount > 0 then
DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99[libdebuff]|r Enabled " .. enabledCount .. " Nampower CVars")
elseif alreadyEnabledCount == table.getn(cvarsToEnable) then
DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99[libdebuff]|r All required Nampower CVars already enabled")
end
if failedCount > 0 then
DEFAULT_CHAT_FRAME:AddMessage("|cffffcc00[libdebuff]|r Warning: Could not check/set " .. failedCount .. " CVars")
end
end
else
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[libdebuff] Debuff tracking disabled! Please update Nampower to v3.0.0 or higher.|r")
StaticPopup_Show("LIBDEBUFF_NAMPOWER_UPDATE", versionString)
end
else
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[libdebuff] Nampower not found! Debuff tracking disabled.|r")
StaticPopup_Show("LIBDEBUFF_NAMPOWER_MISSING")
end
end)
end)
-- ============================================================================
-- DATA STRUCTURES (Simplified - no more manual slot tracking!)
-- ============================================================================
-- ownDebuffs: [targetGUID][spellName] = {startTime, duration, texture, rank}
-- Timer data for OUR debuffs only
pfUI.libdebuff_own = pfUI.libdebuff_own or {}
local ownDebuffs = pfUI.libdebuff_own
-- allAuraCasts: [targetGUID][spellName][casterGuid] = {startTime, duration, rank}
-- Timer data for ALL debuffs (multi-caster support)
pfUI.libdebuff_all_auras = pfUI.libdebuff_all_auras or {}
local allAuraCasts = pfUI.libdebuff_all_auras
-- slotOwnership: [targetGUID][auraSlot] = {casterGuid, spellName, spellId}
-- Maps REAL aura slots (33-48) to caster info - NO SHIFTING NEEDED!
pfUI.libdebuff_slot_ownership = pfUI.libdebuff_slot_ownership or {}
local slotOwnership = pfUI.libdebuff_slot_ownership
-- displayToAura: [targetGUID][displaySlot] = auraSlot
-- Maps DISPLAY slots (1-16) to REAL aura slots (33-48) for DEBUFF_REMOVED correlation
pfUI.libdebuff_display_to_aura = pfUI.libdebuff_display_to_aura or {}
local displayToAura = pfUI.libdebuff_display_to_aura
-- pendingCasts: [targetGUID][spellName] = {casterGuid, rank, time}
-- Temporary storage from SPELL_GO to correlate with DEBUFF_ADDED
pfUI.libdebuff_pending = pfUI.libdebuff_pending or {}
local pendingCasts = pfUI.libdebuff_pending
-- Spell Icon Cache: [spellId] = texture
pfUI.libdebuff_icon_cache = pfUI.libdebuff_icon_cache or {}
local iconCache = pfUI.libdebuff_icon_cache
-- Cast Tracking: [casterGuid] = {spellID, spellName, icon, startTime, duration, endTime}
-- Shared with nameplates for cast-bar display
pfUI.libdebuff_casts = pfUI.libdebuff_casts or {}
pfUI.libdebuff_item_icons = pfUI.libdebuff_item_icons or {} -- [casterGuid] = icon (persists across SPELL_GO)
-- Cleveroids API: [targetGUID][spellID] = {start, duration, caster, stacks}
pfUI.libdebuff_objects_guid = pfUI.libdebuff_objects_guid or {}
local objectsByGuid = pfUI.libdebuff_objects_guid
-- LEGACY: Keep these for backwards compatibility (external modules might check them)
pfUI.libdebuff_own_slots = pfUI.libdebuff_own_slots or {}
pfUI.libdebuff_all_slots = pfUI.libdebuff_all_slots or {}
-- Deduplication: Track recent AURA_CAST events to ignore duplicates
-- [targetGuid][spellName][casterGuid] = timestamp
pfUI.libdebuff_recent_casts = pfUI.libdebuff_recent_casts or {}
local recentCasts = pfUI.libdebuff_recent_casts
-- Callbacks fired after SPELL_GO_SELF is processed: fn(spellId, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
pfUI.libdebuff_spell_go_hooks = pfUI.libdebuff_spell_go_hooks or {}
-- Callbacks fired after SPELL_GO_OTHER is processed: fn(spellId, casterGuid, targetGuid)
pfUI.libdebuff_spell_go_other_hooks = pfUI.libdebuff_spell_go_other_hooks or {}
-- Callbacks fired after SPELL_START_SELF is processed: fn(spellId, casterGuid, targetGuid, castTime)
pfUI.libdebuff_spell_start_self_hooks = pfUI.libdebuff_spell_start_self_hooks or {}
-- Callbacks fired after SPELL_START_OTHER is processed: fn(spellId, casterGuid, targetGuid, castTime)
pfUI.libdebuff_spell_start_other_hooks = pfUI.libdebuff_spell_start_other_hooks or {}
-- Callbacks fired after SPELL_FAILED_OTHER is processed: fn(casterGuid, spellId)
pfUI.libdebuff_spell_failed_other_hooks = pfUI.libdebuff_spell_failed_other_hooks or {}
-- Callbacks fired after AURA_CAST_ON_SELF is processed: fn(spellId, casterGuid, targetGuid)
pfUI.libdebuff_aura_cast_on_self_hooks = pfUI.libdebuff_aura_cast_on_self_hooks or {}
-- Callbacks fired after AURA_CAST_ON_OTHER is processed: fn(spellId, casterGuid, targetGuid)
pfUI.libdebuff_aura_cast_on_other_hooks = pfUI.libdebuff_aura_cast_on_other_hooks or {}
-- Callbacks fired after DEBUFF_ADDED_OTHER is processed: fn(guid, luaSlot, spellId, stackCount)
pfUI.libdebuff_debuff_added_other_hooks = pfUI.libdebuff_debuff_added_other_hooks or {}
-- Callbacks fired after DEBUFF_REMOVED_OTHER is processed: fn(guid, luaSlot, spellId, stackCount)
pfUI.libdebuff_debuff_removed_other_hooks = pfUI.libdebuff_debuff_removed_other_hooks or {}
-- Callbacks fired after UNIT_HEALTH is processed: fn(unitToken)
pfUI.libdebuff_unit_health_hooks = pfUI.libdebuff_unit_health_hooks or {}
-- Callbacks fired after PLAYER_TARGET_CHANGED is processed: fn()
pfUI.libdebuff_player_target_changed_hooks = pfUI.libdebuff_player_target_changed_hooks or {}
-- Callbacks fired after UNIT_DIED is processed: fn(guid)
pfUI.libdebuff_unit_died_hooks = pfUI.libdebuff_unit_died_hooks or {}
-- Callbacks fired after SPELL_CAST_EVENT is processed: fn(success, spellId, castType, targetGuid)
pfUI.libdebuff_spell_cast_hooks = pfUI.libdebuff_spell_cast_hooks or {}
-- Callbacks fired when a cast is identified as a downrank of an already active debuff.
-- fn(spellName, castRank, activeRank, targetGuid, casterGuid)
-- External addons can use this to avoid re-implementing downrank detection themselves.
pfUI.libdebuff_downrank_blocked_hooks = pfUI.libdebuff_downrank_blocked_hooks or {}
local AURA_CAST_DEDUPE_WINDOW = 0.1 -- Ignore duplicates within 100ms
-- Captured combo points from SPELL_CAST_EVENT (before client consumes them)
-- SPELL_CAST_EVENT fires BEFORE UnitAura updates, so GetComboPoints() still works
local capturedCP = nil
-- Pending cast info for libpredict (heal prediction target tracking)
-- SPELL_CAST_EVENT fires with targetGuid BEFORE SPELLCAST_START,
-- which allows libpredict to know the correct target for queued casts.
-- Fields: { spellId, spellName, targetGuid, time }
pfUI.libpredict_pending_cast = pfUI.libpredict_pending_cast or {}
-- ============================================================================
-- STATIC POPUP DIALOGS
-- ============================================================================
local function CopyNampowerLink()
pfUI.chat.urlcopy.CopyText("https://github.com/brues-code/nampower")
end
StaticPopupDialogs["LIBDEBUFF_NAMPOWER_UPDATE"] = {
text = "|cffff0000!!!WARNING!!!|r\n\nNampower Update Required!\n\nYour current version: %s\nRequired version: 3.0.0+\n\nPlease update Nampower to continue using pfUI!",
button1 = "Show Download",
button2 = "Dismiss",
timeout = 0,
whileDead = 1,
hideOnEscape = 0,
preferredIndex = 3,
OnAccept = CopyNampowerLink,
}
StaticPopupDialogs["LIBDEBUFF_NAMPOWER_MISSING"] = {
text = "|cffff0000!!!WARNING!!!|r\n\nNampower Not Found!\n\nNampower 3.0.0+ is required for pfUI to function correctly.\n\nPlease install Nampower!",
button1 = "Show Download",
button2 = "Dismiss",
timeout = 0,
whileDead = 1,
hideOnEscape = 0,
preferredIndex = 3,
OnAccept = CopyNampowerLink,
}
-- ============================================================================
-- SPELL DATA TABLES
-- ============================================================================
-- Debuffs that only ONE player can have on target (overwrites other casters)
local selfOverwriteDebuffs = {
["Faerie Fire"] = true,
["Faerie Fire (Feral)"] = true,
["Demoralizing Shout"] = true,
["Demoralizing Roar"] = true,
["Hunter's Mark"] = true,
["Sunder Armor"] = true,
["Thunder Clap"] = true,
["Expose Armor"] = true,
["Curse of Weakness"] = true,
["Curse of Recklessness"] = true,
["Curse of the Elements"] = true,
["Curse of Shadow"] = true,
["Curse of Tongues"] = true,
["Curse of Exhaustion"] = true,
["Judgement of Wisdom"] = true,
["Judgement of Light"] = true,
["Judgement of the Crusader"] = true,
["Judgement of Justice"] = true,
["Shadow Weaving"] = true,
["Winter's Chill"] = true,
}
-- Debuff pairs that overwrite each other
local debuffOverwritePairs = {
["Faerie Fire"] = "Faerie Fire (Feral)",
["Faerie Fire (Feral)"] = "Faerie Fire",
["Demoralizing Shout"] = "Demoralizing Roar",
["Demoralizing Roar"] = "Demoralizing Shout",
}
-- Combopoint-based abilities: Only show timers for OUR casts
-- Format: [spellName] = { base = N, perCP = N }
-- Duration formula: duration = base + combopoints * perCP
local combopointAbilities = {
-- Druid
["Rip"] = { base = 8, perCP = 2 },
-- Rogue
["Rupture"] = { base = 6, perCP = 2 },
["Kidney Shot"] = { base = 1, perCP = 1 },
["Slice and Dice"] = { base = 9, perCP = 3 },
["Expose Armor"] = { base = 30, perCP = 0 }, -- fixed 30s
}
-- ============================================================================
-- HELPER FUNCTIONS
-- ============================================================================
-- Check if spell is a combo-point ability
local function IsComboPointAbility(spellName)
if not spellName then return false end
return combopointAbilities[spellName] ~= nil
end
-- Get combo-point spell data (base duration and per-CP bonus)
local function GetComboPointData(spellName)
if not spellName then return nil, nil end
local cpData = combopointAbilities[spellName]
if cpData then
return cpData.base, cpData.perCP
end
return nil, nil
end
-- Player GUID Cache
local playerGUID = nil
local function GetPlayerGUID()
if not playerGUID and UnitGUID then
local guid = UnitGUID("player")
playerGUID = guid
end
return playerGUID
end
-- Debug Stats
pfUI.libdebuff_debugstats = pfUI.libdebuff_debugstats or {
enabled = false,
trackAllUnits = false,
aura_cast = 0,
debuff_added = 0,
debuff_removed = 0,
getunitfield_calls = 0,
}
local debugStats = pfUI.libdebuff_debugstats
local function DebugGuid(guid)
if not guid then return "nil" end
local str = tostring(guid)
if string.len(str) > 4 then
return string.sub(str, -4)
end
return str
end
local function IsCurrentTarget(guid)
if debugStats.trackAllUnits then return true end
if not guid or not UnitGUID then return false end
local targetGuid = UnitGUID("target")
return targetGuid == guid
end
local function GetDebugTimestamp()
return string.format("[%.3f]", GetTime())
end
-- Speichert die Ranks der zuletzt gecasteten Spells
pfUI.libdebuff_lastranks = pfUI.libdebuff_lastranks or {}
local lastCastRanks = pfUI.libdebuff_lastranks
-- Speichert Spells die gefailed sind
pfUI.libdebuff_lastfailed = pfUI.libdebuff_lastfailed or {}
local lastFailedSpells = pfUI.libdebuff_lastfailed
-- Get spell icon texture (with caching)
function libdebuff:GetSpellIcon(spellId)
if not spellId or type(spellId) ~= "number" or spellId <= 0 then
return "Interface\\Icons\\INV_Misc_QuestionMark"
end
if iconCache[spellId] then
return iconCache[spellId]
end
local texture = C_Spell.GetSpellTexture(spellId) or "Interface\\Icons\\INV_Misc_QuestionMark"
iconCache[spellId] = texture
return texture
end
pfUI.libdebuff_GetSpellIcon = function(spellId)
return libdebuff:GetSpellIcon(spellId)
end
function libdebuff:DidSpellFail(spell)
if not spell then return false end
local data = lastFailedSpells[spell]
if data and (GetTime() - data.time) < 1 then
return true
end
return false
end
-- ============================================================================
-- CORE: GetUnitField-based Slot Mapping (THE KEY INNOVATION!)
-- ============================================================================
-- Dispel type mapping: SpellRec.dispel index -> Blizzard DebuffTypeColor key
local dispelTypeMap = {
[1] = "Magic",
[2] = "Curse",
[3] = "Disease",
[4] = "Poison",
}
-- Get current debuff state directly from WoW via GetUnitField
-- Returns: { [displaySlot] = {auraSlot, spellId, spellName, stacks, texture, dtype} }
local function GetDebuffSlotMap(guid)
if not guid or not GetUnitField then
return nil
end
local auras = GetUnitField(guid, "aura")
if not auras then return nil end
local auraApps = GetUnitField(guid, "auraApplications")
if debugStats.enabled then
debugStats.getunitfield_calls = debugStats.getunitfield_calls + 1
end
local map = {}
local displaySlot = 0
-- Debuff aura slots are 33-48
for auraSlot = 33, 48 do
local spellId = auras[auraSlot]
if spellId and spellId > 0 then
displaySlot = displaySlot + 1
local spellName = GetSpellRecField and GetSpellRecField(spellId, "name")
local texture = libdebuff:GetSpellIcon(spellId)
local stacks = (auraApps and auraApps[auraSlot] or 0) + 1
local dtype = nil
if GetSpellRecField then
local dispelId = GetSpellRecField(spellId, "dispel")
if dispelId and dispelId > 0 then
dtype = dispelTypeMap[dispelId]
end
end
map[displaySlot] = {
auraSlot = auraSlot,
spellId = spellId,
spellName = spellName or "Unknown",
stacks = stacks,
texture = texture,
dtype = dtype
}
end
end
return map
end
-- Get caster info for a specific aura slot
local function GetSlotCaster(guid, auraSlot, spellName)
-- First check our ownership tracking
if slotOwnership[guid] and slotOwnership[guid][auraSlot] then
local ownership = slotOwnership[guid][auraSlot]
-- Verify spell name matches (slot might have been reused)
if ownership.spellName == spellName then
return ownership.casterGuid, ownership.isOurs
end
end
-- Fallback: Check ownDebuffs
local myGuid = GetPlayerGUID()
if ownDebuffs[guid] and ownDebuffs[guid][spellName] then
return myGuid, true
end
-- Fallback: Check allAuraCasts for any caster
if allAuraCasts[guid] and allAuraCasts[guid][spellName] then
for casterGuid, data in pairs(allAuraCasts[guid][spellName]) do
local timeleft = (data.startTime + data.duration) - GetTime()
if timeleft > 0 then
return casterGuid, (casterGuid == myGuid)
end
end
end
return nil, false
end
-- ============================================================================
-- CLEANUP FUNCTIONS
-- ============================================================================
local lastRangeCheck = 0
-- Recycled buffers for cleanup (avoids table creation per call)
local _cleanupBuf1 = {}
local _cleanupBuf2 = {}
local function CleanupUnit(guid)
if not guid then return false end
local cleaned = false
if ownDebuffs[guid] then
ownDebuffs[guid] = nil
cleaned = true
end
if slotOwnership[guid] then
slotOwnership[guid] = nil
cleaned = true
end
if allAuraCasts[guid] then
allAuraCasts[guid] = nil
cleaned = true
end
if objectsByGuid[guid] then
objectsByGuid[guid] = nil
cleaned = true
end
if pendingCasts[guid] then
pendingCasts[guid] = nil
cleaned = true
end
if debugStats.enabled and cleaned and IsCurrentTarget(guid) then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff0000[CLEANUP]|r GUID %s", DebugGuid(guid)))
end
return cleaned
end
local function CleanupExpiredTimers(guid)
local now = GetTime()
-- Cleanup ownDebuffs
if ownDebuffs[guid] then
local n = 0
for spellName, data in pairs(ownDebuffs[guid]) do
local timeleft = (data.startTime + data.duration) - now
if timeleft < -2 then -- Grace period
n = n + 1
_cleanupBuf1[n] = spellName
end
end
for i = 1, n do
ownDebuffs[guid][_cleanupBuf1[i]] = nil
_cleanupBuf1[i] = nil
end
end
-- Cleanup allAuraCasts
if allAuraCasts[guid] then
for spellName, casterTable in pairs(allAuraCasts[guid]) do
local n2 = 0
for casterGuid, data in pairs(casterTable) do
local timeleft = (data.startTime + data.duration) - now
if timeleft < -2 then
n2 = n2 + 1
_cleanupBuf2[n2] = casterGuid
end
end
for i = 1, n2 do
allAuraCasts[guid][spellName][_cleanupBuf2[i]] = nil
_cleanupBuf2[i] = nil
end
-- Remove empty spell tables
local hasCasters = false
for _ in pairs(allAuraCasts[guid][spellName]) do
hasCasters = true
break
end
if not hasCasters then
allAuraCasts[guid][spellName] = nil
end
end
end
end
local function CleanupOutOfRangeUnits()
local now = GetTime()
-- Cleanup expired pending entries (every 1s)
if not pfUI.libdebuff_last_pending_cleanup then pfUI.libdebuff_last_pending_cleanup = 0 end
if now - pfUI.libdebuff_last_pending_cleanup >= 1 then
pfUI.libdebuff_last_pending_cleanup = now
for guid, spells in pairs(ownDebuffs) do
for spellName, data in pairs(spells) do
if data.pending then
local timeleft = (data.startTime + data.duration) - now
if timeleft < -2 then
spells[spellName] = nil
end
end
end
end
end
if now - lastRangeCheck < 10 then return end
lastRangeCheck = now
local allGuids = {}
for guid in pairs(ownDebuffs) do allGuids[guid] = true end
for guid in pairs(slotOwnership) do allGuids[guid] = true end
for guid in pairs(allAuraCasts) do allGuids[guid] = true end
for guid in pairs(objectsByGuid) do allGuids[guid] = true end
for guid in pairs(pendingCasts) do allGuids[guid] = true end
for guid in pairs(allGuids) do
local exists = UnitExists and UnitExists(guid)
local isDead = UnitIsDead and UnitIsDead(guid)
if not exists or isDead then
CleanupUnit(guid)
end
end
-- Cleanup old lastCastRanks
for spell, data in pairs(lastCastRanks) do
if now - data.time > 3 then
lastCastRanks[spell] = nil
end
end
-- Cleanup old lastFailedSpells
for spell, data in pairs(lastFailedSpells) do
if now - data.time > 2 then
lastFailedSpells[spell] = nil
end
end
-- Cleanup old pendingCasts
for guid, spells in pairs(pendingCasts) do
for spell, data in pairs(spells) do
if now - data.time > 1 then
pendingCasts[guid][spell] = nil
end
end
local isEmpty = true
for _ in pairs(pendingCasts[guid]) do
isEmpty = false
break
end
if isEmpty then
pendingCasts[guid] = nil
end
end
end
-- ============================================================================
-- DURATION FUNCTIONS
-- ============================================================================
function libdebuff:GetDuration(effect, rank)
if L["debuffs"][effect] then
local rank = rank and tonumber((string.gsub(rank, RANK, ""))) or 0
local rank = L["debuffs"][effect][rank] and rank or libdebuff:GetMaxRank(effect)
local duration = L["debuffs"][effect][rank]
if effect == L["dyndebuffs"]["Rupture"] then
local cp = GetComboPoints() or 0
duration = duration + cp*2
elseif effect == L["dyndebuffs"]["Kidney Shot"] then
local cp = GetComboPoints() or 0
duration = duration + cp*1
elseif effect == "Rip" or effect == L["dyndebuffs"]["Rip"] then
local cp = GetComboPoints() or 0
duration = 8 + cp*2
elseif effect == L["dyndebuffs"]["Demoralizing Shout"] then
local _,_,_,_,count = GetTalentInfo(2,1)
if count and count > 0 then duration = duration + ( duration / 100 * (count*10)) end
elseif effect == L["dyndebuffs"]["Shadow Word: Pain"] then
local _,_,_,_,count = GetTalentInfo(3,4)
if count and count > 0 then duration = duration + count * 3 end
elseif effect == L["dyndebuffs"]["Frostbolt"] then
local _,_,_,_,count = GetTalentInfo(3,7)
if count and count > 0 then duration = duration + count end
elseif effect == L["dyndebuffs"]["Gouge"] then
local _,_,_,_,count = GetTalentInfo(3,3)
if count and count > 0 then duration = duration + (count*.5) end
end
return duration
else
return 0
end
end
function libdebuff:GetMaxRank(effect)
local max = 0
for id in pairs(L["debuffs"][effect]) do
if id > max then max = id end
end
return max
end
function libdebuff:UpdateDuration(unit, unitlevel, effect, duration)
if not unit or not effect or not duration then return end
unitlevel = unitlevel or 0
if libdebuff.objects[unit] and libdebuff.objects[unit][unitlevel] and libdebuff.objects[unit][unitlevel][effect] then
libdebuff.objects[unit][unitlevel][effect].duration = duration
end
end
function libdebuff:UpdateUnits()
if not pfUI.uf or not pfUI.uf.target then return end
pfUI.uf:RefreshUnit(pfUI.uf.target, "aura")
end
-- ============================================================================
-- LEGACY API (for turtle-wow.lua compatibility)
-- ============================================================================
libdebuff.pending = {}
libdebuff.objects = {}
function libdebuff:AddPending(unit, unitlevel, effect, duration, caster, rank)
if not unit or duration <= 0 then return end
if not L["debuffs"][effect] then return end
if libdebuff.pending[3] then return end
libdebuff.pending[1] = unit
libdebuff.pending[2] = unitlevel or 0
libdebuff.pending[3] = effect
libdebuff.pending[4] = duration
libdebuff.pending[5] = caster
libdebuff.pending[6] = rank
QueueFunction(libdebuff.PersistPending)
end
function libdebuff:RemovePending()
libdebuff.pending[1] = nil
libdebuff.pending[2] = nil
libdebuff.pending[3] = nil
libdebuff.pending[4] = nil
libdebuff.pending[5] = nil
libdebuff.pending[6] = nil
end
function libdebuff:PersistPending(effect)
if not libdebuff.pending[3] then return end
if libdebuff.pending[3] == effect or ( effect == nil and libdebuff.pending[3] ) then
local p1, p2, p3, p4, p5, p6 = libdebuff.pending[1], libdebuff.pending[2], libdebuff.pending[3], libdebuff.pending[4], libdebuff.pending[5], libdebuff.pending[6]
libdebuff.AddEffect(libdebuff, p1, p2, p3, p4, p5, p6)
end
libdebuff:RemovePending()
end
function libdebuff:AddEffect(unit, unitlevel, effect, duration, caster, rank)
if not rank and caster == "player" and effect then
if libdebuff.pending[3] == effect and libdebuff.pending[6] then
rank = libdebuff.pending[6]
elseif lastCastRanks[effect] and (GetTime() - lastCastRanks[effect].time) < 2 then
rank = lastCastRanks[effect].rank
end
end
if not unit then return end
unitlevel = unitlevel or 0
-- Create tables if needed
libdebuff.objects[unit] = libdebuff.objects[unit] or {}
libdebuff.objects[unit][unitlevel] = libdebuff.objects[unit][unitlevel] or {}
-- Get duration from spell database if not provided
if not duration or duration == 0 then
duration = libdebuff:GetDuration(effect, rank)
end
-- Store/update effect
local now = GetTime()
local existing = libdebuff.objects[unit][unitlevel][effect]
if existing then
existing.start = now
existing.duration = duration
existing.caster = caster
existing.rank = rank
else
libdebuff.objects[unit][unitlevel][effect] = {
start = now,
duration = duration,
caster = caster,
rank = rank
}
end
lastspell = libdebuff.objects[unit][unitlevel][effect]
end
-- ============================================================================
-- MAIN API: UnitDebuff (GetUnitField-based)
-- ============================================================================
local cache = {}
function libdebuff:UnitDebuff(unit, displaySlot)
local unitname = UnitName(unit)
local unitlevel = UnitLevel(unit)
local duration, timeleft = nil, -1
local rank = nil
local caster = nil
local effect = nil
local texture = nil
local stacks = 0
local dtype = nil
-- Nampower: Use GetUnitField for ALL debuff data (no Blizzard UnitDebuff needed)
if hasNampower and UnitGUID then
local guid = UnitGUID(unit)
if not guid then
-- Safety fallback: no GUID available (should not happen with Nampower)
local aura = C_UnitAuras.GetDebuffDataByIndex(unit, displaySlot)
if aura then
return aura.name, rank, aura.icon, aura.applications, aura.dispelName, duration, timeleft, caster
end
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
end
-- Get current slot map from GetUnitField (cached 50ms)
local slotMap = GetDebuffSlotMap(guid)
if not slotMap or not slotMap[displaySlot] then
return nil
end
local slotData = slotMap[displaySlot]
effect = slotData.spellName
texture = slotData.texture
stacks = slotData.stacks
dtype = slotData.dtype
local auraSlot = slotData.auraSlot
-- Get caster info for this slot
local slotCasterGuid, isOurs = GetSlotCaster(guid, auraSlot, effect)
if isOurs then
-- OUR debuff - get timer from ownDebuffs
if ownDebuffs[guid] and ownDebuffs[guid][effect] then
local data = ownDebuffs[guid][effect]
local remaining = (data.startTime + data.duration) - GetTime()
if remaining > 0 then
duration = data.duration
timeleft = remaining
caster = "player"
rank = data.rank
elseif remaining > -1 then
-- Grace period - show 0 timeleft
duration = data.duration
timeleft = 0
caster = "player"
rank = data.rank
end
end
else
-- OTHER player's debuff - get timer from allAuraCasts
if slotCasterGuid and allAuraCasts[guid] and allAuraCasts[guid][effect] then
local data = allAuraCasts[guid][effect][slotCasterGuid]
if data then
local remaining = (data.startTime + data.duration) - GetTime()
if remaining > 0 and data.duration > 0 then
duration = data.duration
timeleft = remaining
caster = "other"
rank = data.rank
end
end
end
-- Fallback: Search all casters if specific one not found
if not duration and allAuraCasts[guid] and allAuraCasts[guid][effect] then
for anyCasterGuid, data in pairs(allAuraCasts[guid][effect]) do
local remaining = (data.startTime + data.duration) - GetTime()
if remaining > 0 and data.duration > 0 then
duration = data.duration
timeleft = remaining
caster = "other"
rank = data.rank
break
end
end
end
end
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
end
-- ============================================================================
-- FALLBACK: Legacy (non-Nampower) system
-- ============================================================================
local aura = C_UnitAuras.GetDebuffDataByIndex(unit, displaySlot)
if aura then
texture = aura.icon
stacks = aura.applications
dtype = aura.dispelName
effect = aura.name
end
if effect and libdebuff.objects[unitname] then
for level, effects in pairs(libdebuff.objects[unitname]) do
if effects[effect] and effects[effect].duration then
local timeleft = effects[effect].start and
effects[effect].start + effects[effect].duration - GetTime()
if timeleft and timeleft > 0 then
return effect, effects[effect].rank, texture, stacks, dtype,
effects[effect].duration, timeleft, effects[effect].caster
end
end
end
end
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
end
-- ============================================================================
-- API: UnitOwnDebuff (only OUR debuffs)
-- ============================================================================
-- Pre-defined sort function for UnitOwnDebuff (avoids closure creation per call)
local _ownDebuffSortFunc = function(a, b)
if a.data.startTime == b.data.startTime then
return a.spellName < b.spellName
end
return a.data.startTime < b.data.startTime
end
function libdebuff:UnitOwnDebuff(unit, id)
if hasNampower and UnitGUID then
local guid = UnitGUID(unit)
if guid and ownDebuffs[guid] then
-- Build sorted list of our active debuffs
local sortedDebuffs = {}
local now = GetTime()
local toRemove = nil
for spellName, data in pairs(ownDebuffs[guid]) do
local timeleft = (data.startTime + data.duration) - now
if timeleft > 0 then
local count = table.getn(sortedDebuffs) + 1
sortedDebuffs[count] = {
spellName = spellName,
data = data,
timeleft = timeleft
}
elseif data.pending then
if timeleft < -2 then
toRemove = toRemove or {}
toRemove[spellName] = true
end
else
toRemove = toRemove or {}
toRemove[spellName] = true
end
end
if toRemove then
for spellName in pairs(toRemove) do
ownDebuffs[guid][spellName] = nil
end
end
-- Sort by startTime (oldest first = lowest display slot)
-- If startTime is equal (e.g. after Carnage refresh), use spellName for stable sorting
table.sort(sortedDebuffs, _ownDebuffSortFunc)
-- Return debuff at position 'id'
if sortedDebuffs[id] then
local entry = sortedDebuffs[id]
local texture = entry.data.texture or "Interface\\Icons\\INV_Misc_QuestionMark"
local displayTimeleft = entry.timeleft > 0 and entry.timeleft or 0
-- Get dtype from SpellRec DBC via stored spellId
local entryDtype = nil
if entry.data.spellId and GetSpellRecField then
local dispelId = GetSpellRecField(entry.data.spellId, "dispel")
if dispelId and dispelId > 0 then
entryDtype = dispelTypeMap[dispelId]
end
end
return entry.spellName, entry.data.rank, texture, 1, entryDtype, entry.data.duration, displayTimeleft, "player"
end
end
return nil
end
-- Fallback: Iterate through all debuffs and filter
for k in pairs(cache) do cache[k] = nil end
local count = 1
for i=1,16 do
local effect, rank, texture, stacks, dtype, duration, timeleft, caster = libdebuff:UnitDebuff(unit, i)
if effect and not cache[effect] and caster and caster == "player" then
cache[effect] = true
if count == id then
return effect, rank, texture, stacks, dtype, duration, timeleft, caster
else
count = count + 1
end
end
end
end
-- ============================================================================
-- API: GetBestAuraCast (for libpredict HoT tracking)
-- ============================================================================
function libdebuff:GetBestAuraCast(guid, spellName)
if not guid or not spellName then return nil end
-- Check ownDebuffs first (for our casts)
if ownDebuffs[guid] and ownDebuffs[guid][spellName] then
local data = ownDebuffs[guid][spellName]
local timeleft = (data.startTime + data.duration) - GetTime()
if timeleft > 0 then
return data.startTime, data.duration, timeleft, data.rank, GetPlayerGUID()
end
end
-- Check allAuraCasts (for any caster)
if allAuraCasts[guid] and allAuraCasts[guid][spellName] then
local bestData = nil
local bestCaster = nil
local bestTimeleft = 0
for casterGuid, data in pairs(allAuraCasts[guid][spellName]) do
local timeleft = (data.startTime + data.duration) - GetTime()
if timeleft > bestTimeleft then
bestTimeleft = timeleft
bestData = data
bestCaster = casterGuid
end
end
if bestData and bestTimeleft > 0 then
return bestData.startTime, bestData.duration, bestTimeleft, bestData.rank, bestCaster
end
end
return nil
end
-- ============================================================================
-- API: GetEnhancedDebuffs (for external modules)
-- ============================================================================
function libdebuff:GetEnhancedDebuffs(targetGUID)
if not targetGUID then return nil end
local result = {}
if ownDebuffs[targetGUID] then
local myGuid = GetPlayerGUID()
for spellName, data in pairs(ownDebuffs[targetGUID]) do
local timeleft = (data.startTime + data.duration) - GetTime()
if timeleft > 0 then
result[spellName] = result[spellName] or {}
result[spellName][myGuid] = {
startTime = data.startTime,
duration = data.duration,
texture = data.texture,
rank = data.rank
}
end
end
end
return result
end
-- ============================================================================
-- NAMPOWER EVENT HANDLING
-- ============================================================================
if hasNampower then
-- Carnage Talent Rank
local carnageRank = 0
local function UpdateCarnageRank()
if class ~= "DRUID" then return end
local _, _, _, _, rank = GetTalentInfo(2, 17)
carnageRank = rank or 0
end
-- Persistent Carnage check frame (reused instead of CreateFrame per Bite)
local carnageState = nil -- {targetGuid, checkTime}
local carnageCheckFrame = CreateFrame("Frame")
carnageCheckFrame:Hide()
carnageCheckFrame:SetScript("OnUpdate", function()
if not carnageState then
this:Hide()
return
end
if GetTime() < carnageState.checkTime then return end
-- Check if we gained a combo point (indicates Carnage proc)
local cp = GetComboPoints() or 0
if cp > 0 then
-- Carnage triggered! Refresh Rip & Rake
local guid = carnageState.targetGuid
local refreshTime = GetTime()
local myGuid = GetPlayerGUID()
-- Refresh in ownDebuffs - only if timer still active
if ownDebuffs[guid] then
if ownDebuffs[guid]["Rip"] then
local timeleft = (ownDebuffs[guid]["Rip"].startTime + ownDebuffs[guid]["Rip"].duration) - refreshTime
if timeleft > 0 then
ownDebuffs[guid]["Rip"].startTime = refreshTime
if debugStats.enabled then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[CARNAGE]|r Rip refreshed (CP detected)")
end
end
end
if ownDebuffs[guid]["Rake"] then
local timeleft = (ownDebuffs[guid]["Rake"].startTime + ownDebuffs[guid]["Rake"].duration) - refreshTime
if timeleft > 0 then
ownDebuffs[guid]["Rake"].startTime = refreshTime
if debugStats.enabled then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[CARNAGE]|r Rake refreshed (CP detected)")
end
end
end
end
-- Refresh in allAuraCasts - only if timer still active
if allAuraCasts[guid] then
if allAuraCasts[guid]["Rip"] and allAuraCasts[guid]["Rip"][myGuid] then
local d = allAuraCasts[guid]["Rip"][myGuid]
if (d.startTime + d.duration) > refreshTime then
d.startTime = refreshTime
end
end
if allAuraCasts[guid]["Rake"] and allAuraCasts[guid]["Rake"][myGuid] then
local d = allAuraCasts[guid]["Rake"][myGuid]
if (d.startTime + d.duration) > refreshTime then
d.startTime = refreshTime
end
end
end
-- Trigger UI updates
if pfTarget and UnitGUID("target") then
local currentTargetGuid = UnitGUID("target")
if currentTargetGuid == guid then
pfTarget.update_aura = true
end
end
if pfUI.nameplates and pfUI.nameplates.OnAuraUpdate then
pfUI.nameplates:OnAuraUpdate(guid)
end
end
carnageState = nil
this:Hide()
end)
local frame = CreateFrame("Frame")
frame:RegisterEvent("PLAYER_ENTERING_WORLD")
frame:RegisterEvent("PLAYER_TALENT_UPDATE")
frame:RegisterEvent("PLAYER_LOGOUT")
frame:RegisterEvent("SPELLCAST_CHANNEL_STOP")
frame:RegisterEvent("SPELL_START_SELF")
frame:RegisterEvent("SPELL_START_OTHER")
frame:RegisterEvent("SPELL_GO_SELF")
frame:RegisterEvent("SPELL_GO_OTHER")
frame:RegisterEvent("SPELL_FAILED_OTHER")
frame:RegisterEvent("UNIT_DIED")
frame:RegisterEvent("SPELL_CAST_EVENT")
frame:RegisterEvent("AURA_CAST_ON_SELF")
frame:RegisterEvent("AURA_CAST_ON_OTHER")
frame:RegisterEvent("DEBUFF_ADDED_OTHER")
frame:RegisterEvent("DEBUFF_REMOVED_OTHER")
frame:RegisterEvent("PLAYER_TARGET_CHANGED")
frame:RegisterEvent("UNIT_HEALTH")
frame:SetScript("OnEvent", function()
if event == "PLAYER_LOGOUT" then
this:UnregisterAllEvents()
this:SetScript("OnEvent", nil)
return
elseif event == "SPELLCAST_CHANNEL_STOP" then
-- Channel interrupted by player - clear ownDebuffs for the channeled spell immediately.
-- DEBUFF_REMOVED fires later (0.5-1s server lag), causing phantom debuff display.
-- We look up the active channel cast and pre-clear ownDebuffs for its target.
local myGuid = GetPlayerGUID()
local castData = myGuid and pfUI.libdebuff_casts[myGuid]
if castData and castData.event == "CHANNEL" and castData.spellName then
local spellName = castData.spellName
local targetGuid = UnitGUID and UnitGUID("target")
if targetGuid and ownDebuffs[targetGuid] and ownDebuffs[targetGuid][spellName] then
local data = ownDebuffs[targetGuid][spellName]
-- Only clear if the timer is still active (not already expired naturally)
local remaining = (data.startTime + data.duration) - GetTime()
if remaining > 0 then
ownDebuffs[targetGuid][spellName] = nil
end
end
pfUI.libdebuff_casts[myGuid] = nil
end
elseif event == "PLAYER_ENTERING_WORLD" then
GetPlayerGUID()
UpdateCarnageRank()
elseif event == "PLAYER_TALENT_UPDATE" then
UpdateCarnageRank()
elseif event == "UNIT_HEALTH" then
local guid = arg1
if guid and UnitIsDead and UnitIsDead(guid) then
CleanupUnit(guid)
end
if pfUI.libdebuff_unit_health_hooks then
for _, fn in pairs(pfUI.libdebuff_unit_health_hooks) do
fn(arg1)
end
end
elseif event == "SPELL_START_SELF" or event == "SPELL_START_OTHER" then
local itemId = arg1
local spellId = arg2
local casterGuid = arg3
local spellType = arg8 or 0 -- 0=Normal, 1=Channel, 2=Autorepeating
-- arg6=castTime, arg7=channel duration
-- prefer arg6 if present — some spells (e.g. Volley post-rework) still send
-- arg8=1 but now have a real cast time in arg6, so we only fall back to arg7
-- when arg6 is nil (true channels like Blizzard)
-- arg6=castTime (ms), arg7=channel duration (ms), arg8=spellType
-- For channels: arg6=0 (no cast time), arg7=duration, spellType=1
-- For normal casts: arg6=castTime, arg7=0, spellType=0
-- Note: "not arg6" is wrong in Lua since 0 is truthy - use arg6 == 0 or nil
local castTime = (arg6 and arg6 > 0) and arg6 or arg7
local isChannel = spellType == 1 and (not arg6 or arg6 == 0)
if not casterGuid or not spellId then return end
local spellName = C_Spell.GetSpellName(spellId)
local icon = libdebuff:GetSpellIcon(spellId)
-- Use item icon for item-triggered casts
if itemId and itemId > 0 then
icon = C_Item.GetItemIconByID(itemId) or icon
pfUI.libdebuff_item_icons[casterGuid] = {
icon = icon,
name = GetItemInfo(itemId),
}
else
pfUI.libdebuff_item_icons[casterGuid] = nil
end
pfUI.libdebuff_casts[casterGuid] = {
spellID = spellId,
itemID = itemId and itemId > 0 and itemId or nil,
spellName = spellName,
icon = icon,
startTime = GetTime(),
duration = castTime and castTime / 1000 or 0,
endTime = castTime and (GetTime() + castTime / 1000) or nil,
event = isChannel and "CHANNEL" or "START"
}
if event == "SPELL_START_SELF" and pfUI.libdebuff_spell_start_self_hooks then
for _, fn in pairs(pfUI.libdebuff_spell_start_self_hooks) do
fn(spellId, casterGuid, arg4, castTime)
end
elseif event == "SPELL_START_OTHER" and pfUI.libdebuff_spell_start_other_hooks then
for _, fn in pairs(pfUI.libdebuff_spell_start_other_hooks) do
fn(spellId, casterGuid, arg4, castTime)
end
end
elseif event == "SPELL_GO_SELF" or event == "SPELL_GO_OTHER" then
local itemId = arg1
local spellId = arg2
local casterGuid = arg3
local targetGuid = arg4
local numHit = arg6 or 0
local numMissed = arg7 or 0
-- Clear cast bar only if SPELL_GO matches the active cast
-- (Reactive procs like Frost Armor trigger SPELL_GO but shouldn't clear the castbar)
-- Don't clear channels on SPELL_GO - channels persist until duration expires or SPELL_FAILED
if casterGuid and pfUI.libdebuff_casts[casterGuid] then
if pfUI.libdebuff_casts[casterGuid].spellID == spellId and pfUI.libdebuff_casts[casterGuid].event ~= "CHANNEL" then
pfUI.libdebuff_casts[casterGuid] = nil
end
end
-- Fire registered SPELL_GO_SELF hooks BEFORE miss guard
-- (Swingtimer needs to see ALL casts, even misses, for swing reset)
if event == "SPELL_GO_SELF" and pfUI.libdebuff_spell_go_hooks then
for _, fn in pairs(pfUI.libdebuff_spell_go_hooks) do
fn(spellId, arg1, arg2, arg3, arg4, arg5, arg6, arg7)
end
end
if numMissed > 0 or numHit == 0 then return end
local spellName = GetSpellRecField and GetSpellRecField(spellId, "name")
local spellRankString = GetSpellRecField and GetSpellRecField(spellId, "rank")
if not spellName then return end
local castRank = 0
if spellRankString and spellRankString ~= "" then
castRank = tonumber((string.gsub(spellRankString, "Rank ", ""))) or 0
end
-- Store in pendingCasts for DEBUFF_ADDED correlation.
-- If this cast is a downrank of an already active debuff, fire the downrank blocked hook
-- so external addons (e.g. SuperCleveRoidMacros) don't need to re-implement this check.
if targetGuid then
pendingCasts[targetGuid] = pendingCasts[targetGuid] or {}
local isDownrankBlocked = false
if castRank > 0 then
local existingOwn = ownDebuffs[targetGuid] and ownDebuffs[targetGuid][spellName]
if existingOwn and existingOwn.rank and existingOwn.rank > castRank then
local existingTimeleft = (existingOwn.startTime + existingOwn.duration) - GetTime()
if existingTimeleft > 0 then
isDownrankBlocked = true
-- Fire hook so external addons know this cast was downrank-blocked
if pfUI.libdebuff_downrank_blocked_hooks then
for _, fn in pairs(pfUI.libdebuff_downrank_blocked_hooks) do
fn(spellName, castRank, existingOwn.rank, targetGuid, casterGuid)
end
end
end
end
end
-- Always write pendingCasts — let consumers use the hook to handle downrank themselves
pendingCasts[targetGuid][spellName] = {
casterGuid = casterGuid,
rank = castRank,
time = GetTime(),
downrankBlocked = isDownrankBlocked
}
end
-- selfdebuff mode: write ownDebuffs immediately on confirmed hit
-- so buffwatch shows our debuffs even when over the 16 debuff cap
if event == "SPELL_GO_SELF" and targetGuid and not isNullTarget then
local selfdebuffMode = pfUI_config and pfUI_config.buffbar and
pfUI_config.buffbar.tdebuff and pfUI_config.buffbar.tdebuff.selfdebuff == "1"
if selfdebuffMode then
local myGuid2 = GetPlayerGUID()
if casterGuid == myGuid2 then
local duration = libdebuff:GetDuration(spellName, castRank) or 0
if duration > 0 then
local texture = libdebuff:GetSpellIcon(spellId)
ownDebuffs[targetGuid] = ownDebuffs[targetGuid] or {}
-- Downrank protection
local existing = ownDebuffs[targetGuid][spellName]
local blocked = false
if existing and existing.rank and castRank > 0 and existing.rank > castRank then
local existingTimeleft = (existing.startTime + existing.duration) - GetTime()
if existingTimeleft > 0 then blocked = true end
end
if not blocked then
ownDebuffs[targetGuid][spellName] = {
startTime = GetTime(),
duration = duration,
texture = texture,
rank = castRank,
spellId = spellId,
stacks = 1,
pending = true,
}
end
end
end
end
end
-- Store rank for our casts
local myGuid = GetPlayerGUID()
if casterGuid == myGuid then
lastCastRanks[spellName] = {
rank = castRank,
time = GetTime()
}
end
-- CARNAGE TALENT: Ferocious Bite refreshes Rip & Rake
-- Check for combo point gain after Bite (indicates Carnage proc)
-- Carnage gives +1 CP immediately after Bite if it procs
if class == "DRUID" and carnageRank >= 1 and spellName == "Ferocious Bite" and casterGuid == myGuid then
if targetGuid and numHit > 0 then
-- Schedule delayed check (50ms to allow CP to register)
carnageState = {
targetGuid = targetGuid,
checkTime = GetTime() + 0.05
}
carnageCheckFrame:Show()
end
end
-- Fire registered SPELL_GO_OTHER hooks
if event == "SPELL_GO_OTHER" and pfUI.libdebuff_spell_go_other_hooks then
for _, fn in pairs(pfUI.libdebuff_spell_go_other_hooks) do
fn(spellId, casterGuid, targetGuid)
end
end
elseif event == "UNIT_DIED" then
if pfUI.libdebuff_unit_died_hooks then
for _, fn in pairs(pfUI.libdebuff_unit_died_hooks) do
fn(arg1)
end
end
elseif event == "SPELL_FAILED_OTHER" then
local casterGuid = arg1
local spellId = arg2
if casterGuid and pfUI.libdebuff_casts[casterGuid] then
-- Only clear if spellID matches to avoid clearing a cast that already moved on
if pfUI.libdebuff_casts[casterGuid].spellID == spellId then
pfUI.libdebuff_casts[casterGuid] = nil
end
end
if pfUI.libdebuff_spell_failed_other_hooks then
for _, fn in pairs(pfUI.libdebuff_spell_failed_other_hooks) do
fn(casterGuid, arg2)
end
end
elseif event == "SPELL_CAST_EVENT" then
-- Capture combo points BEFORE they're consumed
-- This event fires when YOU cast a spell (before server processes it)
local success = arg1
local spellId = arg2
local castType = arg3
local targetGuid = arg4
if success ~= 1 or not spellId then return end
local spellName = C_Spell.GetSpellName(spellId)
-- Store pending cast info for libpredict (heal prediction target tracking)
-- This allows libpredict to resolve the correct target for Nampower queued casts,
-- where CastSpellByName hook fires while current_cast is set and spell_queue
-- cannot be updated. SPELL_CAST_EVENT fires right before SPELLCAST_START.
if spellName and targetGuid and targetGuid ~= "" and targetGuid ~= "0x0000000000000000" then
pfUI.libpredict_pending_cast.spellId = spellId
pfUI.libpredict_pending_cast.spellName = spellName
pfUI.libpredict_pending_cast.targetGuid = targetGuid
pfUI.libpredict_pending_cast.time = GetTime()
else
-- No explicit target - clear pending so libpredict falls back to spell_queue
pfUI.libpredict_pending_cast.spellId = nil
pfUI.libpredict_pending_cast.spellName = nil
pfUI.libpredict_pending_cast.targetGuid = nil
pfUI.libpredict_pending_cast.time = nil
end
-- Only capture CPs for combo-point abilities
if spellName and IsComboPointAbility(spellName) then
capturedCP = GetComboPoints() or 0
end
-- Fire registered SPELL_CAST_EVENT hooks
if pfUI.libdebuff_spell_cast_hooks then
for _, fn in pairs(pfUI.libdebuff_spell_cast_hooks) do
fn(success, spellId, castType, targetGuid)
end
end
elseif event == "AURA_CAST_ON_SELF" or event == "AURA_CAST_ON_OTHER" then
local spellId = arg1
local casterGuid = arg2
local targetGuid = arg3
local effect = arg4
local effectAuraName = arg5
local effectAmplitude = arg6
local effectMiscValue = arg7
local durationMs = arg8
local auraCapStatus = arg9
if not spellId then return end
if not targetGuid or targetGuid == "" or targetGuid == "0x0000000000000000" then return end
local spellName = GetSpellRecField and GetSpellRecField(spellId, "name")
if not spellName then return end
-- Deduplicate: Ignore if we processed this exact cast recently (within 100ms)
-- Nampower fires multiple AURA_CAST events for multi-effect spells (e.g. Faerie Fire has 3 effects)
recentCasts[targetGuid] = recentCasts[targetGuid] or {}
recentCasts[targetGuid][spellName] = recentCasts[targetGuid][spellName] or {}
local now = GetTime()
local lastCastTime = recentCasts[targetGuid][spellName][casterGuid]
if lastCastTime and (now - lastCastTime) < AURA_CAST_DEDUPE_WINDOW then
return -- Duplicate event, ignore
end
recentCasts[targetGuid][spellName][casterGuid] = now
-- Rank aus spellId ermitteln
local rankNum = 0
local rankString = GetSpellRecField(spellId, "rank")
if rankString and rankString ~= "" then
rankNum = tonumber((string.gsub(rankString, "Rank ", ""))) or 0
end
local duration = durationMs and (durationMs / 1000) or 0
local startTime = GetTime()
local myGuid = GetPlayerGUID()
local isOurs = (myGuid and casterGuid == myGuid)
if debugStats.enabled and isOurs then
debugStats.aura_cast = debugStats.aura_cast + 1
end
-- Combo-point abilities: Calculate duration based on CPs used
if IsComboPointAbility(spellName) then
if isOurs then
-- OWN casts: use captured CPs from SPELL_CAST_EVENT (if available)
local cp = capturedCP or 0
local base, perCP = GetComboPointData(spellName)
if base and perCP then
duration = base + cp * perCP
else
-- Fallback to legacy database
duration = libdebuff:GetDuration(spellName, rankNum)
end
capturedCP = nil -- consumed
else
-- OTHER players: CP unknown, no timer (except Expose Armor = fixed 30s)
local base, perCP = GetComboPointData(spellName)
if perCP and perCP == 0 and base then
duration = base -- fixed duration (Expose Armor)
else
duration = 0 -- CP unknown for other players
end
end
elseif duration == 0 then
-- Non-CP managed spells: use database if AURA_CAST returned 0
duration = libdebuff:GetDuration(spellName, rankNum) or 0
end
-- Store in allAuraCasts
if targetGuid and targetGuid ~= "" and targetGuid ~= "0x0000000000000000" then
allAuraCasts[targetGuid] = allAuraCasts[targetGuid] or {}
allAuraCasts[targetGuid][spellName] = allAuraCasts[targetGuid][spellName] or {}
-- Downrank Protection: Check BEFORE clearing old casters!
-- For selfOverwrite debuffs, check ALL existing casters
if selfOverwriteDebuffs[spellName] then
for otherCaster, existingData in pairs(allAuraCasts[targetGuid][spellName]) do
if existingData.rank and rankNum and rankNum > 0 then
local existingTimeleft = (existingData.startTime + existingData.duration) - GetTime()
if existingTimeleft > 0 and rankNum < existingData.rank then
-- Lower rank cannot overwrite higher rank - block the update
if debugStats.enabled then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff0000[DOWNRANK BLOCKED]|r %s: Rank %d from %s cannot overwrite Rank %d from %s (%.1fs left)",
spellName, rankNum, DebugGuid(casterGuid), existingData.rank, DebugGuid(otherCaster), existingTimeleft))
end
return
end
end
end
else
-- For non-selfOverwrite: Check only same caster
local existingData = allAuraCasts[targetGuid][spellName][casterGuid]
if existingData and existingData.rank and rankNum and rankNum > 0 then
local existingTimeleft = (existingData.startTime + existingData.duration) - GetTime()
if existingTimeleft > 0 and rankNum < existingData.rank then
if debugStats.enabled and isOurs then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff0000[DOWNRANK BLOCKED]|r %s: Rank %d cannot overwrite Rank %d (%.1fs left)",
spellName, rankNum, existingData.rank, existingTimeleft))
end
return
end
end
end
-- Handle self-overwrite debuffs (clear other casters)
if selfOverwriteDebuffs[spellName] then
local n = 0
for otherCaster in pairs(allAuraCasts[targetGuid][spellName]) do
if otherCaster ~= casterGuid then
n = n + 1
_cleanupBuf1[n] = otherCaster
end
end
for i = 1, n do
allAuraCasts[targetGuid][spellName][_cleanupBuf1[i]] = nil
_cleanupBuf1[i] = nil
end
-- Clear from ownDebuffs if we're being overwritten
if not isOurs and ownDebuffs[targetGuid] and ownDebuffs[targetGuid][spellName] then
ownDebuffs[targetGuid][spellName] = nil
end
end
-- Handle variant pairs (Faerie Fire <-> Faerie Fire (Feral))
if debuffOverwritePairs[spellName] then
local otherVariant = debuffOverwritePairs[spellName]
if allAuraCasts[targetGuid][otherVariant] and allAuraCasts[targetGuid][otherVariant][casterGuid] then
allAuraCasts[targetGuid][otherVariant][casterGuid] = nil
end
end
-- Store timer data
allAuraCasts[targetGuid][spellName][casterGuid] = {
startTime = startTime,
duration = duration,
rank = rankNum
}
-- UPDATE slotOwnership for selfOverwrite refreshes
-- (DEBUFF_ADDED doesn't fire on refresh, so we must update here!)
if selfOverwriteDebuffs[spellName] and slotOwnership[targetGuid] then
for auraSlot, ownership in pairs(slotOwnership[targetGuid]) do
if ownership.spellName == spellName then
-- Update the casterGuid and isOurs for this slot
ownership.casterGuid = casterGuid
ownership.isOurs = isOurs
if debugStats.enabled and IsCurrentTarget(targetGuid) then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[SLOT UPDATED]|r aura=%d %s newCaster=%s isOurs=%s",
auraSlot, spellName, DebugGuid(casterGuid), tostring(isOurs)))
end
break
end
end
end
if debugStats.enabled and IsCurrentTarget(targetGuid) then
DEFAULT_CHAT_FRAME:AddMessage(string.format("%s |cff00ffff[AURA_CAST]|r %s target=%s caster=%s isOurs=%s dur=%.1fs",
GetDebugTimestamp(), spellName, DebugGuid(targetGuid), DebugGuid(casterGuid), tostring(isOurs), duration))
end
end
-- Notify nameplates
if pfUI.nameplates and pfUI.nameplates.OnAuraUpdate then
pfUI.nameplates:OnAuraUpdate(targetGuid)
end
-- Notify unitframes of debuff updates (UNIT_AURA doesn't fire on refreshes!)
-- Check player
if UnitGUID("player") then
local playerGuid = UnitGUID("player")
if playerGuid == targetGuid and pfPlayer then
pfPlayer.update_aura = true
end
end
-- Check target
if UnitGUID("target") then
local tarUnitGUID = UnitGUID("target")
if tarUnitGUID == targetGuid and pfTarget then
pfTarget.update_aura = true
end
end
-- Only track in ownDebuffs if it's OUR debuff
if not isOurs then return end
if targetGuid == myGuid then return end -- Skip self-buffs
if not targetGuid or targetGuid == "" or targetGuid == "0x0000000000000000" then return end
-- Get texture
local texture = libdebuff:GetSpellIcon(spellId)
-- Write ownDebuffs if:
-- (a) Entry already exists (refresh of active debuff), OR
-- (b) pendingCasts has an entry for this spell (SPELL_GO confirmed a hit is in-flight,
-- not a miss). This allows the downrank hook in SPELL_GO to fire correctly on first cast.
-- Without (b), ownDebuffs is empty until DEBUFF_ADDED (~300ms later), meaning the downrank
-- check in SPELL_GO has no data to work with on the first cast.
local entryExists = ownDebuffs[targetGuid] and ownDebuffs[targetGuid][spellName]
local pendingConfirm = pendingCasts[targetGuid] and pendingCasts[targetGuid][spellName]
if not entryExists and not pendingConfirm then return end
ownDebuffs[targetGuid] = ownDebuffs[targetGuid] or {}
local data = ownDebuffs[targetGuid][spellName]
-- For first-cast path (pendingConfirm but no existing entry): create a new entry
if not data then
if not pendingConfirm then return end -- race condition: cleared by DEBUFF_REMOVED
data = {}
ownDebuffs[targetGuid][spellName] = data
end
-- Downrank Protection: Check if existing debuff is still active and has higher rank
if data.startTime and data.duration and data.rank and rankNum > 0 then
local existingTimeleft = (data.startTime + data.duration) - GetTime()
if existingTimeleft > 0 then
-- Existing debuff is still active
if rankNum < data.rank then
-- Lower rank cannot overwrite higher rank - block the update
if debugStats.enabled then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff0000[DOWNRANK BLOCKED]|r %s: Rank %d cannot overwrite Rank %d (%.1fs left)",
spellName, rankNum, data.rank, existingTimeleft))
end
return
end
end
end
data.startTime = startTime
data.duration = duration
data.texture = texture
data.rank = rankNum
data.spellId = spellId
-- Handle variant pairs for ownDebuffs
if debuffOverwritePairs[spellName] then
local otherVariant = debuffOverwritePairs[spellName]
if ownDebuffs[targetGuid][otherVariant] then
ownDebuffs[targetGuid][otherVariant] = nil
end
end
-- Store for Cleveroids API
objectsByGuid[targetGuid] = objectsByGuid[targetGuid] or {}
objectsByGuid[targetGuid][spellId] = {
start = startTime,
duration = duration,
caster = "player",
stacks = 1
}
if event == "AURA_CAST_ON_SELF" and pfUI.libdebuff_aura_cast_on_self_hooks then
for _, fn in pairs(pfUI.libdebuff_aura_cast_on_self_hooks) do
fn(spellId, casterGuid, targetGuid)
end
elseif event == "AURA_CAST_ON_OTHER" and pfUI.libdebuff_aura_cast_on_other_hooks then
for _, fn in pairs(pfUI.libdebuff_aura_cast_on_other_hooks) do
fn(spellId, casterGuid, targetGuid)
end
end
elseif event == "DEBUFF_ADDED_OTHER" then
local guid = arg1
local displaySlot = arg2 -- Display slot (1-16), compacted
local spellId = arg3
local stacks = arg4
local auraSlot_0based = arg6 -- Nampower 2.29+: raw slot 0-based (32-47)
-- Convert 0-based (Nampower event) to 1-based (Lua GetUnitField array)
local auraSlot = auraSlot_0based and (auraSlot_0based + 1) or nil
-- Invalidate slot map cache for this GUID
local spellName = GetSpellRecField and GetSpellRecField(spellId, "name")
if not spellName then return end
if debugStats.enabled then
debugStats.debuff_added = debugStats.debuff_added + 1
end
-- If unit is dead, cleanup and skip
if UnitIsDead and UnitIsDead(guid) then
CleanupUnit(guid)
return
end
-- Get auraSlot from event parameter (Nampower 2.29+)
-- Fallback to GetUnitField lookup if not available
if not auraSlot then
local slotMap = GetDebuffSlotMap(guid)
if slotMap and slotMap[displaySlot] then
auraSlot = slotMap[displaySlot].auraSlot
end
end
-- Fallback: Calculate aura slot if GetUnitField didn't work
-- (This assumes no gaps, which isn't always true, but better than nothing)
if not auraSlot then
auraSlot = 32 + displaySlot
end
-- Get caster from pendingCasts (SPELL_GO correlation)
local casterGuid = nil
if pendingCasts[guid] and pendingCasts[guid][spellName] then
local pending = pendingCasts[guid][spellName]
if GetTime() - pending.time < 0.5 then
casterGuid = pending.casterGuid
pendingCasts[guid][spellName] = nil
end
end
-- Fallback: Check allAuraCasts for most recent caster
if not casterGuid and allAuraCasts[guid] and allAuraCasts[guid][spellName] then
local mostRecent = nil
local mostRecentTime = 0
for casterId, data in pairs(allAuraCasts[guid][spellName]) do
if data.startTime > mostRecentTime then
mostRecentTime = data.startTime
mostRecent = casterId
end
end
if mostRecent then
casterGuid = mostRecent
end
end
local myGuid = GetPlayerGUID()
local isOurs = (myGuid and casterGuid == myGuid)
-- Fallback: Check ownDebuffs timing
if not isOurs and not casterGuid then
if ownDebuffs[guid] and ownDebuffs[guid][spellName] then
local age = GetTime() - ownDebuffs[guid][spellName].startTime
if age < 0.5 then
isOurs = true
casterGuid = myGuid
end
end
end
-- Store slot ownership (KEY: auraSlot is STABLE, no shifting needed!)
slotOwnership[guid] = slotOwnership[guid] or {}
slotOwnership[guid][auraSlot] = {
casterGuid = casterGuid,
spellName = spellName,
spellId = spellId,
isOurs = isOurs
}
-- Store displaySlot → auraSlot mapping for DEBUFF_REMOVED
displayToAura[guid] = displayToAura[guid] or {}
displayToAura[guid][displaySlot] = auraSlot
if debugStats.enabled and IsCurrentTarget(guid) then
DEFAULT_CHAT_FRAME:AddMessage(string.format("%s |cff00ff00[DEBUFF_ADDED]|r display=%d aura=%d %s caster=%s isOurs=%s",
GetDebugTimestamp(), displaySlot, auraSlot, spellName, DebugGuid(casterGuid), tostring(isOurs)))
end
-- CRITICAL FIX: Update ownDebuffs here too for refresh timing!
-- This prevents the gap between DEBUFF_REMOVED and AURA_CAST where buffwatch shows nothing
if isOurs and casterGuid then
local myGuid = GetPlayerGUID()
if myGuid and casterGuid == myGuid then
-- Check if we have timer data from allAuraCasts
if allAuraCasts[guid] and allAuraCasts[guid][spellName] and allAuraCasts[guid][spellName][casterGuid] then
local auraData = allAuraCasts[guid][spellName][casterGuid]
local texture = libdebuff:GetSpellIcon(spellId)
ownDebuffs[guid] = ownDebuffs[guid] or {}
ownDebuffs[guid][spellName] = {
startTime = auraData.startTime,
duration = auraData.duration,
texture = texture,
rank = auraData.rank or 0,
spellId = spellId
}
if debugStats.enabled and IsCurrentTarget(guid) then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff00ff[OWNDEBUFF SYNC]|r %s from DEBUFF_ADDED", spellName))
end
end
end
end
-- Cleanup expired timers
CleanupExpiredTimers(guid)
-- Notify nameplates
if pfUI.nameplates and pfUI.nameplates.OnAuraUpdate then
pfUI.nameplates:OnAuraUpdate(guid)
end
if pfUI.libdebuff_debuff_added_other_hooks then
for _, fn in pairs(pfUI.libdebuff_debuff_added_other_hooks) do
fn(arg1, arg2, arg3, arg4)
end
end
elseif event == "DEBUFF_REMOVED_OTHER" then
local guid = arg1
local displaySlot = arg2 -- Display slot (1-16), compacted
local spellId = arg3
local auraSlot_0based = arg6 -- Nampower 2.29+: raw slot 0-based (32-47)
-- Convert 0-based (Nampower event) to 1-based (Lua GetUnitField array)
local auraSlot = auraSlot_0based and (auraSlot_0based + 1) or nil
-- Invalidate slot map cache for this GUID
local spellName = (GetSpellRecField and GetSpellRecField(spellId, "name")) or "?"
if debugStats.enabled then
debugStats.debuff_removed = debugStats.debuff_removed + 1
if IsCurrentTarget(guid) then
DEFAULT_CHAT_FRAME:AddMessage(string.format("%s |cffff9900[DEBUFF_REMOVED]|r display=%d aura=%d (0based=%d) %s",
GetDebugTimestamp(), displaySlot, auraSlot or -1, auraSlot_0based or -1, spellName))
end
end
-- If unit is dead, cleanup all
if UnitIsDead and UnitIsDead(guid) then
CleanupUnit(guid)
return
end
-- Get auraSlot from event parameter (Nampower 2.29+)
-- Fallback to displayToAura mapping if not available
local foundAuraSlot = auraSlot
if not foundAuraSlot and displayToAura[guid] and displayToAura[guid][displaySlot] then
foundAuraSlot = displayToAura[guid][displaySlot]
end
local wasOurs = false
local removedCasterGuid = nil
if foundAuraSlot then
-- Get ownership info for this specific slot
if slotOwnership[guid] and slotOwnership[guid][foundAuraSlot] then
local ownership = slotOwnership[guid][foundAuraSlot]
wasOurs = ownership.isOurs
removedCasterGuid = ownership.casterGuid
end
-- Clear both mappings (with nil-checks)
if slotOwnership[guid] then
slotOwnership[guid][foundAuraSlot] = nil
end
if displayToAura[guid] then
displayToAura[guid][displaySlot] = nil
end
if debugStats.enabled and IsCurrentTarget(guid) then
DEFAULT_CHAT_FRAME:AddMessage(string.format("%s |cffff9900[SLOT CLEARED]|r aura=%d [arg6] %s wasOurs=%s caster=%s",
GetDebugTimestamp(), foundAuraSlot, spellName, tostring(wasOurs), DebugGuid(removedCasterGuid)))
end
end
-- Remove from ownDebuffs if it was ours
if wasOurs and ownDebuffs[guid] and ownDebuffs[guid][spellName] then
local age = GetTime() - ownDebuffs[guid][spellName].startTime
-- Only delete if not recently renewed
if age > 1 then
ownDebuffs[guid][spellName] = nil
end
end
-- Remove from allAuraCasts
if removedCasterGuid and allAuraCasts[guid] and allAuraCasts[guid][spellName] then
if allAuraCasts[guid][spellName][removedCasterGuid] then
local auraData = allAuraCasts[guid][spellName][removedCasterGuid]
local age = GetTime() - auraData.startTime
-- Only delete if not recently refreshed
if age > 1 then
allAuraCasts[guid][spellName][removedCasterGuid] = nil
end
end
end
-- Cleanup expired timers
CleanupExpiredTimers(guid)
-- Notify nameplates
if pfUI.nameplates and pfUI.nameplates.OnAuraUpdate then
pfUI.nameplates:OnAuraUpdate(guid)
end
if pfUI.libdebuff_debuff_removed_other_hooks then
for _, fn in pairs(pfUI.libdebuff_debuff_removed_other_hooks) do
fn(arg1, arg2, arg3, arg4)
end
end
elseif event == "PLAYER_TARGET_CHANGED" then
if not UnitGUID then return end
local targetGuid = UnitGUID("target")
if targetGuid and targetGuid ~= "" then
-- Invalidate slot map cache on retarget
-- Prevents stale slot mappings after untarget/retarget cycles
-- Cleanup expired timers for new target
CleanupExpiredTimers(targetGuid)
end
if pfUI.libdebuff_player_target_changed_hooks then
for _, fn in pairs(pfUI.libdebuff_player_target_changed_hooks) do
fn()
end
end
end
-- Periodic cleanup
CleanupOutOfRangeUnits()
end)
-- Cleveroids API
if CleveRoids then
CleveRoids.libdebuff = libdebuff
libdebuff.objects = objectsByGuid
end
end
-- add libdebuff to pfUI API
pfUI.api.libdebuff = libdebuff
-- Expose debugStats for external access
libdebuff.debugStats = debugStats
-- ============================================================================
-- DEBUG COMMANDS
-- ============================================================================
_G.SLASH_LIBDEBUGSTATS1 = "/libdebugstats"
_G.SlashCmdList["LIBDEBUGSTATS"] = function(msg)
msg = string.lower(msg or "")
if msg == "start" then
debugStats.enabled = true
debugStats.trackAllUnits = false
debugStats.aura_cast = 0
debugStats.debuff_added = 0
debugStats.debuff_removed = 0
debugStats.getunitfield_calls = 0
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[libdebuff]|r Debug tracking STARTED")
elseif msg == "stop" then
debugStats.enabled = false
DEFAULT_CHAT_FRAME:AddMessage("|cffff9900[libdebuff]|r Debug tracking STOPPED")
elseif msg == "stats" then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff=== LIBDEBUFF STATS (GetUnitField Edition) ===|r")
DEFAULT_CHAT_FRAME:AddMessage(string.format("AURA_CAST events: %d", debugStats.aura_cast))
DEFAULT_CHAT_FRAME:AddMessage(string.format("DEBUFF_ADDED events: %d", debugStats.debuff_added))
DEFAULT_CHAT_FRAME:AddMessage(string.format("DEBUFF_REMOVED events: %d", debugStats.debuff_removed))
DEFAULT_CHAT_FRAME:AddMessage(string.format("GetUnitField calls: %d", debugStats.getunitfield_calls))
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00No manual slot shifting needed!|r")
elseif msg == "target" then
if not UnitGUID("target") then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[libdebuff]|r No target!")
return
end
local guid = UnitGUID("target")
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff=== TARGET DEBUFF STATE ===|r")
DEFAULT_CHAT_FRAME:AddMessage(string.format("GUID: %s", tostring(guid)))
-- Show GetUnitField slot map
local slotMap = GetDebuffSlotMap(guid)
if slotMap then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00GetUnitField Slots:|r")
for displaySlot, data in pairs(slotMap) do
local casterGuid, isOurs = GetSlotCaster(guid, data.auraSlot, data.spellName)
DEFAULT_CHAT_FRAME:AddMessage(string.format(" Display %d (aura %d): %s [caster=%s, ours=%s]",
displaySlot, data.auraSlot, data.spellName, DebugGuid(casterGuid), tostring(isOurs)))
end
else
DEFAULT_CHAT_FRAME:AddMessage("|cffff9900No debuffs via GetUnitField|r")
end
-- Show ownDebuffs
if ownDebuffs[guid] then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00ownDebuffs:|r")
for spell, data in pairs(ownDebuffs[guid]) do
local timeleft = (data.startTime + data.duration) - GetTime()
DEFAULT_CHAT_FRAME:AddMessage(string.format(" %s: dur=%.1f left=%.1f", spell, data.duration, timeleft))
end
end
else
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[libdebuff] GetUnitField Edition - Commands:|r")
DEFAULT_CHAT_FRAME:AddMessage(" /libdebugstats start - Start debug tracking")
DEFAULT_CHAT_FRAME:AddMessage(" /libdebugstats stop - Stop debug tracking")
DEFAULT_CHAT_FRAME:AddMessage(" /libdebugstats stats - Show statistics")
DEFAULT_CHAT_FRAME:AddMessage(" /libdebugstats target - Show target debuff state")
end
end
_G.SLASH_MEMCHECK1 = "/memcheck"
_G.SlashCmdList["MEMCHECK"] = function()
local function countTable(t)
local count = 0
if not t then return 0 end
for _ in pairs(t) do count = count + 1 end
return count
end
local function countNestedEntries(t)
local total = 0
if not t then return 0 end
for _, nested in pairs(t) do
if type(nested) == "table" then
total = total + countTable(nested)
end
end
return total
end
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff========== LIBDEBUFF MEMORY (GetUnitField Edition) ==========|r")
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00Primary Tables:|r"))
DEFAULT_CHAT_FRAME:AddMessage(string.format(" ownDebuffs: %d GUIDs, %d debuffs", countTable(ownDebuffs), countNestedEntries(ownDebuffs)))
DEFAULT_CHAT_FRAME:AddMessage(string.format(" slotOwnership: %d GUIDs, %d slots", countTable(slotOwnership), countNestedEntries(slotOwnership)))
DEFAULT_CHAT_FRAME:AddMessage(string.format(" allAuraCasts: %d GUIDs", countTable(allAuraCasts)))
DEFAULT_CHAT_FRAME:AddMessage(string.format(" pendingCasts: %d GUIDs", countTable(pendingCasts)))
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00No ownSlots/allSlots (eliminated by GetUnitField approach!)|r")
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff============================================================|r")
end
DEFAULT_CHAT_FRAME:AddMessage("|cff33ff99[libdebuff]|r GetUnitField Edition loaded!")