diff --git a/Conditionals.lua b/Conditionals.lua index 398488a..036ade7 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -926,6 +926,24 @@ function CleveRoids.GetSpammableConditional(name) return CleveRoids.spamConditions[name] or "nomybuff" end +-- PERFORMANCE: Cache for stripped spell names (removes rank suffix) +local _strippedNameCache = {} +local _strippedCacheSize = 0 +local _STRIPPED_CACHE_MAX = 128 + +local function GetStrippedSpellName(name) + if not name or name == "" then return "" end + local cached = _strippedNameCache[name] + if cached then return cached end + + local stripped = string.gsub(name, "%(.-%)%s*", "") + if _strippedCacheSize < _STRIPPED_CACHE_MAX then + _strippedNameCache[name] = stripped + _strippedCacheSize = _strippedCacheSize + 1 + end + return stripped +end + -- Checks whether or not we're currently casting a spell with cast time -- Returns TRUE if we should allow the cast (not casting, or not casting the specified spell) -- Returns FALSE if we should block the cast (currently casting) @@ -948,8 +966,9 @@ function CleveRoids.CheckCasting(castingSpell) end -- With parameter: check if we're casting a specific spell - local spellName = string.gsub(CleveRoids.CurrentSpell.spellName or "", "%(.-%)%s*", "") - local casting = string.gsub(castingSpell, "%(.-%)%s*", "") + -- PERFORMANCE: Use cached stripped names to avoid string.gsub per call + local spellName = GetStrippedSpellName(CleveRoids.CurrentSpell.spellName) + local casting = GetStrippedSpellName(castingSpell) -- If we're casting this specific spell, block the recast if CleveRoids.CurrentSpell.type == "cast" and spellName == casting then @@ -982,8 +1001,9 @@ function CleveRoids.CheckChanneled(channeledSpell) end -- Remove the "(Rank X)" part from the spells name in order to allow downranking - local spellName = string.gsub(CleveRoids.CurrentSpell.spellName or "", "%(.-%)%s*", "") - local channeled = string.gsub(channeledSpell, "%(.-%)%s*", "") + -- PERFORMANCE: Use cached stripped names to avoid string.gsub per call + local spellName = GetStrippedSpellName(CleveRoids.CurrentSpell.spellName) + local channeled = GetStrippedSpellName(channeledSpell) -- If we're channeling this specific spell, block the recast if CleveRoids.CurrentSpell.type == "channeled" and spellName == channeled then @@ -2001,20 +2021,18 @@ function CleveRoids.ValidateUnitDebuff(unit, args) ) end - -- FALLBACK: If not found in tracking table (e.g., shared debuff from another player), - -- scan actual debuffs on target using UnitDebuff() - -- IMPORTANT: Skip this fallback for personal debuffs - they should only match if player cast them - local isPersonalDebuff = false - if table.getn(matchingSpellIDs) > 0 then - for _, spellID in ipairs(matchingSpellIDs) do - if CleveRoids.libdebuff:IsPersonalDebuff(spellID) then - isPersonalDebuff = true - break - end - end - end + -- FALLBACK: If not found in tracking table, scan actual debuffs on target + -- This handles: + -- 1. Shared debuffs from other players (we only track player's own casts) + -- 2. Personal debuffs during the 0.2s pending delay after casting + -- 3. Any debuffs not yet registered in tracking (edge cases) + -- + -- For simple existence checks ([nodebuff]Spell), we want to find the debuff + -- regardless of whether it's in our tracking table. Time-remaining checks + -- will use tracking table data when available, but existence is from scan. + local isSimpleExistenceCheck = not args.operator and not args.amount - if not found and not isPersonalDebuff and CleveRoids.hasSuperwow then + if not found and CleveRoids.hasSuperwow and isSimpleExistenceCheck then -- Scan debuff slots for i = 1, 16 do local tex, debuffStacks, _, debuffSpellID = UnitDebuff(unit, i) diff --git a/Core.lua b/Core.lua index 6563729..1ab7b10 100644 --- a/Core.lua +++ b/Core.lua @@ -1934,17 +1934,20 @@ end function CleveRoids.GetParsedMsg(msg) if not msg then return end - -- ALWAYS refresh the side-flag for '?' even when we hit the cache - local _, ignorecount = string.gsub(CleveRoids.Trim(msg), "^%?", "") - CleveRoids._ignoretooltip = ignorecount - + -- PERFORMANCE: Check cache first before doing string operations local cached = CleveRoids.ParsedMsg[msg] if cached then - -- keep a per-msg copy too (helps future readers/tools) - cached.ignoretooltip = cached.ignoretooltip or ignorecount + -- Use cached ignoretooltip value (already computed during first parse) + CleveRoids._ignoretooltip = cached.ignoretooltip or 0 return cached.action, cached.conditionals end + -- Only compute ignoretooltip for new messages (not cache hits) + -- PERFORMANCE: Use string.sub for simple prefix check instead of gsub + local trimmed = CleveRoids.Trim(msg) + local ignorecount = (string.sub(trimmed, 1, 1) == "?") and 1 or 0 + CleveRoids._ignoretooltip = ignorecount + local action, conditionals = CleveRoids.ParseMsg(msg) CleveRoids.ParsedMsg[msg] = { action = action, @@ -2055,13 +2058,16 @@ function CleveRoids.TestAction(cmd, args) CleveRoids.FixEmptyTarget(conditionals) - for k, v in pairs(conditionals) do + -- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation + local k, v = next(conditionals) + while k do if not CleveRoids.ignoreKeywords[k] then if not CleveRoids.Keywords[k] or not CleveRoids.Keywords[k](conditionals) then conditionals.target = origTarget return end end + k, v = next(conditionals, k) end conditionals.target = origTarget diff --git a/Extensions/CursiveCustomSpells.lua b/Extensions/CursiveCustomSpells.lua index d2eac0d..9b8fdc8 100644 --- a/Extensions/CursiveCustomSpells.lua +++ b/Extensions/CursiveCustomSpells.lua @@ -250,11 +250,8 @@ local function HookCursiveLoadCurses() -- Call original first originalLoadCurses(self) - -- Then inject our custom spells - local count = InjectCustomSpells() - if count > 0 then - DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[CursiveCustomSpells]|r Injected " .. count .. " custom spells into Cursive.") - end + -- Then inject our custom spells (silent) + InjectCustomSpells() end Cursive.curses._customSpellsHooked = true diff --git a/Utility.lua b/Utility.lua index 8c9767f..51de6a8 100644 --- a/Utility.lua +++ b/Utility.lua @@ -906,12 +906,13 @@ lib.personalDebuffs = lib.personalDebuffs or { -- NOTE: 51750 is the CAST spell for Turtle WoW, not a debuff (debuff is 51752) -- SHAMAN - [8050] = 12, -- Flame Shock (Rank 1) - [8052] = 12, -- Flame Shock (Rank 2) - [8053] = 12, -- Flame Shock (Rank 3) - [10447] = 12, -- Flame Shock (Rank 4) - [10448] = 12, -- Flame Shock (Rank 5) - [29228] = 12, -- Flame Shock (Rank 6) + -- NOTE: Flame Shock is 15s in Turtle WoW (was 12s in vanilla) + [8050] = 15, -- Flame Shock (Rank 1) + [8052] = 15, -- Flame Shock (Rank 2) + [8053] = 15, -- Flame Shock (Rank 3) + [10447] = 15, -- Flame Shock (Rank 4) + [10448] = 15, -- Flame Shock (Rank 5) + [29228] = 15, -- Flame Shock (Rank 6) -- Frost Shock [8056] = 8, -- Frost Shock (Rank 1) @@ -1386,6 +1387,36 @@ function lib:AddEffect(guid, unitName, spellID, duration, stacks, caster) lib.objects[guid][spellID] = rec + -- PFUI INTEGRATION: Inject all tracked debuffs into pfUI's libdebuff + -- This allows pfUI to show accurate timers for all player-cast debuffs + if pfUI and pfUI.api and pfUI.api.libdebuff and unitName and caster == "player" then + local pflib = pfUI.api.libdebuff + local spellName = SpellInfo(spellID) + + if spellName and pflib.AddEffect then + -- Get target level for pfUI's tracking structure + local targetLevel = UnitLevel(guid) or UnitLevel("target") or 1 + + -- Strip rank from spell name for pfUI (it uses base names) + local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + + -- Also register the duration in pfUI's duration table + if pflib.debuffs then + pflib.debuffs[baseName] = duration + end + + -- Add the effect to pfUI's tracking + pflib:AddEffect(unitName, targetLevel, baseName, duration, "player") + + if CleveRoids.debug then + DEFAULT_CHAT_FRAME:AddMessage( + string.format("|cff00ff00[pfUI Inject]|r %s (%ds) on %s (level %d)", + baseName, duration, unitName, targetLevel) + ) + end + end + end + -- DEBUG: Show what we stored if CleveRoids.debug then local spellName = SpellInfo(spellID) or "Unknown" @@ -2082,37 +2113,89 @@ end -- Frame for delayed personal debuff tracking and judgement scanning -- NOTE: Carnage refresh is now handled via PLAYER_COMBO_POINTS event in ComboPointTracker.lua + +-- PERFORMANCE: Upvalues for frequently called functions +local _GetTime = GetTime +local _UnitExists = UnitExists +local _UnitDebuff = UnitDebuff +local _UnitIsDead = UnitIsDead +local _UnitName = UnitName +local _SpellInfo = SpellInfo +local _string_find = string.find +local _string_format = string.format +local _string_sub = string.sub +local _tostring = tostring +local _next = next +local _getn = table.getn -- Lua 5.0 compatible table length + +-- PERFORMANCE: Static buffers for removal tracking - reused every frame +-- Using mark-and-sweep pattern instead of table.remove() for O(1) removal +local _pendingJudgementsBuffer = {} +local _pendingPersonalBuffer = {} +local _pendingCCBuffer = {} +local _pendingSharedBuffer = {} + +-- PERFORMANCE: Throttling - run at 20Hz max instead of every frame (60+Hz) +-- Minimum delay is 0.2s, so 20Hz (50ms) gives us 4 checks per minimum delay +local _lastDelayedTrackingUpdate = 0 +local _DELAYED_TRACKING_INTERVAL = 0.05 -- 50ms = 20Hz + local delayedTrackingFrame = CreateFrame("Frame", "CleveRoidsDelayedTrackingFrame", UIParent) delayedTrackingFrame:SetScript("OnUpdate", function() + -- PERFORMANCE: Throttle updates - most pending items have 0.2-0.5s delays anyway + local currentTime = _GetTime() + if (currentTime - _lastDelayedTrackingUpdate) < _DELAYED_TRACKING_INTERVAL then + return + end + _lastDelayedTrackingUpdate = currentTime + + -- PERFORMANCE: Early exit if all queues are empty + local hasJudgements = lib.pendingJudgements and lib.pendingJudgements[1] + local hasPersonal = lib.pendingPersonalDebuffs and lib.pendingPersonalDebuffs[1] + local hasCC = lib.pendingCCDebuffs and lib.pendingCCDebuffs[1] + local hasShared = lib.pendingSharedDebuffs and lib.pendingSharedDebuffs[1] + local hasOverrides = lib.rankRefreshOverrides and _next(lib.rankRefreshOverrides) + + if not (hasJudgements or hasPersonal or hasCC or hasShared or hasOverrides) then + return + end + + -- Cache frequently accessed values + local hasSuperwow = CleveRoids.hasSuperwow + local debug = CleveRoids.debug + -- Process pending judgement scans to detect actual debuff IDs - if lib.pendingJudgements then - local toRemove = {} - for i, pending in ipairs(lib.pendingJudgements) do - local elapsed = GetTime() - pending.timestamp + if hasJudgements then + local writeIdx = 0 + local pendingList = lib.pendingJudgements + + for i = 1, _getn(pendingList) do + local pending = pendingList[i] + local elapsed = currentTime - pending.timestamp -- Scan target after 0.5 seconds to find the actual judgement debuff if elapsed >= 0.5 then -- Check if this is still the current target - local _, currentTargetGUID = UnitExists("target") + local _, currentTargetGUID = _UnitExists("target") currentTargetGUID = CleveRoids.NormalizeGUID(currentTargetGUID) - if currentTargetGUID == pending.targetGUID and CleveRoids.hasSuperwow then + if currentTargetGUID == pending.targetGUID and hasSuperwow then -- Scan all debuffs on target to find judgement-type debuffs for slot = 1, 16 do - local _, _, _, debuffSpellID = UnitDebuff("target", slot) + local _, _, _, debuffSpellID = _UnitDebuff("target", slot) if not debuffSpellID then break end - local debuffName = SpellInfo(debuffSpellID) + local debuffName = _SpellInfo(debuffSpellID) -- Check if this is a judgement debuff (name starts with "Judgement") - if debuffName and string.find(debuffName, "^Judgement") then + if debuffName and _string_find(debuffName, "^Judgement") then -- Found a judgement debuff! Store it for refresh tracking if not lib.detectedJudgementDebuffIDs[debuffSpellID] then lib.detectedJudgementDebuffIDs[debuffSpellID] = true lib.judgementSpells[debuffSpellID] = true -- Add to refresh list - if CleveRoids.debug then + if debug then DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ffff[Judgement Detect]|r Found debuff %s (ID:%d) from cast (ID:%d) - added to refresh list", + _string_format("|cff00ffff[Judgement Detect]|r Found debuff %s (ID:%d) from cast (ID:%d) - added to refresh list", debuffName, debuffSpellID, pending.castSpellID) ) end @@ -2120,35 +2203,41 @@ delayedTrackingFrame:SetScript("OnUpdate", function() end end end - - -- Mark for removal - table.insert(toRemove, i) + -- Item processed, don't copy to output + else + -- Item not ready, keep it + writeIdx = writeIdx + 1 + if writeIdx ~= i then + pendingList[writeIdx] = pending + end end end - -- Remove processed scans (iterate backwards to avoid index shifting) - for i = table.getn(toRemove), 1, -1 do - table.remove(lib.pendingJudgements, toRemove[i]) + -- PERFORMANCE: Clear remaining slots and update length + for i = writeIdx + 1, _getn(pendingList) do + pendingList[i] = nil end end -- Process pending personal debuffs - if lib.pendingPersonalDebuffs then - local pendingCount = table.getn(lib.pendingPersonalDebuffs) + if hasPersonal then + local pendingCount = _getn(lib.pendingPersonalDebuffs) -- Debug: Show pending count every few seconds (avoid spam) - if CleveRoids.debug and pendingCount > 0 then - local now = GetTime() - if not lib._lastPendingDebugTime or (now - lib._lastPendingDebugTime) > 2.0 then - lib._lastPendingDebugTime = now + 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) + _string_format("|cffaaaaaa[Pending Debug]|r %d personal debuffs waiting", pendingCount) ) end end - local toRemove = {} - for i, pending in ipairs(lib.pendingPersonalDebuffs) do - local elapsed = GetTime() - pending.timestamp + local writeIdx = 0 + local pendingList = lib.pendingPersonalDebuffs + + for i = 1, _getn(pendingList) do + local pending = pendingList[i] + local elapsed = currentTime - pending.timestamp -- Add debuff after 0.2 second delay (enough time for server sync) if elapsed >= 0.2 then @@ -2157,7 +2246,7 @@ delayedTrackingFrame:SetScript("OnUpdate", function() local isBleedSpell = CleveRoids.BleedSpellIDs and CleveRoids.BleedSpellIDs[pending.spellID] local bleedVerified = true - if isBleedSpell and CleveRoids.hasSuperwow and pending.targetGUID then + if isBleedSpell and hasSuperwow and pending.targetGUID then -- Check if mob is in bleed whitelist (skip verification for known bleeders) local isWhitelisted = CleveRoids.MobsThatBleed and CleveRoids.MobsThatBleed[pending.targetGUID] @@ -2165,12 +2254,12 @@ delayedTrackingFrame:SetScript("OnUpdate", function() -- Skip verification if target is dead (debuffs are removed on death) -- Dead targets would cause false positive immunity detection -- Note: SuperWoW allows GUID-based queries for all unit functions - if UnitIsDead(pending.targetGUID) then + if _UnitIsDead(pending.targetGUID) then -- Target died - can't verify immunity, assume bleed landed - if CleveRoids.debug then - local spellNameDebug = SpellInfo(pending.spellID) or "Bleed" + if debug then + local spellNameDebug = _SpellInfo(pending.spellID) or "Bleed" DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff6600[Bleed Verify Skip]|r Target %s is dead - skipping immunity check for %s", + _string_format("|cffff6600[Bleed Verify Skip]|r Target %s is dead - skipping immunity check for %s", pending.targetName or "Unknown", spellNameDebug) ) end @@ -2180,7 +2269,7 @@ delayedTrackingFrame:SetScript("OnUpdate", function() local totalDebuffs = 0 for slot = 1, 48 do - local _, _, _, debuffSpellID = UnitDebuff(pending.targetGUID, slot) + local _, _, _, debuffSpellID = _UnitDebuff(pending.targetGUID, slot) if not debuffSpellID then if slot <= 16 then break end -- Regular debuffs are dense, overflow continues on nil else @@ -2201,26 +2290,26 @@ delayedTrackingFrame:SetScript("OnUpdate", function() -- Record as BLEED immunity directly (bypass split damage override in RecordImmunity) -- RecordImmunity would record Rake/Pounce as "physical" (initial school), -- but we specifically detected the BLEED debuff didn't land - local spellNameForImmunity = SpellInfo(pending.spellID) or "Bleed" + local spellNameForImmunity = _SpellInfo(pending.spellID) or "Bleed" if not CleveRoids_ImmunityData["bleed"] then CleveRoids_ImmunityData["bleed"] = {} end CleveRoids_ImmunityData["bleed"][pending.targetName] = true - if CleveRoids.debug then + if debug then DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff6600[Bleed Immunity]|r %s is immune to bleed (%s) - only %d debuffs on target", + _string_format("|cffff6600[Bleed Immunity]|r %s is immune to bleed (%s) - only %d debuffs on target", pending.targetName, spellNameForImmunity, totalDebuffs) ) end end else -- Many debuffs = likely pushed off at debuff cap - if CleveRoids.debug then - local spellNameDebug = SpellInfo(pending.spellID) or "Bleed" + if debug then + local spellNameDebug = _SpellInfo(pending.spellID) or "Bleed" DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff6600[Debuff Cap]|r %s not found on %s - likely pushed off (%d debuffs on target)", + _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 @@ -2233,11 +2322,11 @@ delayedTrackingFrame:SetScript("OnUpdate", function() -- Only apply the debuff to tracking if it was verified (or not a bleed/was whitelisted) if bleedVerified then -- Debug: Show what we're about to add to tracking - if CleveRoids.debug then - local spellNameDebug = SpellInfo(pending.spellID) or "Unknown" - local guidStr = string.sub(tostring(pending.targetGUID or "nil"), 1, 20) + if debug then + local spellNameDebug = _SpellInfo(pending.spellID) or "Unknown" + local guidStr = _string_sub(_tostring(pending.targetGUID or "nil"), 1, 20) DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff88ff88[Pending Process]|r Adding %s (ID:%d, %ds) GUID:%s", + _string_format("|cff88ff88[Pending Process]|r Adding %s (ID:%d, %ds) GUID:%s", spellNameDebug, pending.spellID, pending.duration or 0, guidStr) ) end @@ -2256,19 +2345,19 @@ delayedTrackingFrame:SetScript("OnUpdate", function() if CleveRoids.lastRakeCast.targetGUID == pending.targetGUID and CleveRoids.lastRakeCast.spellID == pending.spellID then CleveRoids.lastRakeCast.pending = nil -- Mark as verified - if CleveRoids.debug then + if debug then DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ff00[Carnage]|r Rake verified on %s - ready for Ferocious Bite refresh", + _string_format("|cff00ff00[Carnage]|r Rake verified on %s - ready for Ferocious Bite refresh", pending.targetName or "Unknown") ) end end end - if CleveRoids.debug then - local spellName = SpellInfo(pending.spellID) or "Unknown" + if debug then + local spellName = _SpellInfo(pending.spellID) or "Unknown" DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ff00[Delayed Track]|r Applied %s (ID:%d) to tracking on %s", + _string_format("|cff00ff00[Delayed Track]|r Applied %s (ID:%d) to tracking on %s", spellName, pending.spellID, pending.targetName or "Unknown") ) end @@ -2279,32 +2368,39 @@ delayedTrackingFrame:SetScript("OnUpdate", function() if CleveRoids.lastRakeCast.targetGUID == pending.targetGUID and CleveRoids.lastRakeCast.spellID == pending.spellID then CleveRoids.lastRakeCast = nil -- Clear invalid Rake data - if CleveRoids.debug then + if debug then DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff6600[Carnage]|r Rake failed to apply on %s - clearing tracking data", + _string_format("|cffff6600[Carnage]|r Rake failed to apply on %s - clearing tracking data", pending.targetName or "Unknown") ) end end end end - - -- Mark for removal - table.insert(toRemove, i) + -- Item processed, don't copy to output + else + -- Item not ready, keep it + writeIdx = writeIdx + 1 + if writeIdx ~= i then + pendingList[writeIdx] = pending + end end end - -- Remove processed debuffs (iterate backwards to avoid index shifting) - for i = table.getn(toRemove), 1, -1 do - table.remove(lib.pendingPersonalDebuffs, toRemove[i]) + -- PERFORMANCE: Clear remaining slots + for i = writeIdx + 1, _getn(pendingList) do + pendingList[i] = nil end end -- Process pending CC debuffs for immunity detection - if lib.pendingCCDebuffs then - local toRemove = {} - for i, pending in ipairs(lib.pendingCCDebuffs) do - local elapsed = GetTime() - pending.timestamp + if hasCC then + local writeIdx = 0 + local pendingList = lib.pendingCCDebuffs + + for i = 1, _getn(pendingList) do + local pending = pendingList[i] + local elapsed = currentTime - pending.timestamp -- Check after delay: 0.4s for hidden CC (no visible debuff), 0.2s for normal CC -- Hidden CC (e.g., Pounce stun) needs longer delay to wait for "afflicted by" messages @@ -2322,13 +2418,13 @@ delayedTrackingFrame:SetScript("OnUpdate", function() -- CC IMMUNITY VERIFICATION: Check if CC effect actually landed -- Uses hybrid approach: direct spell ID match OR mechanic-based validation -- (CC debuff IDs often differ from cast IDs, e.g., Pounce cast ≠ Pounce Stun debuff) - if not ccVerified and CleveRoids.hasSuperwow and pending.targetGUID then + if not ccVerified and hasSuperwow and pending.targetGUID then -- Skip verification if target is dead (debuffs are removed on death) - if UnitIsDead(pending.targetGUID) then + if _UnitIsDead(pending.targetGUID) then ccVerified = true -- Assume CC landed, can't verify on dead target - if CleveRoids.debug then + if debug then DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff6600[CC Verify Skip]|r Target %s is dead - skipping immunity check for %s", + _string_format("|cffff6600[CC Verify Skip]|r Target %s is dead - skipping immunity check for %s", pending.targetName or "Unknown", pending.ccType or "CC") ) end @@ -2336,17 +2432,17 @@ delayedTrackingFrame:SetScript("OnUpdate", function() -- Method 1: Direct spell ID matching (scan debuffs for exact spell ID) if pending.spellID then for slot = 1, 48 do - local texture, _, _, debuffSpellID = UnitDebuff(pending.targetGUID, slot) + local texture, _, _, debuffSpellID = _UnitDebuff(pending.targetGUID, slot) if not texture then if slot <= 16 then break end -- Regular debuffs are dense, overflow continues on nil else totalDebuffs = totalDebuffs + 1 -- Debug: Show each debuff found during CC verification - if CleveRoids.debug then - local debuffName = SpellInfo(debuffSpellID) or "Unknown" + if debug then + local debuffName = _SpellInfo(debuffSpellID) or "Unknown" local mechanic = CleveRoids.GetSpellMechanic and CleveRoids.GetSpellMechanic(debuffSpellID) or 0 DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffaaaaaa[CC Scan]|r Slot %d: %s (ID:%d, Mech:%d) - Looking for %s (ID:%d)", + _string_format("|cffaaaaaa[CC Scan]|r Slot %d: %s (ID:%d, Mech:%d) - Looking for %s (ID:%d)", slot, debuffName, debuffSpellID or 0, mechanic, pending.spellName or "CC", pending.spellID or 0) ) end @@ -2362,16 +2458,16 @@ delayedTrackingFrame:SetScript("OnUpdate", function() -- CC debuff IDs often differ from cast IDs (e.g., Pounce stun uses different ID) -- Check if target has ANY debuff of the correct CC type (stun, fear, etc.) if not ccVerified and pending.ccType and CleveRoids.ValidateUnitCC then - if CleveRoids.debug then + if debug then DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffaaaa00[CC Verify]|r Direct ID match failed, trying mechanic check for %s...", + _string_format("|cffaaaa00[CC Verify]|r Direct ID match failed, trying mechanic check for %s...", pending.ccType) ) end ccVerified = CleveRoids.ValidateUnitCC(pending.targetGUID, pending.ccType) - if CleveRoids.debug then + if debug then DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00aaff[CC Verify]|r Mechanic check result: %s", + _string_format("|cff00aaff[CC Verify]|r Mechanic check result: %s", ccVerified and "FOUND" or "NOT FOUND") ) end @@ -2380,12 +2476,12 @@ delayedTrackingFrame:SetScript("OnUpdate", function() end -- If CC didn't land, check if it's immunity or debuff cap - if not ccVerified and not UnitIsDead(pending.targetGUID) then + if not ccVerified and not _UnitIsDead(pending.targetGUID) then -- totalDebuffs already counted above, reuse it - if totalDebuffs == 0 and CleveRoids.hasSuperwow and pending.targetGUID then + if totalDebuffs == 0 and hasSuperwow and pending.targetGUID then -- Count wasn't done (dead target check skipped counting), do it now for slot = 1, 48 do - local texture, _, _, debuffSpellID = UnitDebuff(pending.targetGUID, slot) + local texture, _, _, debuffSpellID = _UnitDebuff(pending.targetGUID, slot) if not texture then if slot <= 16 then break end else @@ -2401,15 +2497,15 @@ delayedTrackingFrame:SetScript("OnUpdate", function() resolvedTargetName = lib.guidToName[pending.targetGUID] -- Try to resolve from current target if not resolvedTargetName then - local _, currentTargetGUID = UnitExists("target") + local _, currentTargetGUID = _UnitExists("target") if currentTargetGUID and CleveRoids.NormalizeGUID(currentTargetGUID) == pending.targetGUID then - resolvedTargetName = UnitName("target") + resolvedTargetName = _UnitName("target") lib.guidToName[pending.targetGUID] = resolvedTargetName end end -- Try to resolve using SuperWoW GUID-based query - if not resolvedTargetName and CleveRoids.hasSuperwow then - local name = UnitName(pending.targetGUID) + if not resolvedTargetName and hasSuperwow then + local name = _UnitName(pending.targetGUID) if name and name ~= "Unknown" then resolvedTargetName = name lib.guidToName[pending.targetGUID] = resolvedTargetName @@ -2423,23 +2519,23 @@ delayedTrackingFrame:SetScript("OnUpdate", function() if resolvedTargetName and resolvedTargetName ~= "" and pending.ccType then CleveRoids.RecordCCImmunity(resolvedTargetName, pending.ccType, nil, pending.spellName) - if CleveRoids.debug then + if debug then DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ff00[CC Immunity]|r %s is immune to %s (%s) - verified: debuff missing, only %d debuffs on target", + _string_format("|cff00ff00[CC Immunity]|r %s is immune to %s (%s) - verified: debuff missing, only %d debuffs on target", resolvedTargetName, pending.ccType, pending.spellName or "Unknown", totalDebuffs) ) end - elseif CleveRoids.debug then + elseif debug then DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff6600[CC Immunity Skip]|r Could not resolve target name for GUID %s - cannot record %s immunity", + _string_format("|cffff6600[CC Immunity Skip]|r Could not resolve target name for GUID %s - cannot record %s immunity", pending.targetGUID or "nil", pending.ccType or "Unknown") ) end else -- Many debuffs = likely pushed off - if CleveRoids.debug then + if debug then DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff6600[CC Debuff Cap]|r %s not found on %s - likely pushed off (%d debuffs)", + _string_format("|cffff6600[CC Debuff Cap]|r %s not found on %s - likely pushed off (%d debuffs)", pending.ccType, resolvedTargetName or "Unknown", totalDebuffs) ) end @@ -2453,29 +2549,37 @@ delayedTrackingFrame:SetScript("OnUpdate", function() if resolvedTargetName and pending.ccType then CleveRoids.RemoveCCImmunity(resolvedTargetName, pending.ccType) end - if CleveRoids.debug then + if debug then DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff00ff00[CC Verified]|r %s landed on %s", + _string_format("|cff00ff00[CC Verified]|r %s landed on %s", pending.ccType, resolvedTargetName or "Unknown") ) end end - - table.insert(toRemove, i) + -- Item processed, don't copy to output + else + -- Item not ready, keep it + writeIdx = writeIdx + 1 + if writeIdx ~= i then + pendingList[writeIdx] = pending + end end end - -- Remove processed entries - for i = table.getn(toRemove), 1, -1 do - table.remove(lib.pendingCCDebuffs, toRemove[i]) + -- PERFORMANCE: Clear remaining slots + for i = writeIdx + 1, _getn(pendingList) do + pendingList[i] = nil end end -- Process pending shared debuffs for immunity detection - if lib.pendingSharedDebuffs then - local toRemove = {} - for i, pending in ipairs(lib.pendingSharedDebuffs) do - local elapsed = GetTime() - pending.timestamp + if hasShared then + local writeIdx = 0 + local pendingList = lib.pendingSharedDebuffs + + for i = 1, _getn(pendingList) do + local pending = pendingList[i] + local elapsed = currentTime - pending.timestamp -- Check after 0.2s delay (enough time for server sync) if elapsed >= 0.2 then @@ -2483,21 +2587,21 @@ delayedTrackingFrame:SetScript("OnUpdate", function() local totalDebuffs = 0 -- Skip verification if target is dead (debuffs are removed on death) - if CleveRoids.hasSuperwow and pending.targetGUID then - if UnitIsDead(pending.targetGUID) then + if hasSuperwow and pending.targetGUID then + if _UnitIsDead(pending.targetGUID) then -- Target died - can't verify immunity, assume debuff landed debuffVerified = true - if CleveRoids.debug then - local spellNameDebug = SpellInfo(pending.spellID) or "Shared Debuff" + if debug then + local spellNameDebug = _SpellInfo(pending.spellID) or "Shared Debuff" DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff6600[Shared Verify Skip]|r Target %s is dead - skipping immunity check for %s", + _string_format("|cffff6600[Shared Verify Skip]|r Target %s is dead - skipping immunity check for %s", pending.targetName or "Unknown", spellNameDebug) ) end else -- Target is alive - check if debuff exists for slot = 1, 48 do - local _, _, _, debuffSpellID = UnitDebuff(pending.targetGUID, slot) + local _, _, _, debuffSpellID = _UnitDebuff(pending.targetGUID, slot) if not debuffSpellID then if slot <= 16 then break end -- Regular debuffs are dense, overflow continues on nil else @@ -2517,10 +2621,10 @@ delayedTrackingFrame:SetScript("OnUpdate", function() -- Process result if debuffVerified then -- Debuff landed - add to tracking - if CleveRoids.debug then - local spellNameDebug = SpellInfo(pending.spellID) or "Unknown" + if debug then + local spellNameDebug = _SpellInfo(pending.spellID) or "Unknown" DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cff88ff88[Shared Verified]|r %s (ID:%d) verified on %s", + _string_format("|cff88ff88[Shared Verified]|r %s (ID:%d) verified on %s", spellNameDebug, pending.spellID, pending.targetName or "Unknown") ) end @@ -2543,39 +2647,43 @@ delayedTrackingFrame:SetScript("OnUpdate", function() end CleveRoids_ImmunityData[pending.school][pending.targetName] = true - if CleveRoids.debug then - local spellNameDebug = SpellInfo(pending.spellID) or "Unknown" + if debug then + local spellNameDebug = _SpellInfo(pending.spellID) or "Unknown" DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff6600[Shared Immunity]|r %s is immune to %s (%s) - only %d debuffs on target", + _string_format("|cffff6600[Shared Immunity]|r %s is immune to %s (%s) - only %d debuffs on target", pending.targetName, pending.school, spellNameDebug, totalDebuffs) ) end end else -- Many debuffs = likely pushed off at debuff cap - if CleveRoids.debug then - local spellNameDebug = SpellInfo(pending.spellID) or "Unknown" + if debug then + local spellNameDebug = _SpellInfo(pending.spellID) or "Unknown" DEFAULT_CHAT_FRAME:AddMessage( - string.format("|cffff6600[Shared Debuff Cap]|r %s not found on %s - likely pushed off (%d debuffs)", + _string_format("|cffff6600[Shared Debuff Cap]|r %s not found on %s - likely pushed off (%d debuffs)", spellNameDebug, pending.targetName or "Unknown", totalDebuffs) ) end end end - - table.insert(toRemove, i) + -- Item processed, don't copy to output + else + -- Item not ready, keep it + writeIdx = writeIdx + 1 + if writeIdx ~= i then + pendingList[writeIdx] = pending + end end end - -- Remove processed entries - for i = table.getn(toRemove), 1, -1 do - table.remove(lib.pendingSharedDebuffs, toRemove[i]) + -- PERFORMANCE: Clear remaining slots + for i = writeIdx + 1, _getn(pendingList) do + pendingList[i] = nil end end -- Clean up old rank refresh overrides (older than 2 seconds) - if lib.rankRefreshOverrides then - local currentTime = GetTime() + if hasOverrides then for spellName, override in pairs(lib.rankRefreshOverrides) do if currentTime - override.timestamp > 2.0 then lib.rankRefreshOverrides[spellName] = nil @@ -3452,6 +3560,19 @@ evLearn:SetScript("OnEvent", function() if remaining > 0 then -- Refresh: reset start time to now rec.start = GetTime() + + -- Sync refresh to pfUI + local targetName = lib.guidToName[targetGUID] + if pfUI and pfUI.api and pfUI.api.libdebuff and targetName then + local pflib = pfUI.api.libdebuff + local spellName = SpellInfo(flameShockID) + if spellName and pflib.AddEffect then + local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + local targetLevel = UnitLevel(targetGUID) or UnitLevel("target") or 1 + pflib:AddEffect(targetName, targetLevel, baseName, rec.duration, "player") + end + end + if CleveRoids.debug then DEFAULT_CHAT_FRAME:AddMessage( string.format("|cff0070dd[Molten Blast]|r Refreshed Flame Shock to %.1fs on %s",