mirror of
https://github.com/brues-code/pfUI.git
synced 2026-09-22 07:36:56 +00:00
deeec89955
Replace per-locale name tables and per-rank ID lists with single
canonical-rank lookups through C_Spell. Spell.dbc bits hoisted to
named constants at module top.
libpredict:
- Four 7-locale tables (PRAYER_OF_HEALING / REJUVENATION / RENEW /
REGROWTH) collapsed to one C_Spell.GetSpellName(rank1id) call each.
- 25-entry SPELL_IDS (all ranks of Rejuv + Renew) for SPELL_GO_SELF
HoT detection replaced by name comparison against REJUVENATION /
RENEW. No per-rank ID maintenance.
libdebuff:
- GetSpellRecField(id, "name") → C_Spell.GetSpellName(id) at all call
sites; the presence-guard pattern is gone (ClassicAPI is a hard
dep, per memory).
- GetSpellRecField(id, "rank") → C_Spell.GetSpellSubtext(id).
swingtimer:
- Hoist FLAG_AUTOATTACK / ATTR_KEEP_SWINGS / ATTR_ON_NEXT_SWING to
module-top constants via tonumber("0xNN", 16) so the SPELL_GO_SELF
hot path stops re-parsing them on every call. Lua 5.0 has no hex
number literals; strtoul-backed tonumber handles the "0x" prefix.
1815 lines
67 KiB
Lua
1815 lines
67 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.
|
|
--
|
|
-- The public per-aura readers (UnitDebuff, UnitOwnDebuff) were retired in favor
|
|
-- of ClassicAPI's C_UnitAuras (which now provides sourceUnit/sourceGUID and
|
|
-- non-player expirationTime). What remains in libdebuff is the cast-event
|
|
-- bookkeeping consumed by GetBestAuraCast / GetEnhancedDebuffs and the
|
|
-- libdebuff_*_hooks broadcast surface (subscribers in actionbar / swingtimer
|
|
-- / libtotem react to SPELL_GO and SPELL_FAILED).
|
|
|
|
-- 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
|
|
|
|
-- 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 = C_Spell.GetSpellName(spellId)
|
|
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
|
|
|
|
-- ============================================================================
|
|
-- 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
|
|
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
|
|
-- via C_Spell.ChannelInfo. DEBUFF_REMOVED fires later (0.5-1s server lag),
|
|
-- causing phantom debuff display without this pre-clear.
|
|
local spellName = select(1, C_Spell.ChannelInfo())
|
|
if spellName then
|
|
local targetGuid = UnitGUID and UnitGUID("target")
|
|
if targetGuid and ownDebuffs[targetGuid] and ownDebuffs[targetGuid][spellName] then
|
|
local data = ownDebuffs[targetGuid][spellName]
|
|
local remaining = (data.startTime + data.duration) - GetTime()
|
|
if remaining > 0 then
|
|
ownDebuffs[targetGuid][spellName] = nil
|
|
end
|
|
end
|
|
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 spellId = arg2
|
|
local casterGuid = arg3
|
|
-- arg6=castTime (ms), arg7=channel duration (ms). Prefer arg6 when set —
|
|
-- some spells (e.g. post-rework Volley) flag as channel via arg8 but ship a
|
|
-- real cast time in arg6, so we only fall back to arg7 for true channels
|
|
-- like Blizzard where arg6 is 0/nil.
|
|
local castTime = (arg6 and arg6 > 0) and arg6 or arg7
|
|
if not casterGuid or not spellId then return end
|
|
|
|
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
|
|
|
|
-- 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 = C_Spell.GetSpellName(spellId)
|
|
if not spellName then return end
|
|
local spellRankString = C_Spell.GetSpellSubtext(spellId)
|
|
|
|
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
|
|
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 = C_Spell.GetSpellName(spellId)
|
|
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 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 = C_Spell.GetSpellName(spellId)
|
|
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)
|
|
|
|
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 = C_Spell.GetSpellName(spellId) 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)
|
|
|
|
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!") |