diff --git a/Conditionals.lua b/Conditionals.lua index f955a6b..c42dc18 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -903,8 +903,37 @@ CleveRoids.OverflowBuffs = {} -- Tracks buff/debuff durations from ANY caster, not just player -- Structure: [targetGuid][spellName][casterGuid] = { start, duration, spellId } -- Aligned with pfUI's allAuraCasts multi-caster architecture +-- When pfUI 7.6+ is active, this table is unused — GetAuraTrackingData() reads +-- from pfUI.libdebuff_all_auras instead (which has full downrank protection). CleveRoids.AllCasterAuraTracking = {} +-- Unified accessor: returns per-spell caster table for a target GUID. +-- When pfUI 7.6+ is active, reads from pfUI.libdebuff_all_auras (field: .startTime) +-- and translates to our field names (.start). Falls back to AllCasterAuraTracking. +-- Returns: targetData table [spellName][casterGuid] = {...}, or nil +-- usePfUI: true if the returned data uses pfUI field names (.startTime instead of .start) +function CleveRoids.GetAuraTrackingData(targetGuid) + if not targetGuid then return nil, false end + + -- pfUI path: read directly from pfUI's table (has downrank protection built-in) + if CleveRoids.hasPfUI76 and pfUI and pfUI.libdebuff_all_auras then + local data = pfUI.libdebuff_all_auras[targetGuid] + if data then return data, true end + -- Fall through: our table may have test entries even when pfUI is active + end + + -- Standalone path (or pfUI had no data for this GUID) + local data = CleveRoids.AllCasterAuraTracking[targetGuid] + if data then return data, false end + return nil, false +end + +-- Read start time from an aura entry (handles pfUI .startTime vs our .start) +local function AuraStart(auraData, isPfUI) + if isPfUI then return auraData.startTime end + return auraData.start +end + -- Helper: check if a spell is personal using lib:IsPersonalDebuff + name fallback local function IsPersonalAura(spellId, spellName) local lib = CleveRoids.libdebuff @@ -922,9 +951,10 @@ end -- Helper to get time remaining from all-caster tracking (by spellId) -- Returns player's entry for personal debuffs, any caster for shared auras. +-- Reads from pfUI.libdebuff_all_auras when pfUI 7.6+ is active. function CleveRoids.GetAllCasterAuraTimeRemaining(targetGuid, spellId) if not targetGuid or not spellId then return nil end - local targetData = CleveRoids.AllCasterAuraTracking[targetGuid] + local targetData, isPfUI = CleveRoids.GetAuraTrackingData(targetGuid) if not targetData then return nil end local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") @@ -939,8 +969,9 @@ function CleveRoids.GetAllCasterAuraTimeRemaining(targetGuid, spellId) -- Always check player's own entry first if playerGuid and casters[playerGuid] then local auraData = casters[playerGuid] - if auraData.start and auraData.duration then - local remaining = auraData.duration + auraData.start - now + local startTime = AuraStart(auraData, isPfUI) + if startTime and auraData.duration then + local remaining = auraData.duration + startTime - now if remaining > 0 then return remaining end end end @@ -950,8 +981,9 @@ function CleveRoids.GetAllCasterAuraTimeRemaining(targetGuid, spellId) -- Shared aura: return any active caster's entry for _, auraData in pairs(casters) do - if auraData.start and auraData.duration then - local remaining = auraData.duration + auraData.start - now + local startTime = AuraStart(auraData, isPfUI) + if startTime and auraData.duration then + local remaining = auraData.duration + startTime - now if remaining > 0 then return remaining end end end @@ -960,9 +992,10 @@ end -- Helper to find aura by name (or spell ID string) for a target -- Returns player's entry for personal debuffs, any caster for shared auras. +-- Reads from pfUI.libdebuff_all_auras when pfUI 7.6+ is active. function CleveRoids.FindAllCasterAuraByName(targetGuid, searchName) if not targetGuid or not searchName then return nil, nil end - local targetData = CleveRoids.AllCasterAuraTracking[targetGuid] + local targetData, isPfUI = CleveRoids.GetAuraTrackingData(targetGuid) if not targetData then return nil, nil end -- Resolve spell ID to name for direct lookup @@ -997,8 +1030,9 @@ function CleveRoids.FindAllCasterAuraByName(targetGuid, searchName) -- Always check player's own entry first if playerGuid and casters[playerGuid] then local auraData = casters[playerGuid] - if auraData.start and auraData.duration then - local remaining = auraData.duration + auraData.start - now + local startTime = AuraStart(auraData, isPfUI) + if startTime and auraData.duration then + local remaining = auraData.duration + startTime - now if remaining > 0 then return remaining, playerGuid end end end @@ -1015,8 +1049,9 @@ function CleveRoids.FindAllCasterAuraByName(targetGuid, searchName) -- Shared aura: return any active caster's entry for cGuid, auraData in pairs(casters) do - if auraData.start and auraData.duration then - local remaining = auraData.duration + auraData.start - now + local startTime = AuraStart(auraData, isPfUI) + if startTime and auraData.duration then + local remaining = auraData.duration + startTime - now if remaining > 0 then return remaining, cGuid end end end @@ -1272,9 +1307,11 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu local now = GetTime() -- Store aura duration for all-caster tracking (even without auraCapStatus) + -- When pfUI enhanced is active, pfUI writes to pfUI.libdebuff_all_auras with + -- full downrank protection — we read from that table via GetAuraTrackingData(). if spellId and durationMs and durationMs > 0 then local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") - if spellName then + if spellName and not CleveRoids.hasPfUI76 then CleveRoids._allCasterAuraDirty = true if not CleveRoids.AllCasterAuraTracking[targetGuid] then CleveRoids.AllCasterAuraTracking[targetGuid] = {} @@ -1283,32 +1320,51 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu CleveRoids.AllCasterAuraTracking[targetGuid][spellName] = {} end local casterKey = casterGuid or "unknown" - -- PERFORMANCE: Reuse existing entry table when possible + -- Downrank protection: don't let a lower rank overwrite a higher rank with time remaining local existing = CleveRoids.AllCasterAuraTracking[targetGuid][spellName][casterKey] - if existing then - existing.start = now - existing.duration = durationMs / 1000 - existing.spellId = spellId - else - CleveRoids.AllCasterAuraTracking[targetGuid][spellName][casterKey] = { - start = now, - duration = durationMs / 1000, - spellId = spellId, - } + if existing and existing.spellId and existing.spellId ~= spellId then + local lib = CleveRoids.libdebuff + if lib and lib.GetSpellRank then + local newRank = lib:GetSpellRank(spellId) + local existingRank = lib:GetSpellRank(existing.spellId) + if newRank > 0 and existingRank > 0 and newRank < existingRank then + local timeleft = (existing.start + existing.duration) - now + if timeleft > 0 then + CleveRoids.DebugChanged("AuraTrack_rankblock_" .. spellName .. "_" .. casterKey, + string.format("|cffff6600[AuraTrack]|r %s Rank %d blocked by Rank %d (%.1fs left) on %s", + spellName, newRank, existingRank, timeleft, + string.sub(tostring(targetGuid), 1, 16))) + spellName = nil -- skip pendingBuffCasts below too + end + end + end + end + + -- Write/update tracking entry (skipped if downrank blocked above) + if spellName then + if existing then + existing.start = now + existing.duration = durationMs / 1000 + existing.spellId = spellId + else + CleveRoids.AllCasterAuraTracking[targetGuid][spellName][casterKey] = { + start = now, + duration = durationMs / 1000, + spellId = spellId, + } + end end end -- Debug output when enabled - if CleveRoids.debug then - local debugName = spellName or GetSpellRecField(spellId, "name") or "Unknown" - DEFAULT_CHAT_FRAME:AddMessage(string.format( - "|cff00ffff[AuraTrack]|r %s (ID:%d) on %s by %s, dur=%.1fs", - debugName, spellId, string.sub(tostring(targetGuid), 1, 16), - string.sub(tostring(casterGuid), 1, 16), durationMs / 1000 - )) + if spellName then + CleveRoids.DebugChanged("AuraTrack_" .. tostring(spellId) .. "_" .. string.sub(tostring(targetGuid), 1, 16), + string.format("|cff00ffff[AuraTrack]|r %s (ID:%d) on %s by %s, dur=%.1fs", + spellName, spellId, string.sub(tostring(targetGuid), 1, 16), + string.sub(tostring(casterGuid), 1, 16), durationMs / 1000)) end - -- NEW: Store in pendingBuffCasts for BUFF_ADDED_OTHER to confirm as buff + -- Store in pendingBuffCasts for BUFF_ADDED_OTHER to confirm as buff -- (AURA_CAST_ON_OTHER fires for both buffs and debuffs; BUFF_ADDED_OTHER confirms buff) local lib = CleveRoids.libdebuff if lib and not lib.hasPfUIEnhanced then @@ -1470,7 +1526,7 @@ autoAttackFrame:SetScript("OnEvent", function() if spellId and spellId > 0 and durationMs and durationMs > 0 then local playerGUID = CleveRoids.GetGUID("player") local durSpellName = GetSpellRecField and GetSpellRecField(spellId, "name") - if playerGUID and durSpellName then + if playerGUID and durSpellName and not CleveRoids.hasPfUI76 then CleveRoids._allCasterAuraDirty = true if not CleveRoids.AllCasterAuraTracking[playerGUID] then CleveRoids.AllCasterAuraTracking[playerGUID] = {} @@ -1488,13 +1544,12 @@ autoAttackFrame:SetScript("OnEvent", function() duration = durationSec, spellId = spellId, } + end - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage(string.format( - "|cff88ff88[AuraDurUpdate]|r %s (slot:%d, ID:%d) dur=%.1fs", - durSpellName, auraSlot, spellId, durationSec - )) - end + if durSpellName then + CleveRoids.DebugChanged("AuraDurUpdate_" .. tostring(spellId), + string.format("|cff88ff88[AuraDurUpdate]|r %s (slot:%d, ID:%d) dur=%.1fs", + durSpellName, auraSlot, spellId, durationMs / 1000)) end end @@ -1805,21 +1860,13 @@ local function IsPendingDebuffCast(spellName, targetUnit) if CleveRoids.castStartTime and CleveRoids.castDuration then local remaining = CleveRoids.castDuration - (GetTime() - CleveRoids.castStartTime) if remaining > 0.1 then - -- Cast is in-flight, debuff is pending - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage(string.format( - "|cffff00ff[PendingDebuff]|r %s pending via CurrentSpell (%.1fs remaining)", - spellName, remaining)) - end + CleveRoids.DebugChanged("pending_" .. normalizedCheck, + string.format("|cffff00ff[PendingDebuff]|r %s pending via CurrentSpell", spellName)) return true end else - -- No timing info but spell type is "cast" - assume pending - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage(string.format( - "|cffff00ff[PendingDebuff]|r %s pending via CurrentSpell (no timing info)", - spellName)) - end + CleveRoids.DebugChanged("pending_" .. normalizedCheck, + string.format("|cffff00ff[PendingDebuff]|r %s pending via CurrentSpell", spellName)) return true end end @@ -1833,11 +1880,8 @@ local function IsPendingDebuffCast(spellName, targetUnit) local normalizedQueued = NormalizeSpellNameForComparison(queuedName) if normalizedQueued == normalizedCheck then -- Spell is queued, debuff is pending - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage(string.format( - "|cffff00ff[PendingDebuff]|r %s pending via queuedSpell", - spellName)) - end + CleveRoids.DebugChanged("PendingDebuff_" .. spellName, + string.format("|cffff00ff[PendingDebuff]|r %s pending via queuedSpell", spellName)) return true end end @@ -1857,16 +1901,13 @@ local function IsPendingDebuffCast(spellName, targetUnit) if normalizedPending == normalizedCheck then local casterGuid = type(pendingData) == "table" and pendingData.casterGuid or nil if casterGuid and casterGuid == playerGuid then - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage(string.format( - "|cffff00ff[PendingDebuff]|r %s pending via pfUI.libdebuff_pending (ours)", - spellName)) - end + CleveRoids.DebugChanged("PendingDebuff_" .. spellName, + string.format("|cffff00ff[PendingDebuff]|r %s pending via pfUI.libdebuff_pending (ours)", spellName)) return true - elseif CleveRoids.debug and casterGuid then - DEFAULT_CHAT_FRAME:AddMessage(string.format( - "|cffff00ff[PendingDebuff]|r %s in pfUI pending but caster=%s (not ours) - skipping", - spellName, string.sub(tostring(casterGuid), 1, 16))) + elseif casterGuid then + CleveRoids.DebugChanged("PendingDebuff_other_" .. spellName, + string.format("|cffff00ff[PendingDebuff]|r %s in pfUI pending but caster=%s (not ours) - skipping", + spellName, string.sub(tostring(casterGuid), 1, 16))) end end end @@ -1883,11 +1924,9 @@ local function IsPendingDebuffCast(spellName, targetUnit) if pendingName then local normalizedPending = NormalizeSpellNameForComparison(pendingName) if normalizedPending == normalizedCheck then - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage(string.format( - "|cffff00ff[PendingDebuff]|r %s pending via libdebuff pending array (ID:%d)", + CleveRoids.DebugChanged("PendingDebuff_" .. spellName, + string.format("|cffff00ff[PendingDebuff]|r %s pending via libdebuff pending array (ID:%d)", spellName, pending.spellID)) - end return true end end @@ -4303,24 +4342,15 @@ function CleveRoids.ValidateUnitDebuff(unit, args) stacks = rec.stacks or 0 foundSpellId = sid - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ff00[Tracking]|r %s (ID:%d): %.1fs left", - args.name, sid, timeRemaining) - ) - end + CleveRoids.DebugChanged("tracking_" .. sid .. "_" .. tostring(guid), + string.format("|cff00ff00[Tracking]|r %s (ID:%d) active on %s", + args.name, sid, tostring(guid))) break else -- Timer expired — clean up - if CleveRoids.debug then - local expiredTime = GetTime() - (rec.start + rec.duration) - if expiredTime < 2.0 then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff6600[Tracking]|r %s (ID:%d) expired %.1fs ago", - args.name, sid, expiredTime) - ) - end - end + CleveRoids.DebugChanged("tracking_" .. sid .. "_" .. tostring(guid), + string.format("|cffff6600[Tracking]|r %s (ID:%d) expired", + args.name, sid)) lib.objects[guid][sid] = nil end end @@ -4336,13 +4366,9 @@ function CleveRoids.ValidateUnitDebuff(unit, args) local searchNameLower = not searchID and args.name and _string_lower(args.name) or nil local gufResult = API.FindUnitAuraInfo(unit, searchID, searchNameLower) if gufResult == false then - -- Aura definitively not on target — timer is stale - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff6600[Tracking]|r %s (ID:%d) stale (not on target per GetUnitField)", - args.name, foundSpellId) - ) - end + CleveRoids.DebugChanged("tracking_" .. foundSpellId .. "_" .. tostring(guid), + string.format("|cffff6600[Tracking]|r %s (ID:%d) stale (not on target)", + args.name, foundSpellId)) lib.objects[guid][foundSpellId] = nil found = false remaining = nil @@ -4353,15 +4379,13 @@ function CleveRoids.ValidateUnitDebuff(unit, args) end if not found and CleveRoids.debugVerbose then - DEFAULT_CHAT_FRAME:AddMessage( + CleveRoids.DebugChanged("tracking_miss_" .. args.name .. "_" .. tostring(guid), string.format("|cffff0000[Tracking]|r %s not in tracking table (checked %d ranks)", - args.name, table.getn(matchingSpellIDs)) - ) + args.name, table.getn(matchingSpellIDs))) end elseif CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff0000[Tracking]|r Unknown spell: %s", args.name) - ) + CleveRoids.DebugChanged("tracking_unknown_" .. args.name, + string.format("|cffff0000[Tracking]|r Unknown spell: %s", args.name)) end -- STEP 2: NAME-BASED FALLBACK for custom/Turtle WoW spells not in ID cache diff --git a/Core.lua b/Core.lua index dc5c248..17a860d 100644 --- a/Core.lua +++ b/Core.lua @@ -547,12 +547,6 @@ end) function CleveRoids.QueueActionUpdate() if CleveRoidMacros.realtime == 0 then CleveRoids.isActionUpdateQueued = true - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff00ff[QueueActionUpdate]|r Queued, isActionUpdateQueued = %s", - tostring(CleveRoids.isActionUpdateQueued)) - ) - end end end @@ -1076,12 +1070,6 @@ function CleveRoids.TestForActiveAction(actions) if useCombatLogOnly then -- Only trust HasReactiveProc for these spells local hasProc = CleveRoids.HasReactiveProc and CleveRoids.HasReactiveProc(spellName) - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ff00[UPDATE USABLE]|r %s: hasProc=%s, previousUsable=%s, inRange=%s, oom=%s", - spellName, tostring(hasProc), tostring(previousUsable), tostring(actions.active.inRange), tostring(actions.active.oom)) - ) - end if hasProc then -- Proc is active, show as usable if in range and have enough rage/mana if actions.active.inRange ~= 0 and not actions.active.oom then @@ -1091,21 +1079,13 @@ function CleveRoids.TestForActiveAction(actions) else actions.active.usable = nil end - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ff00[UPDATE USABLE]|r %s: SET usable=%s (proc active)", - spellName, tostring(actions.active.usable)) - ) - end else -- No proc = not usable actions.active.usable = nil - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ff00[UPDATE USABLE]|r %s: SET usable=nil (no proc)", spellName) - ) - end end + CleveRoids.DebugChanged("usable_" .. spellName, + string.format("|cff00ff00[UPDATE USABLE]|r %s: hasProc=%s, usable=%s", + spellName, tostring(hasProc), tostring(actions.active.usable))) else -- For other reactive spells, use the original fallback logic -- Check combat log-based proc tracking first (stance-independent) @@ -4276,14 +4256,8 @@ function CleveRoids.OnUpdate(self) else -- Event-Driven Mode (Default): Only update if a relevant game event has queued it. if CR.isActionUpdateQueued then - if CR.debug then - DEFAULT_CHAT_FRAME:AddMessage("|cffff00ff[OnUpdate]|r Processing queued action update") - end CR.TestForAllActiveActions() - CR.isActionUpdateQueued = false -- Reset the flag after updating - if CR.debug then - DEFAULT_CHAT_FRAME:AddMessage("|cffff00ff[OnUpdate]|r Action update complete, flag reset") - end + CR.isActionUpdateQueued = false end end @@ -6517,6 +6491,8 @@ SlashCmdList["CLEVEROID"] = function(msg) CleveRoids.debug = not CleveRoids.debug CleveRoids.Print("debug " .. (CleveRoids.debug and "enabled" or "disabled")) end + -- Clear keyed debug state so messages re-fire when debug is re-enabled + CleveRoids._lastDebugState = {} return end @@ -7115,7 +7091,13 @@ SlashCmdList["CLEVEROID"] = function(msg) CleveRoids.Print("|cffffaa00Tracked Auras:|r") local trackingCount = 0 local now = GetTime() - for targetGuid, spellNames in pairs(CleveRoids.AllCasterAuraTracking or {}) do + -- Determine which backing table to iterate + local isPfUI = CleveRoids.hasPfUI76 and pfUI and pfUI.libdebuff_all_auras + local backingTable = isPfUI and pfUI.libdebuff_all_auras or CleveRoids.AllCasterAuraTracking or {} + if isPfUI then + CleveRoids.Print(" (reading from pfUI.libdebuff_all_auras)") + end + for targetGuid, spellNames in pairs(backingTable) do local unitName = nil -- Try to find unit name for this GUID (use pcall to handle invalid units like "focus") for _, testUnit in ipairs({"target", "mouseover", "party1", "party2", "party3", "party4"}) do @@ -7131,8 +7113,9 @@ SlashCmdList["CLEVEROID"] = function(msg) for spellName, casters in pairs(spellNames) do for casterGuid, auraData in pairs(casters) do - if auraData.start and auraData.duration then - local remaining = auraData.duration + auraData.start - now + local startTime = isPfUI and auraData.startTime or auraData.start + if startTime and auraData.duration then + local remaining = auraData.duration + startTime - now if remaining > 0 then local display = unitName or (string.sub(targetGuid, 1, 16) .. "...") CleveRoids.Print(string.format(" %s on %s: %.1fs left (caster: %s)", @@ -7153,7 +7136,7 @@ SlashCmdList["CLEVEROID"] = function(msg) CleveRoids.Print("|cffffaa00Target Buff Check:|r") local targetGuid = CleveRoids.GetGUID("target") CleveRoids.Print(" Target GUID: " .. tostring(targetGuid)) - local targetData = CleveRoids.AllCasterAuraTracking[targetGuid] + local targetData = CleveRoids.GetAuraTrackingData(targetGuid) if targetData then local count = 0 for _, casters in pairs(targetData) do diff --git a/Extensions/OverflowBuffFrame.lua b/Extensions/OverflowBuffFrame.lua index 6fa8d46..8727d7b 100644 --- a/Extensions/OverflowBuffFrame.lua +++ b/Extensions/OverflowBuffFrame.lua @@ -295,8 +295,8 @@ local function GetTargetOverflowBuffs() local targetGuid = CleveRoids.GetGUID("target") if not targetGuid then return results end - local trackingData = CleveRoids.AllCasterAuraTracking - if not trackingData or not trackingData[targetGuid] then return results end + local trackingData, isPfUI = CleveRoids.GetAuraTrackingData(targetGuid) + if not trackingData then return results end -- Only show overflow for targets that are buff-capped (all 32 buff slots occupied). -- GetUnitField "aura" returns spell IDs for all 48 slots including hidden auras, @@ -342,13 +342,14 @@ local function GetTargetOverflowBuffs() local now = GetTime() - for spellName, casters in pairs(trackingData[targetGuid]) do + for spellName, casters in pairs(trackingData) do -- Find best entry across all casters for this spell local bestEntry = nil local bestRemaining = 0 for casterGuid, auraData in pairs(casters) do - if auraData.spellId and auraData.start and auraData.duration then - local remaining = auraData.duration + auraData.start - now + local startTime = isPfUI and auraData.startTime or auraData.start + if startTime and auraData.duration then + local remaining = auraData.duration + startTime - now if remaining > bestRemaining then bestRemaining = remaining bestEntry = auraData @@ -357,7 +358,9 @@ local function GetTargetOverflowBuffs() end if bestEntry and bestRemaining > 0 then + -- Our table stores .spellId directly; pfUI doesn't, so resolve from name local spellId = bestEntry.spellId + or (_G.GetSpellIdForName and _G.GetSpellIdForName(spellName)) -- v3.0+: Skip hidden auras (not real overflow) if not (_G.IsAuraHidden and _G.IsAuraHidden(spellId) == 1) then local isVisible = bestEntry._testEntry == nil and visibleSpellIds[spellId] diff --git a/Init.lua b/Init.lua index f0ba1dd..070561d 100644 --- a/Init.lua +++ b/Init.lua @@ -158,6 +158,17 @@ CleveRoids._actionsListBuffer = {} -- PERFORMANCE: Static buffer for arg backup in SendEventForAction CleveRoids._originalArgsBuffer = {} +-- KEYED DEBUG: Only prints when the message for a given key changes from last print. +-- Usage: CleveRoids.DebugChanged("immunity_moonfire", formatted_msg) +-- Prevents spam when the same state is reported repeatedly (e.g., per-frame or per-eval). +CleveRoids._lastDebugState = {} +function CleveRoids.DebugChanged(key, msg) + if not CleveRoids.debug then return end + if CleveRoids._lastDebugState[key] == msg then return end + CleveRoids._lastDebugState[key] = msg + DEFAULT_CHAT_FRAME:AddMessage(msg) +end + -- Spell queue state (Nampower) CleveRoids.queuedSpell = nil CleveRoids.lastCastSpell = nil diff --git a/Utility.lua b/Utility.lua index 7ff861b..525ccdb 100644 --- a/Utility.lua +++ b/Utility.lua @@ -2107,12 +2107,9 @@ function lib:ShouldApplyDebuffRank(targetGUID, newSpellID) preservedTimeRemaining = timeRemaining } - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00aaff[Rank Preserve]|r Cast %s Rank %d, preserving Rank %d timer (%.1fs remaining)", - newBaseName, newRank, highestExistingRank, timeRemaining) - ) - end + CleveRoids.DebugChanged("rank_preserve_" .. newBaseName .. "_" .. tostring(targetGUID), + string.format("|cff00aaff[Rank Preserve]|r Cast %s Rank %d, preserving Rank %d timer (%.1fs remaining)", + newBaseName, newRank, highestExistingRank, timeRemaining)) -- Return special value indicating we should preserve the higher rank's timer return { @@ -2254,6 +2251,44 @@ function lib:AddEffect(guid, unitName, spellID, duration, stacks, caster) lib.objects[guid][spellID] = rec + -- OWN DEBUFFS TRACKING: Mirror pfUI's ownDebuffs structure for player casts + -- When pfUI enhanced is active, pfUI handles ownDebuffs writes from its own + -- SPELL_GO/AURA_CAST/DEBUFF_ADDED handlers. Without pfUI, we populate it here + -- so lib.ownDebuffs has consistent data for downrank checks and debuff queries. + if caster == "player" and not lib.hasPfUIEnhanced then + local spellName = GetSpellRecField(spellID, "name") + if spellName then + local spellRankStr = GetSpellRecField(spellID, "rank") + local rankNum = spellRankStr and tonumber((string.gsub(spellRankStr, "Rank ", ""))) or 0 + + lib.ownDebuffs[guid] = lib.ownDebuffs[guid] or {} + + -- Downrank protection: don't overwrite a higher rank that's still active + local existing = lib.ownDebuffs[guid][spellName] + local blocked = false + if existing and existing.rank and existing.spellId and existing.spellId ~= spellID then + if rankNum > 0 and existing.rank > rankNum then + local existingTimeleft = (existing.startTime + existing.duration) - GetTime() + if existingTimeleft > 0 then + blocked = true + end + end + end + + if not blocked then + local texture = lib:GetCachedIcon(spellID) + lib.ownDebuffs[guid][spellName] = { + startTime = GetTime(), + duration = duration, + texture = texture, + rank = rankNum, + spellId = spellID, + stacks = stacks or 1, + } + end + end + end + -- PFUI INTEGRATION: Inject all tracked debuffs into pfUI's libdebuff (pre-7.6 only) -- pfUI 7.6+ handles all duration tracking internally via GetUnitField if pfUI and pfUI.api and pfUI.api.libdebuff and unitName and not CleveRoids.hasPfUI76 then @@ -2286,14 +2321,11 @@ function lib:AddEffect(guid, unitName, spellID, duration, stacks, caster) end end - -- DEBUG: Show what we stored if CleveRoids.debug then local spellName = GetSpellRecField(spellID, "name") or "Unknown" - local casterStr = caster or "nil" - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ffff[DEBUG AddEffect]|r %s (ID:%d) stored duration:%ds on %s, caster:%s, GUID:%s", - spellName, spellID, duration, unitName or "Unknown", casterStr, tostring(guid)) - ) + CleveRoids.DebugChanged("addeffect_" .. spellID .. "_" .. tostring(guid), + string.format("|cff00ffff[AddEffect]|r %s (ID:%d) %ds on %s, caster:%s", + spellName, spellID, duration, unitName or "Unknown", caster or "nil")) end end @@ -3194,37 +3226,23 @@ delayedTrackingFrame:SetScript("OnUpdate", function() local hasShared = lib.pendingSharedDebuffs and _next(lib.pendingSharedDebuffs) local hasOverrides = lib.rankRefreshOverrides and _next(lib.rankRefreshOverrides) - -- DEBUG: Track pending queue state - check EVERY time to diagnose issue + -- DEBUG: Track pending queue state (keyed — only prints on change) if CleveRoids.debug then local sharedCount = lib.pendingSharedDebuffs and _getn(lib.pendingSharedDebuffs) or 0 local personalCount = lib.pendingPersonalDebuffs and _getn(lib.pendingPersonalDebuffs) or 0 - -- Log if any shared pending exists, regardless of hasShared check if sharedCount > 0 then - -- Only log occasionally to avoid spam (every ~0.5s) - if not lib._lastPendingQueueLog or (currentTime - lib._lastPendingQueueLog) > 0.5 then - lib._lastPendingQueueLog = currentTime - local firstItem = lib.pendingSharedDebuffs[1] - DEFAULT_CHAT_FRAME:AddMessage( - _string_format("|cffaaaaaa[Pending Queue]|r shared:%d (hasShared:%s, [1]:%s) personal:%d", - sharedCount, hasShared and "TRUE" or "FALSE", firstItem and "exists" or "NIL", personalCount) - ) - end + CleveRoids.DebugChanged("pending_shared", + _string_format("|cffaaaaaa[Pending Queue]|r shared:%d personal:%d", + sharedCount, personalCount)) + elseif CleveRoids._lastDebugState["pending_shared"] then + CleveRoids._lastDebugState["pending_shared"] = nil end - -- Log personal debuff queue state when items exist but hasPersonal might be false if personalCount > 0 then - if not lib._lastPersonalQueueLog or (currentTime - lib._lastPersonalQueueLog) > 0.5 then - lib._lastPersonalQueueLog = currentTime - local firstItem = lib.pendingPersonalDebuffs[1] - local firstItemInfo = "NIL" - if firstItem then - firstItemInfo = _string_format("spellID:%s,targetGUID:%s", - _tostring(firstItem.spellID or "nil"), firstItem.targetGUID and "exists" or "nil") - end - DEFAULT_CHAT_FRAME:AddMessage( - _string_format("|cffaaaaaa[Personal Queue]|r count:%d, hasPersonal:%s, [1]:%s", - personalCount, hasPersonal and "TRUE" or "FALSE", firstItemInfo) - ) - end + CleveRoids.DebugChanged("pending_personal", + _string_format("|cffaaaaaa[Personal Queue]|r count:%d", + personalCount)) + elseif CleveRoids._lastDebugState["pending_personal"] then + CleveRoids._lastDebugState["pending_personal"] = nil end end @@ -3324,14 +3342,9 @@ delayedTrackingFrame:SetScript("OnUpdate", function() pendingCount = pendingCount + 1 end - -- Debug: Show pending count every few seconds (avoid spam) if debug and pendingCount > 0 then - if not lib._lastPendingDebugTime or (currentTime - lib._lastPendingDebugTime) > 2.0 then - lib._lastPendingDebugTime = currentTime - DEFAULT_CHAT_FRAME:AddMessage( - _string_format("|cffaaaaaa[Pending Debug]|r %d personal debuffs waiting", pendingCount) - ) - end + CleveRoids.DebugChanged("pending_debug", + _string_format("|cffaaaaaa[Pending Debug]|r %d personal debuffs waiting", pendingCount)) end -- Use pairs() to iterate and rebuild without holes @@ -3492,33 +3505,80 @@ delayedTrackingFrame:SetScript("OnUpdate", function() end CleveRoids_ImmunityData["bleed"][pending.targetName] = true - if debug then - DEFAULT_CHAT_FRAME:AddMessage( - _string_format("|cffff6600[Bleed Immunity]|r %s is immune to bleed (%s) - only %d debuffs on target", - pending.targetName, spellNameForImmunity, totalDebuffs) - ) - end + CleveRoids.DebugChanged("bleed_immune_" .. _tostring(pending.targetName), + _string_format("|cffff6600[Bleed Immunity]|r %s is immune to bleed (%s) - only %d debuffs on target", + pending.targetName, spellNameForImmunity, totalDebuffs)) end else - -- Many debuffs = likely pushed off at debuff cap - if debug then - local spellNameDebug = _GetSpellRecField(pending.spellID, "name") or "Bleed" - DEFAULT_CHAT_FRAME:AddMessage( - _string_format("|cffff6600[Debuff Cap]|r %s not found on %s - likely pushed off (%d debuffs on target)", - spellNameDebug, pending.targetName or "Unknown", totalDebuffs) - ) - end + CleveRoids.DebugChanged("bleed_cap_" .. _tostring(pending.targetName) .. "_" .. pending.spellID, + _string_format("|cffff6600[Debuff Cap]|r %s not found on %s - likely pushed off (%d debuffs on target)", + _GetSpellRecField(pending.spellID, "name") or "Bleed", pending.targetName or "Unknown", totalDebuffs)) end end end end end - -- NON-BLEED VERIFICATION: Scan debuffs for non-bleed personal debuffs without SPELL_GO data - -- This catches "soft immunity" where SPELL_GO reports hit but debuff doesn't actually apply - -- (e.g., some Turtle WoW boss mechanics, or server-side immunity without SPELL_MISS event) - if not isBleedSpell and debuffVerified and not pending.spellGoHit and not pending.spellGoMissed then - local nonBleedVerifyUnit = pending.targetGUID and ResolveGUIDUnit(pending.targetGUID) or nil + -- NON-BLEED VERIFICATION: Verify non-bleed personal debuffs actually applied + -- Always verify even when spellGoHit=true: SPELL_GO "hit" means initial damage landed, + -- not that the DoT was applied (e.g., lower rank Moonfire deals initial damage but DoT + -- is blocked by "a more powerful spell is already active") + if not isBleedSpell and debuffVerified and not pending.spellGoMissed then + -- Fast path: use pfUI's downrank-protected ownDebuffs table when available + -- pfUI blocks lower-rank overwrites, so if ownDebuffs still has a higher rank + -- with time remaining, the lower rank DoT didn't apply + local downrankBlocked = false + if lib.ownDebuffs and pending.targetGUID then + local spellName = _GetSpellRecField(pending.spellID, "name") + if spellName then + local existing = lib.ownDebuffs[pending.targetGUID] and lib.ownDebuffs[pending.targetGUID][spellName] + if existing and existing.rank and existing.spellId and existing.spellId ~= pending.spellID then + local pendingRankStr = _GetSpellRecField(pending.spellID, "rank") + local pendingRank = pendingRankStr and tonumber((string.gsub(pendingRankStr, "Rank ", ""))) or 0 + if pendingRank > 0 and existing.rank > pendingRank then + local existingTimeleft = (existing.startTime + existing.duration) - GetTime() + if existingTimeleft > 0 then + downrankBlocked = true + debuffVerified = false + CleveRoids.DebugChanged("downrank_" .. pending.spellID .. "_" .. _tostring(pending.targetGUID), + _string_format("|cffff6600[Downrank Blocked]|r %s Rank %d blocked by Rank %d (%.1fs left)", + spellName, pendingRank, existing.rank, existingTimeleft)) + end + end + end + end + end + + -- Non-pfUI fallback: check lib.objects for a higher rank of the same spell + if not downrankBlocked and lib.objects and pending.targetGUID and lib.objects[pending.targetGUID] then + local pendingBaseName = lib.GetSpellBaseName and lib:GetSpellBaseName(pending.spellID) + local pendingRank = lib.GetSpellRank and lib:GetSpellRank(pending.spellID) + if pendingBaseName and pendingRank > 0 then + for existingSID, rec in pairs(lib.objects[pending.targetGUID]) do + if existingSID ~= pending.spellID and rec and rec.caster == "player" + and rec.start and rec.duration then + local remaining = rec.duration + rec.start - GetTime() + if remaining > 0 then + local existingBaseName = lib:GetSpellBaseName(existingSID) + if existingBaseName == pendingBaseName then + local existingRank = lib:GetSpellRank(existingSID) + if existingRank > pendingRank then + downrankBlocked = true + debuffVerified = false + CleveRoids.DebugChanged("downrank_obj_" .. pending.spellID .. "_" .. _tostring(pending.targetGUID), + _string_format("|cffff6600[Downrank Blocked]|r %s Rank %d blocked by Rank %d in lib.objects (%.1fs left)", + pendingBaseName, pendingRank, existingRank, remaining)) + break + end + end + end + end + end + end + end + + local nonBleedVerifyUnit = not downrankBlocked + and pending.targetGUID and ResolveGUIDUnit(pending.targetGUID) or nil if nonBleedVerifyUnit then if _IsUnitDead(nonBleedVerifyUnit) then -- Target died - can't verify, assume landed @@ -3581,22 +3641,14 @@ delayedTrackingFrame:SetScript("OnUpdate", function() -- Use RecordImmunity for proper DBC school lookup (with bleed override) if pending.targetName and pending.targetName ~= "" then CleveRoids.RecordImmunity(pending.targetName, nil, nil, pending.spellID) - if debug then - local spellNameDebug = _GetSpellRecField(pending.spellID, "name") or "Debuff" - DEFAULT_CHAT_FRAME:AddMessage( - _string_format("|cffff6600[NonBleed Immunity]|r %s is immune to %s - only %d debuffs on target", - pending.targetName, spellNameDebug, totalDebuffs) - ) - end + CleveRoids.DebugChanged("nonbleed_immune_" .. _tostring(pending.targetName) .. "_" .. pending.spellID, + _string_format("|cffff6600[NonBleed Immunity]|r %s is immune to %s - only %d debuffs on target", + pending.targetName, _GetSpellRecField(pending.spellID, "name") or "Debuff", totalDebuffs)) end else - if debug then - local spellNameDebug = _GetSpellRecField(pending.spellID, "name") or "Debuff" - DEFAULT_CHAT_FRAME:AddMessage( - _string_format("|cffff6600[NonBleed Debuff Cap]|r %s not found on %s - likely pushed off (%d debuffs)", - spellNameDebug, pending.targetName or "Unknown", totalDebuffs) - ) - end + CleveRoids.DebugChanged("nonbleed_cap_" .. _tostring(pending.targetName) .. "_" .. pending.spellID, + _string_format("|cffff6600[NonBleed Debuff Cap]|r %s not found on %s - likely pushed off (%d debuffs)", + _GetSpellRecField(pending.spellID, "name") or "Debuff", pending.targetName or "Unknown", totalDebuffs)) end end end @@ -5196,12 +5248,9 @@ ev:SetScript("OnEvent", function() -- Refresh the timer lib.ownDebuffs[targetGuid][spellName].startTime = GetTime() - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ff00[SPELL_GO REFRESH]|r %s refreshed on %s", - spellName, lib.guidToName[targetGuid] or "Unknown") - ) - end + CleveRoids.DebugChanged("spellgo_refresh_" .. spellName .. "_" .. tostring(targetGuid), + string.format("|cff00ff00[SPELL_GO REFRESH]|r %s refreshed on %s", + spellName, lib.guidToName[targetGuid] or "Unknown")) end end end @@ -5632,13 +5681,9 @@ ev:SetScript("OnEvent", function() if existing and existing.startTime and existing.duration then local timeleft = (existing.startTime + existing.duration) - now if timeleft > 0 and rankNum > 0 and existing.rank and rankNum < existing.rank then - -- Lower rank cannot overwrite higher rank - if CleveRoids.debug then - DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff6600[AURA_CAST RANK BLOCK]|r %s Rank %d cannot overwrite Rank %d", - spellName, rankNum, existing.rank) - ) - end + CleveRoids.DebugChanged("auracast_rank_" .. spellName .. "_" .. tostring(targetGuid), + string.format("|cffff6600[AURA_CAST RANK BLOCK]|r %s Rank %d cannot overwrite Rank %d", + spellName, rankNum, existing.rank)) return end end @@ -5648,6 +5693,8 @@ ev:SetScript("OnEvent", function() duration = duration, texture = texture, rank = rankNum, + spellId = spellId, + stacks = 1, slot = nil, -- Will be set by DEBUFF_ADDED }