diff --git a/ComboPointTracker.lua b/ComboPointTracker.lua index 94e1737..142aec4 100644 --- a/ComboPointTracker.lua +++ b/ComboPointTracker.lua @@ -260,8 +260,8 @@ function CleveRoids.IsComboScalingSpell(spellName) for spell, data in pairs(CleveRoids.ComboScalingSpells) do if data.all_ranks then -- Remove rank from spell name for comparison - local baseName = string.gsub(spell, "%(Rank %d+%)", "") - local checkName = string.gsub(spellName, "%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(spell) + local checkName = CleveRoids.StripRank(spellName) if baseName == checkName then return true end @@ -283,8 +283,8 @@ function CleveRoids.GetComboScalingData(spellName) -- Check for spells marked as all_ranks for spell, data in pairs(CleveRoids.ComboScalingSpells) do if data.all_ranks then - local baseName = string.gsub(spell, "%(Rank %d+%)", "") - local checkName = string.gsub(spellName, "%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(spell) + local checkName = CleveRoids.StripRank(spellName) if baseName == checkName then return data end @@ -492,7 +492,7 @@ function CleveRoids.TrackComboPointCastByID(spellID, targetGUID) local spellName = GetSpellRecField(spellID, "name") if spellName then -- Remove rank info for comparison - local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(spellName) if CleveRoids.ComboPointTracking[spellName] then local tracking = CleveRoids.ComboPointTracking[spellName] if tracking.combo_points and tracking.combo_points > 0 and @@ -648,7 +648,7 @@ if _G.CastSpell then if isCombo and currentCP and currentCP > 0 then local duration = CleveRoids.CalculateComboScaledDuration(spellName, currentCP) if duration then - local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(spellName) CleveRoids.ComboPointTracking[baseName] = { combo_points = currentCP, duration = duration, @@ -695,7 +695,7 @@ if _G.UseAction then if spellName and CleveRoids.IsComboScalingSpell(spellName) then local duration = CleveRoids.CalculateComboScaledDuration(spellName, currentCP) if duration then - local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(spellName) CleveRoids.ComboPointTracking[baseName] = { combo_points = currentCP, duration = duration, @@ -733,7 +733,7 @@ if CastSpellByName then -- Pre-populate tracking local duration = CleveRoids.CalculateComboScaledDuration(spellName, currentCP) if duration then - local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(spellName) CleveRoids.ComboPointTracking[baseName] = { combo_points = currentCP, duration = duration, diff --git a/Compatibility/pfUI.lua b/Compatibility/pfUI.lua index 5db1750..7607fdc 100644 --- a/Compatibility/pfUI.lua +++ b/Compatibility/pfUI.lua @@ -101,7 +101,7 @@ local function GetCarnageOverride(effect) for spellID, override in pairs(CleveRoids.carnageDurationOverrides) do local spellName = GetSpellRecField(spellID, "name") if spellName then - local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(spellName) if baseName == effect and override.timestamp and (GetTime() - override.timestamp) < 5 then local timeleft = override.duration - (GetTime() - override.timestamp) if timeleft < 0 then timeleft = 0 end @@ -211,7 +211,7 @@ function Extension.HookPfUILibdebuff() -- Get spell name for this ID local spellName = GetSpellRecField(spellID, "name") if spellName then - local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(spellName) if baseName == effect then -- Same spell - check if still active local remaining = rec.duration + rec.start - GetTime() @@ -389,7 +389,7 @@ function Extension.SyncComboDurationToPfUI(guid, spellID, duration) end -- Remove rank from spell name to match pfUI's format - local effectName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + local effectName = CleveRoids.StripRank(spellName) -- Update pfUI's stored debuff duration local pflib = pfUI.api.libdebuff diff --git a/Conditionals.lua b/Conditionals.lua index e5740a3..d0d17bf 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -107,7 +107,7 @@ local function BuildSpellNameCache() for sid, _ in pairs(lib.personalDebuffs) do local name = GetSpellRecField(sid, "name") if name then - name = gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "") + name = CleveRoids.StripRank(name) if not _spellNameToIDs[name] then _spellNameToIDs[name] = {} end @@ -120,7 +120,7 @@ local function BuildSpellNameCache() for sid, _ in pairs(lib.sharedDebuffs) do local name = GetSpellRecField(sid, "name") if name then - name = gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "") + name = CleveRoids.StripRank(name) if not _spellNameToIDs[name] then _spellNameToIDs[name] = {} end @@ -143,7 +143,7 @@ local function GetSpellIDsForName(spellName) -- Strip rank from input spell name to match cache keys -- This handles cases like "Faerie Fire (Feral)(Rank 4)" -> "Faerie Fire (Feral)" if spellName then - spellName = string.gsub(spellName, "%s*%(%s*Rank%s+%d+%s*%)", "") + spellName = CleveRoids.StripRank(spellName) -- Convert underscores to spaces for matching (e.g., "Thunder_Clap" -> "Thunder Clap") spellName = string.gsub(spellName, "_", " ") end @@ -619,14 +619,14 @@ local function Multi(t, func, conditionals, condition) if conditionals and conditionals._groups and conditionals._groups[condition] then local groups = conditionals._groups[condition] -- All groups must pass (AND between groups) - -- PERFORMANCE: Index-based loop instead of ipairs() to avoid iterator closure - local numGroups = table.getn(groups) + -- PERFORMANCE: Use .n field (set at build time) instead of table.getn() O(n) scan + local numGroups = groups.n or table.getn(groups) for gi = 1, numGroups do local group = groups[gi] local groupPassed = false local groupOp = group.operator or "OR" local values = group.values - local numValues = table.getn(values) + local numValues = group.n or table.getn(values) if groupOp == "AND" then -- AND within group: ALL values must match @@ -696,14 +696,14 @@ local function NegatedMulti(t, func, conditionals, condition) if conditionals and conditionals._groups and conditionals._groups[condition] then local groups = conditionals._groups[condition] -- All groups must pass (AND between groups) - -- PERFORMANCE: Index-based loop instead of ipairs() to avoid iterator closure - local numGroups = table.getn(groups) + -- PERFORMANCE: Use .n field (set at build time) instead of table.getn() O(n) scan + local numGroups = groups.n or table.getn(groups) for gi = 1, numGroups do local group = groups[gi] local groupPassed = false local groupOp = group.operator or "OR" local values = group.values - local numValues = table.getn(values) + local numValues = group.n or table.getn(values) -- FLIPPED from positive conditionals (De Morgan's law for intuitive behavior) if groupOp == "AND" then @@ -967,7 +967,7 @@ function CleveRoids.FindAllCasterAuraByName(targetGuid, searchName) if not casters then local searchLower = string.lower(searchName) for spellName, c in pairs(targetData) do - local baseName = string.gsub(spellName, "%s*%(%s*Rank%s+%d+%s*%)", "") + local baseName = CleveRoids.StripRank(spellName) if string.lower(baseName) == searchLower then casters = c break @@ -1073,7 +1073,7 @@ local function OnAutoAttackOther(attackerGuid, targetGuid, totalDamage, hitInfo, if CleveRoids.debug then local spellName = GetSpellRecField and GetSpellRecField(spellID, "name") or "Unknown" - local baseName = spellName and string.gsub(spellName, "%s*%(Rank %d+%)", "") or "Unknown" + local baseName = CleveRoids.StripRank(spellName) or "Unknown" DEFAULT_CHAT_FRAME:AddMessage( string.format("|cff00ffaa[Judgement Refresh]|r Refreshed %s (ID:%d) on melee hit - new duration: %ds", baseName, spellID, rec.duration) @@ -1083,7 +1083,7 @@ local function OnAutoAttackOther(attackerGuid, targetGuid, totalDamage, hitInfo, -- Sync to pfUI if loaded (pre-7.6 only) if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff then local spellName = GetSpellRecField and GetSpellRecField(spellID, "name") or nil - local baseName = spellName and string.gsub(spellName, "%s*%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(spellName) local targetName = (lib.guidToName and lib.guidToName[normalizedTarget]) or UnitName("target") local targetLevel = UnitLevel("target") or 0 if targetName and baseName then @@ -1242,6 +1242,7 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu if spellId and durationMs and durationMs > 0 then local spellName = GetSpellRecField and GetSpellRecField(spellId, "name") if spellName then + CleveRoids._allCasterAuraDirty = true if not CleveRoids.AllCasterAuraTracking[targetGuid] then CleveRoids.AllCasterAuraTracking[targetGuid] = {} end @@ -1324,6 +1325,9 @@ local function OnAuraCastOther(spellId, casterGuid, targetGuid, effect, effectAu end -- Cleanup old aura tracking entries (3-level: targetGuid → spellName → casterGuid) + -- PERFORMANCE: Skip cleanup if nothing was written since last cleanup + if not CleveRoids._allCasterAuraDirty then return end + CleveRoids._allCasterAuraDirty = false local tGuid, spellNames = next(CleveRoids.AllCasterAuraTracking) while tGuid do local nextTGuid = next(CleveRoids.AllCasterAuraTracking, tGuid) @@ -1413,6 +1417,7 @@ autoAttackFrame:SetScript("OnEvent", function() local playerGUID = CleveRoids.GetGUID("player") local durSpellName = GetSpellRecField and GetSpellRecField(spellId, "name") if playerGUID and durSpellName then + CleveRoids._allCasterAuraDirty = true if not CleveRoids.AllCasterAuraTracking[playerGUID] then CleveRoids.AllCasterAuraTracking[playerGUID] = {} end @@ -1712,7 +1717,7 @@ end local function NormalizeSpellNameForComparison(spellName) if not spellName then return nil end -- Strip rank suffix: "Faerie Fire (Feral)(Rank 4)" -> "Faerie Fire (Feral)" - local normalized = string.gsub(spellName, "%s*%(%s*Rank%s+%d+%s*%)", "") + local normalized = CleveRoids.StripRank(spellName) -- Convert underscores to spaces and lowercase normalized = string.lower(string.gsub(normalized, "_", " ")) return normalized @@ -1798,7 +1803,7 @@ local function _get_debuff_timeleft(unitToken, auraName) -- Strip rank suffix for consistent matching (e.g., "Rake(Rank 4)" -> "Rake") if auraName then - auraName = string.gsub(auraName, "%s*%(%s*Rank%s+%d+%s*%)", "") + auraName = CleveRoids.StripRank(auraName) -- Convert underscores to spaces for matching (e.g., "Thunder_Clap" -> "Thunder Clap") auraName = string.gsub(auraName, "_", " ") end @@ -1811,7 +1816,7 @@ local function _get_debuff_timeleft(unitToken, auraName) -- For overflow slots 17-48, nil means "regular buff filtered out", not "end of list" if not effect and idx <= 16 then break end -- Strip rank from effect name for comparison - local effectBase = effect and string.gsub(effect, "%s*%(%s*Rank%s+%d+%s*%)", "") + local effectBase = CleveRoids.StripRank(effect) if effectBase and effectBase == auraName and timeleft and timeleft >= 0 then return timeleft, duration end @@ -3546,7 +3551,7 @@ function CleveRoids.ValidateAura(unit, args, isbuff) -- Strip rank suffix for consistent matching (e.g., "Faerie Fire (Feral)(Rank 4)" -> "Faerie Fire (Feral)") if args.name then - args.name = string.gsub(args.name, "%s*%(%s*Rank%s+%d+%s*%)", "") + args.name = CleveRoids.StripRank(args.name) -- Convert underscores to spaces for matching (e.g., "Thunder_Clap" -> "Thunder Clap") args.name = string.gsub(args.name, "_", " ") end @@ -4032,7 +4037,7 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if not args.name then return false end -- Strip rank suffix for consistent matching (e.g., "Faerie Fire (Feral)(Rank 4)" -> "Faerie Fire (Feral)") - args.name = string.gsub(args.name, "%s*%(%s*Rank%s+%d+%s*%)", "") + args.name = CleveRoids.StripRank(args.name) -- Convert underscores to spaces for matching (e.g., "Thunder_Clap" -> "Thunder Clap") args.name = string.gsub(args.name, "_", " ") @@ -4159,7 +4164,7 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if rec and rec.caster == "player" then local n = GetSpellRecField and GetSpellRecField(sid, "name") if n then - n = _string_gsub(n, "%s*%(%s*Rank%s+%d+%s*%)", "") + n = CleveRoids.StripRank(n) if _string_lower(n) == cleanupNameLower then if CleveRoids.debug then DEFAULT_CHAT_FRAME:AddMessage( @@ -4246,7 +4251,7 @@ function CleveRoids.ValidateUnitDebuff(unit, args) if rec and rec.caster == "player" then local n = GetSpellRecField and GetSpellRecField(sid, "name") if n then - n = _string_gsub(n, "%s*%(%s*Rank%s+%d+%s*%)", "") + n = CleveRoids.StripRank(n) if _string_lower(n) == fallbackNameLower then local timeRemaining = rec.duration + rec.start - GetTime() if timeRemaining > 0 then @@ -4485,7 +4490,7 @@ function CleveRoids.ValidateUnitDebuff(unit, args) -- For overflow slots 17-48, nil means "regular buff filtered out", not "end of list" if not effect and idx <= 16 then break end -- Strip rank from effect name for comparison - local effectBase = effect and string.gsub(effect, "%s*%(%s*Rank%s+%d+%s*%)", "") + local effectBase = CleveRoids.StripRank(effect) if effectBase and effectBase == args.name then local shouldSkip = false @@ -5019,7 +5024,7 @@ function CleveRoids.CheckSpellCast(unit, spell) end if castEntry.spellName then -- Strip rank suffix for comparison: "Heal(Rank 4)" -> "Heal" - local baseName = string.gsub(castEntry.spellName, "%s*%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(castEntry.spellName) if baseName == spell or castEntry.spellName == spell then return true end @@ -5447,6 +5452,7 @@ end --- @return boolean function CleveRoids.IsUnitInMeleeRange(unit, cleaveRange) if not UnitExists(unit) then return false end + if UnitIsDead(unit) then return false end local threshold = cleaveRange and 5 or 2 diff --git a/Console.lua b/Console.lua index a0d8f2d..dce3422 100644 --- a/Console.lua +++ b/Console.lua @@ -260,7 +260,7 @@ SlashCmdList.CAST = function(msg) end if duration and CleveRoids.ComboPointTracking then -- Remove rank from spell name for pfUI compatibility - local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(spellName) -- Populate name-based tracking BEFORE the spell is cast -- This allows pfUI's AddEffect hook to find it CleveRoids.ComboPointTracking[baseName] = { diff --git a/Core.lua b/Core.lua index 243c189..b5e0a95 100644 --- a/Core.lua +++ b/Core.lua @@ -750,7 +750,7 @@ function CleveRoids.GetSpellCost(spellSlot, bookType) reagent = nil local name = GetSpellName(spellSlot, bookType) if name then - name = string.gsub(name, "%s*%(%s*Rank%s+%d+%s*%)%s*$", "") -- strip "(Rank X)" only + name = CleveRoids.StripRank(name) reagent = _ReagentBySpell[name] end end @@ -1926,7 +1926,7 @@ function CleveRoids.ParseMsg(msg) -- store the raw action for callers and strip trailing "(Rank X)" for comparisons conditionals.action = action - action = string.gsub(action, "%s*%(%s*Rank%s+%d+%s*%)%s*$", "") + action = CleveRoids.StripRank(action) -- IMPORTANT: if there's NO conditional block, return nil conditionals so -- DoWithConditionals will hit the {macroName} execution branch. @@ -1991,7 +1991,7 @@ function CleveRoids.ParseMsg(msg) if not conditionals._groups then conditionals._groups = {} end - conditionals._groups[condition] = { { values = { conditionals.action }, operator = "OR" } } + conditionals._groups[condition] = { { values = { conditionals.action }, n = 1, operator = "OR" }, n = 1 } end else -- existing code for when conditionals[condition] already exists @@ -2011,9 +2011,12 @@ function CleveRoids.ParseMsg(msg) conditionals._groups = {} end if not conditionals._groups[condition] then - conditionals._groups[condition] = {} + conditionals._groups[condition] = { n = 0 } end - table.insert(conditionals._groups[condition], { values = { conditionals.action }, operator = "OR" }) + local grp = conditionals._groups[condition] + local newEntry = { values = { conditionals.action }, n = 1, operator = "OR" } + grp.n = grp.n + 1 + grp[grp.n] = newEntry end else -- Has args. Ensure the key's value is a table and add new arguments. @@ -2064,10 +2067,12 @@ function CleveRoids.ParseMsg(msg) -- Create a new group for this conditional instance -- Structure: { values = { ... }, operator = "OR" or "AND" } if not conditionals._groups[condition] then - conditionals._groups[condition] = {} + conditionals._groups[condition] = { n = 0 } end - local currentGroup = { values = {}, operator = operatorType } - table.insert(conditionals._groups[condition], currentGroup) + local currentGroup = { values = {}, n = 0, operator = operatorType } + local grp = conditionals._groups[condition] + grp.n = grp.n + 1 + grp[grp.n] = currentGroup -- Split args by the determined separator for _, arg_item in CleveRoids.splitString(args, separator) do @@ -2096,7 +2101,8 @@ function CleveRoids.ParseMsg(msg) if not operator or not amount then -- No operator found, treat as simple string argument table.insert(conditionals[condition], processed_arg) - table.insert(currentGroup.values, processed_arg) + currentGroup.n = currentGroup.n + 1 + currentGroup.values[currentGroup.n] = processed_arg else local name_to_use = (name and name ~= "") and name or conditionals.action local final_amount_str, num_replacements = string.gsub(amount, "#", "") @@ -2130,7 +2136,8 @@ function CleveRoids.ParseMsg(msg) comparisons = comparisons -- Store all comparisons } table.insert(conditionals[condition], entry) - table.insert(currentGroup.values, entry) + currentGroup.n = currentGroup.n + 1 + currentGroup.values[currentGroup.n] = entry else -- Fallback to single comparison if parsing failed local entry = { @@ -2140,7 +2147,8 @@ function CleveRoids.ParseMsg(msg) checkStacks = should_check_stacks } table.insert(conditionals[condition], entry) - table.insert(currentGroup.values, entry) + currentGroup.n = currentGroup.n + 1 + currentGroup.values[currentGroup.n] = entry end else -- Normal single-comparison conditional (existing behavior) @@ -2151,7 +2159,8 @@ function CleveRoids.ParseMsg(msg) checkStacks = should_check_stacks } table.insert(conditionals[condition], entry) - table.insert(currentGroup.values, entry) + currentGroup.n = currentGroup.n + 1 + currentGroup.values[currentGroup.n] = entry end end end @@ -3983,48 +3992,61 @@ CleveRoids.CLEANUP_INTERVAL = 5 -- Run cleanup every 5 seconds local GetTime = GetTime local UnitAffectingCombat = UnitAffectingCombat local pairs = pairs +local next = next +local UnitPosition = UnitPosition +local IsAltKeyDown = IsAltKeyDown +local IsShiftKeyDown = IsShiftKeyDown +local IsControlKeyDown = IsControlKeyDown + +-- PERFORMANCE: Module-level constants (avoid re-creation per frame) +local POS_TRACK_INTERVAL = 0.01 -- 10ms between position samples (100 Hz, matches MonkeySpeed) +local SEQUENCE_STALE_TIME = 300 -- 5 minutes function CleveRoids.OnUpdate(self) + -- PERFORMANCE: Local alias for global namespace (saves ~80+ global lookups per frame) + local CR = CleveRoids + local CRM = CleveRoidMacros + -- Prevent SuperWoW API calls during shutdown (crash prevention) - if CleveRoids.isShuttingDown then return end + if CR.isShuttingDown then return end -- Clear macro stop flags at the start of each frame -- This ensures /stopmacro and /skipmacro only affect commands in the same frame (same macro execution) -- Without SuperMacro, this is necessary because we can't hook into macro line execution - if CleveRoids.stopMacroFlag then - CleveRoids.stopMacroFlag = false + if CR.stopMacroFlag then + CR.stopMacroFlag = false end - if CleveRoids.skipMacroFlag then - CleveRoids.skipMacroFlag = false + if CR.skipMacroFlag then + CR.skipMacroFlag = false end - if CleveRoids.stopOnCastFlag then - CleveRoids.stopOnCastFlag = false + if CR.stopOnCastFlag then + CR.stopOnCastFlag = false end -- PERFORMANCE: Single GetTime() call per frame local time = GetTime() -- PERFORMANCE: Early exit if not ready (before any other checks) - if not CleveRoids.ready then + if not CR.ready then -- Handle initialization timer only when not ready - if CleveRoids.initializationTimer and time >= CleveRoids.initializationTimer then - CleveRoids.initializationTimer = nil - CleveRoids.IndexItems() + if CR.initializationTimer and time >= CR.initializationTimer then + CR.initializationTimer = nil + CR.IndexItems() -- FIX: Set ready=true BEFORE IndexActionBars so GetAction() works -- IndexActionSlot calls GetAction() which has an early-exit if ready=false, -- preventing TestForActiveAction from populating actions.active - CleveRoids.ready = true + CR.ready = true -- FIX: Suppress action event handlers during initial indexing -- IndexActionBars sends ACTIONBAR_SLOT_CHANGED for each slot which triggers -- addon handlers (pfUI, Bongos). 120 rapid calls can cause issues. - CleveRoids._suppressActionHandlers = true - CleveRoids.IndexActionBars() - CleveRoids._suppressActionHandlers = false + CR._suppressActionHandlers = true + CR.IndexActionBars() + CR._suppressActionHandlers = false - CleveRoids.TestForAllActiveActions() - CleveRoids.lastUpdate = time + CR.TestForAllActiveActions() + CR.lastUpdate = time -- FIX: Force refresh ALL Blizzard action buttons after initialization -- Blizzard queries IsUsableAction during login before we're ready, @@ -4033,8 +4055,8 @@ function CleveRoids.OnUpdate(self) -- would trigger 120 rapid handler calls causing issues. if not (pfUI and pfUI.bars) then for slot = 1, 120 do - if CleveRoids.Actions[slot] then - CleveRoids.SendEventForAction(slot, "ACTIONBAR_SLOT_CHANGED", slot) + if CR.Actions[slot] then + CR.SendEventForAction(slot, "ACTIONBAR_SLOT_CHANGED", slot) end end end @@ -4048,17 +4070,16 @@ function CleveRoids.OnUpdate(self) -- Track player position at ~100 Hz with 4-sample circular buffer for snappy movement detection. -- Matches MonkeySpeed's 0.01s detection interval for similar responsiveness. -- PERFORMANCE: Pre-allocated circular buffer eliminates ~100 table allocs/sec and table.remove shifting. - local posTrackInterval = 0.01 -- 10ms between position samples (100 Hz, matches MonkeySpeed) - local lastPosTime = CleveRoids._positionTrackTime or 0 + local lastPosTime = CR._positionTrackTime or 0 - if (time - lastPosTime) >= posTrackInterval then - CleveRoids._positionTrackTime = time + if (time - lastPosTime) >= POS_TRACK_INTERVAL then + CR._positionTrackTime = time if UnitPosition then local x, y = UnitPosition("player") if x and y then -- Initialize circular buffer if needed (pre-allocate 4 entry tables) - local history = CleveRoids._positionHistory + local history = CR._positionHistory if not history then history = { { x = 0, y = 0, time = 0 }, @@ -4066,14 +4087,15 @@ function CleveRoids.OnUpdate(self) { x = 0, y = 0, time = 0 }, { x = 0, y = 0, time = 0 }, } - CleveRoids._positionHistory = history - CleveRoids._posHistoryIndex = 0 - CleveRoids._posHistoryCount = 0 + CR._positionHistory = history + CR._posHistoryIndex = 0 + CR._posHistoryCount = 0 end -- Advance circular index (1-based, wraps at 4) - local idx = math.mod(CleveRoids._posHistoryIndex, 4) + 1 - CleveRoids._posHistoryIndex = idx + local idx = CR._posHistoryIndex + 1 + if idx > 4 then idx = 1 end + CR._posHistoryIndex = idx -- Reuse existing entry (zero allocation) local entry = history[idx] @@ -4082,69 +4104,70 @@ function CleveRoids.OnUpdate(self) entry.time = time -- Track how many samples we have (caps at 4) - if CleveRoids._posHistoryCount < 4 then - CleveRoids._posHistoryCount = CleveRoids._posHistoryCount + 1 + if CR._posHistoryCount < 4 then + CR._posHistoryCount = CR._posHistoryCount + 1 end -- Maintain legacy vars for any code that uses them directly - if CleveRoids._posHistoryCount >= 2 then - local prevIdx = math.mod((idx - 2 + 4), 4) + 1 - CleveRoids._previousPlayerPos = history[prevIdx] - CleveRoids._currentPlayerPos = entry + if CR._posHistoryCount >= 2 then + local prevIdx = idx - 1 + if prevIdx < 1 then prevIdx = 4 end + CR._previousPlayerPos = history[prevIdx] + CR._currentPlayerPos = entry end end end end -- PERFORMANCE: Delayed WDB warmup after login (ensures GetItemInfo works after WDB clear) - if CleveRoids.wdbWarmupTime and time >= CleveRoids.wdbWarmupTime then - CleveRoids.wdbWarmupTime = nil - CleveRoids.DoWDBWarmup() + if CR.wdbWarmupTime and time >= CR.wdbWarmupTime then + CR.wdbWarmupTime = nil + CR.DoWDBWarmup() end -- PERFORMANCE: Cache refresh rate calculation (avoid per-frame division) - local refreshRate = CleveRoids.cachedRefreshRate + local refreshRate = CR.cachedRefreshRate if not refreshRate then - refreshRate = 1 / (CleveRoidMacros.refresh or 5) - CleveRoids.cachedRefreshRate = refreshRate + refreshRate = 1 / (CRM.refresh or 5) + CR.cachedRefreshRate = refreshRate end -- PERFORMANCE: Throttle check FIRST - skip most work on non-throttled frames - local lastUpdate = CleveRoids.lastUpdate or 0 - local bypassThrottle = CleveRoids.isActionUpdateQueued and CleveRoidMacros.realtime == 0 + local lastUpdate = CR.lastUpdate or 0 + local bypassThrottle = CR.isActionUpdateQueued and CRM.realtime == 0 local shouldUpdate = bypassThrottle or (time - lastUpdate) >= refreshRate if not shouldUpdate then return -- Early exit for non-throttled frames end - CleveRoids.lastUpdate = time + CR.lastUpdate = time -- Process deferred equipment index updates (for throttled UNIT_INVENTORY_CHANGED) -- PERFORMANCE: Skip check entirely if no pending update - local pendingTime = CleveRoids.equipIndexPendingTime + local pendingTime = CR.equipIndexPendingTime if pendingTime and not UnitAffectingCombat("player") then - if (time - (CleveRoids.lastEquipIndexTime or 0)) >= 0.2 then - CleveRoids.lastEquipIndexTime = time - CleveRoids.equipIndexPendingTime = nil - CleveRoids.lastItemIndexTime = time - CleveRoids.IndexItems() - CleveRoids.Actions = {} - CleveRoids.Macros = {} - CleveRoids.IndexActionBars() + if (time - (CR.lastEquipIndexTime or 0)) >= 0.2 then + CR.lastEquipIndexTime = time + CR.equipIndexPendingTime = nil + CR.lastItemIndexTime = time + CR.IndexItems() + CR.Actions = {} + CR.Macros = {} + CR.IndexActionBars() - if CleveRoidMacros.realtime == 0 then - CleveRoids.QueueActionUpdate() + if CRM.realtime == 0 then + CR.QueueActionUpdate() end end end -- PERFORMANCE: Check for expired reactive procs only if we have any -- Use statically allocated removal buffer to avoid per-frame allocation - local reactiveProcs = CleveRoids.reactiveProcs + local reactiveProcs = CR.reactiveProcs if reactiveProcs then local hasExpiredProc = false - local toRemove = CleveRoids._procRemovalBuffer + local toRemove = CR._procRemovalBuffer local removeCount = 0 -- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation @@ -4166,67 +4189,67 @@ function CleveRoids.OnUpdate(self) -- If any proc expired, immediately update all actions if hasExpiredProc then - CleveRoids.TestForAllActiveActions() - CleveRoids.isActionUpdateQueued = false + CR.TestForAllActiveActions() + CR.isActionUpdateQueued = false end end -- PERFORMANCE: Check for modifier key state changes (no events for these in vanilla WoW) -- Only queue update if modifier state actually changed - avoids full TestForAllActiveActions -- Nampower v2.41+ fires KEY_DOWN/KEY_UP for all keys including modifiers, so skip polling - if not CleveRoids.NampowerAPI.features.hasKeyEvents then - local altDown = IsAltKeyDown() and true or false - local shiftDown = IsShiftKeyDown() and true or false - local ctrlDown = IsControlKeyDown() and true or false + if not CR.NampowerAPI.features.hasKeyEvents then + local altDown = IsAltKeyDown() + local shiftDown = IsShiftKeyDown() + local ctrlDown = IsControlKeyDown() - if altDown ~= CleveRoids._lastAltDown or - shiftDown ~= CleveRoids._lastShiftDown or - ctrlDown ~= CleveRoids._lastCtrlDown then - CleveRoids._lastAltDown = altDown - CleveRoids._lastShiftDown = shiftDown - CleveRoids._lastCtrlDown = ctrlDown + if altDown ~= CR._lastAltDown or + shiftDown ~= CR._lastShiftDown or + ctrlDown ~= CR._lastCtrlDown then + CR._lastAltDown = altDown + CR._lastShiftDown = shiftDown + CR._lastCtrlDown = ctrlDown -- Modifier changed - queue update in event-driven mode, or just mark for realtime - if CleveRoidMacros.realtime == 0 then - CleveRoids.isActionUpdateQueued = true + if CRM.realtime == 0 then + CR.isActionUpdateQueued = true end end end -- Check the saved variable to decide which update mode to use. - if CleveRoidMacros.realtime == 1 then + if CRM.realtime == 1 then -- Realtime Mode: Force an update on every throttled tick for maximum responsiveness. - CleveRoids.TestForAllActiveActions() + CR.TestForAllActiveActions() else -- Event-Driven Mode (Default): Only update if a relevant game event has queued it. - if CleveRoids.isActionUpdateQueued then - if CleveRoids.debug then + if CR.isActionUpdateQueued then + if CR.debug then DEFAULT_CHAT_FRAME:AddMessage("|cffff00ff[OnUpdate]|r Processing queued action update") end - CleveRoids.TestForAllActiveActions() - CleveRoids.isActionUpdateQueued = false -- Reset the flag after updating - if CleveRoids.debug then + 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 end end -- The rest of this function handles time-based logic that must always run. - if CleveRoids.CurrentSpell.autoAttackLock and (time - CleveRoids.autoAttackLockElapsed) > refreshRate then - CleveRoids.CurrentSpell.autoAttackLock = false - CleveRoids.autoAttackLockElapsed = nil + if CR.CurrentSpell.autoAttackLock and (time - CR.autoAttackLockElapsed) > refreshRate then + CR.CurrentSpell.autoAttackLock = false + CR.autoAttackLockElapsed = nil end -- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation - local Sequences = CleveRoids.Sequences + local Sequences = CR.Sequences local seqKey, sequence = next(Sequences) while seqKey do if sequence.index > 1 and sequence.reset.secs and (time - (sequence.lastUpdate or 0)) >= sequence.reset.secs then - CleveRoids.ResetSequence(sequence) + CR.ResetSequence(sequence) end seqKey, sequence = next(Sequences, seqKey) end -- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation - local spell_tracking = CleveRoids.spell_tracking + local spell_tracking = CR.spell_tracking local guid, cast = next(spell_tracking) while guid do local nextGuid = next(spell_tracking, guid) -- Get next before potential removal @@ -4237,8 +4260,8 @@ function CleveRoids.OnUpdate(self) end -- Clean stale castTracking entries (standalone mode only, pfUI 7.6 manages its own) - if not CleveRoids.hasPfUI76 then - local ct = CleveRoids.castTracking + if not CR.hasPfUI76 then + local ct = CR.castTracking local ctGuid, ctEntry = next(ct) while ctGuid do local nextCtGuid = next(ct, ctGuid) @@ -4252,12 +4275,12 @@ function CleveRoids.OnUpdate(self) -- PERFORMANCE OPTIMIZATION: Run memory cleanup less frequently (every 5 seconds instead of every frame) -- This reduces CPU usage while maintaining effective memory management - if (time - CleveRoids.lastCleanupTime) >= CleveRoids.CLEANUP_INTERVAL then - CleveRoids.lastCleanupTime = time + if (time - CR.lastCleanupTime) >= CR.CLEANUP_INTERVAL then + CR.lastCleanupTime = time -- MEMORY: Clean up carnageDurationOverrides older than 30 seconds -- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation - local carnageOverrides = CleveRoids.carnageDurationOverrides + local carnageOverrides = CR.carnageDurationOverrides if carnageOverrides then local spellID, data = next(carnageOverrides) while spellID do @@ -4271,7 +4294,7 @@ function CleveRoids.OnUpdate(self) -- MEMORY: Clean up old ComboPointTracking entries (older than 60 seconds) -- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation - local comboTracking = CleveRoids.ComboPointTracking + local comboTracking = CR.ComboPointTracking if comboTracking then local trackName, data = next(comboTracking) while trackName do @@ -4284,25 +4307,34 @@ function CleveRoids.OnUpdate(self) end -- MEMORY: Clear spell name caches every 60 seconds (12 cleanup cycles) - CleveRoids._spellCacheCleanupCounter = (CleveRoids._spellCacheCleanupCounter or 0) + 1 - if CleveRoids._spellCacheCleanupCounter >= 12 then - CleveRoids._spellCacheCleanupCounter = 0 - if CleveRoids.ClearSpellNameCaches then - CleveRoids.ClearSpellNameCaches() + CR._spellCacheCleanupCounter = (CR._spellCacheCleanupCounter or 0) + 1 + if CR._spellCacheCleanupCounter >= 12 then + CR._spellCacheCleanupCounter = 0 + if CR.ClearSpellNameCaches then + CR.ClearSpellNameCaches() + end + if CR.ClearStripRankCache then + CR.ClearStripRankCache() end end -- MEMORY: Clean up stale perTargetState entries in sequences (older than 5 minutes) -- This prevents memory bloat from tracking many targets over time - local SEQUENCE_TARGET_STALE_TIME = 300 -- 5 minutes - for _, sequence in pairs(CleveRoids.Sequences) do - if sequence.perTargetState then - for guid, state in pairs(sequence.perTargetState) do - if state.lastUpdate and (time - state.lastUpdate) > SEQUENCE_TARGET_STALE_TIME then - sequence.perTargetState[guid] = nil + -- PERFORMANCE: Use next() instead of pairs() to avoid iterator closure allocation + local seqKey2, seq2 = next(CR.Sequences) + while seqKey2 do + if seq2.perTargetState then + local ptGuid, ptState = next(seq2.perTargetState) + while ptGuid do + local nextPtGuid = next(seq2.perTargetState, ptGuid) + if ptState.lastUpdate and (time - ptState.lastUpdate) > SEQUENCE_STALE_TIME then + seq2.perTargetState[ptGuid] = nil end + ptGuid = nextPtGuid + ptState = nextPtGuid and seq2.perTargetState[nextPtGuid] end end + seqKey2, seq2 = next(CR.Sequences, seqKey2) end end end @@ -5240,7 +5272,7 @@ function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time rec.start = GetTime() local spellName = GetSpellRecField(spellID, "name") - local baseName = spellName and string.gsub(spellName, "%s*%(Rank %d+%)", "") or "Unknown" + local baseName = CleveRoids.StripRank(spellName) or "Unknown" if CleveRoids.debug then DEFAULT_CHAT_FRAME:AddMessage( diff --git a/Extensions/OverflowBuffFrame.lua b/Extensions/OverflowBuffFrame.lua index b054b04..b4557c1 100644 --- a/Extensions/OverflowBuffFrame.lua +++ b/Extensions/OverflowBuffFrame.lua @@ -122,7 +122,7 @@ local function CreateIconButton(parent, index, iconTable) GameTooltip:SetOwner(btn, "ANCHOR_BOTTOMLEFT") local spellName = GetSpellRecField and GetSpellRecField(data.spellId, "name") or ("Spell " .. data.spellId) if spellName then - local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(spellName) GameTooltip:AddLine(baseName, 1, 1, 1) end if data.remaining and data.remaining > 0 then diff --git a/NampowerAPI.lua b/NampowerAPI.lua index 6c5ee6b..a8f5a8b 100644 --- a/NampowerAPI.lua +++ b/NampowerAPI.lua @@ -194,7 +194,25 @@ (aggressive/defensive/passive) including for infernal. - CastSpell now handles spells not in DBC gracefully. - Current version: v3.3.0 + Environmental Damage, Damage Shield, Dispel Events (v3.4+): + - ENVIRONMENTAL_DMG_SELF / ENVIRONMENTAL_DMG_OTHER - Environmental damage events + (exhaustion, drowning, fall, lava, slime, fire). Params: unitGuid, dmgType, + damage, absorb, resist. No CVar needed. + - DAMAGE_SHIELD_SELF / DAMAGE_SHIELD_OTHER - Damage shield events (Thorns, etc.). + Params: unitGuid, targetGuid, damage, spellSchool. No CVar needed. + - SPELL_DISPEL_BY_SELF / SPELL_DISPEL_BY_OTHER - Spell dispel events. + Params: casterGuid, targetGuid, spellId. No CVar needed. + + GetSpellRangeData, PvP Unit Data Blocking (v4.0+): + - GetSpellRangeData(rangeIndex) - Returns minRange, maxRange, flags, name for a + SpellRange DBC entry. flags=0 is ranged, flags=1 is melee (Combat Range). + Use with GetSpellRecField(spellId, "rangeIndex") to look up a spell's range. + - BREAKING: GetUnitData/GetUnitField now return nil for PvP-flagged units you can + attack. BUFF_ADDED_OTHER/BUFF_REMOVED_OTHER no longer fire for those units. + Existing API wrappers (GetUnitHealth, etc.) fall back to standard WoW API. + - Hooks initialized later so _GUID events work even with old perf_boost. + + Current version: v4.0.0 ]] local _G = _G or getfenv(0) @@ -411,6 +429,16 @@ API.VERSION_REQUIREMENTS = { -- v3.3+ - Chat bubble controls, demon autocast reaction state preservation ["ChatBubbleControls"] = { 3, 3, 0 }, -- NP_ChatBubbleDistance, NP_ChatBubblesWhisper, NP_ChatBubblesRaid, NP_ChatBubblesBattleground ["DemonAutocastReactions"] = { 3, 3, 0 }, -- NP_PreserveGreaterDemonAutocast now saves/restores reaction states including infernal + + -- v3.4+ - Environmental damage, damage shield, dispel events + ["EnvironmentalDmgEvents"] = { 3, 4, 0 }, -- ENVIRONMENTAL_DMG_SELF/OTHER events + ["DamageShieldEvents"] = { 3, 4, 0 }, -- DAMAGE_SHIELD_SELF/OTHER events + ["SpellDispelEvents"] = { 3, 4, 0 }, -- SPELL_DISPEL_BY_SELF/OTHER events + + -- v4.0+ - GetSpellRangeData, PvP unit data blocking, GUID event init fix + ["GetSpellRangeData"] = { 4, 0, 0, "GetSpellRangeData" }, + ["PvpUnitDataBlocking"] = { 4, 0, 0 }, -- GetUnitData/GetUnitField return nil for PvP-flagged attackable units; BUFF/DEBUFF_ADDED/REMOVED_OTHER suppressed + ["GuidEventInitFix"] = { 4, 0, 0 }, -- _GUID events now work with old perf_boost (hooks initialized later) } -- Check if a specific feature is available @@ -611,6 +639,16 @@ local function InitializeFeatures() f.hasChatBubbleControls = API.HasFeature("ChatBubbleControls") f.hasDemonAutocastReactions = API.HasFeature("DemonAutocastReactions") + -- v3.4+ Environmental damage, damage shield, dispel events + f.hasEnvironmentalDmgEvents = API.HasFeature("EnvironmentalDmgEvents") + f.hasDamageShieldEvents = API.HasFeature("DamageShieldEvents") + f.hasSpellDispelEvents = API.HasFeature("SpellDispelEvents") + + -- v4.0+ GetSpellRangeData, PvP unit data blocking, GUID event init fix + f.hasGetSpellRangeData = API.HasFeature("GetSpellRangeData") + f.hasPvpUnitDataBlocking = API.HasFeature("PvpUnitDataBlocking") + f.hasGuidEventInitFix = API.HasFeature("GuidEventInitFix") + -- Runtime detection for enhanced spell functions (verify by testing) if f.hasEnhancedSpellFunctions and GetSpellTexture then local success, result = pcall(function() @@ -1350,7 +1388,7 @@ function API.FindUnitAuraInfo(unitToken, searchSpellId, searchNameLower) local name = GetSpellRecField and GetSpellRecField(auraId, "name") if name then -- Strip rank suffix for consistent matching - local baseName = string.gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "") + local baseName = CleveRoids.StripRank(name) if string.lower(baseName) == searchNameLower then matched = true end @@ -3422,6 +3460,17 @@ function API.IsAuraHidden(spellId) return _G.IsAuraHidden(spellId) end +-- Get spell range data from SpellRange DBC by rangeIndex (v4.0+) +-- Returns minRange, maxRange, flags, name or nil if unavailable/invalid +-- flags: 0 = ranged, 1 = melee (Combat Range) +-- Note: returned ranges are raw DBC values before combat reach adjustments +function API.GetSpellRangeData(rangeIndex) + if not API.features.hasGetSpellRangeData or not _G.GetSpellRangeData then + return nil + end + return _G.GetSpellRangeData(rangeIndex) +end + -------------------------------------------------------------------------------- -- HEALTH/POWER WRAPPERS (GetUnitField extended token support with fallback) -------------------------------------------------------------------------------- diff --git a/Utility.lua b/Utility.lua index e77827e..3126c84 100644 --- a/Utility.lua +++ b/Utility.lua @@ -17,6 +17,29 @@ local tonumber = tonumber local tostring = tostring local GetTime = GetTime +-------------------------------------------------------------------------------- +-- PERFORMANCE: Memoized rank-stripping function +-- Caches results to avoid repeated string.gsub + string allocation on every call. +-- Two patterns are used across the codebase; we handle both with one function. +-- Cache grows with unique spell names (~200-300 total), negligible memory. +-------------------------------------------------------------------------------- +local _stripRankCache = {} +local _STRIP_RANK_CACHE_MAX = 512 + +function CleveRoids.StripRank(name) + if not name then return nil end + local cached = _stripRankCache[name] + if cached then return cached end + -- Handles both "Spell (Rank 4)" and "Spell(Rank 4)" with flexible whitespace + cached = gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "") + _stripRankCache[name] = cached + return cached +end + +function CleveRoids.ClearStripRankCache() + _stripRankCache = {} +end + -- Spells with physical damage + resistable CC effect (weapon-dependent) -- These spells deal physical damage that ALWAYS lands (unless dodged/parried/blocked), -- but apply a CC effect that can be resisted independently. @@ -2040,7 +2063,7 @@ function lib:GetSpellBaseName(spellID) if not name then return nil end -- Remove rank suffix - return string.gsub(name, "%s*%(Rank %d+%)", "") + return CleveRoids.StripRank(name) end -- Track rank refreshes for pfUI integration @@ -2265,7 +2288,7 @@ function lib:AddEffect(guid, unitName, spellID, duration, stacks, caster) 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+%)", "") + local baseName = CleveRoids.StripRank(spellName) -- Also register the duration in pfUI's duration table if pflib.debuffs then @@ -2523,7 +2546,7 @@ local function SeedUnit(unit) local spellName = GetSpellRecField(spellID, "name") if spellName and pflib.AddEffect then local targetLevel = UnitLevel(unit) or 1 - local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(spellName) if pflib.debuffs then pflib.debuffs[baseName] = duration end @@ -2612,7 +2635,7 @@ local function SeedUnit(unit) local spellName = GetSpellRecField(spellID, "name") if spellName and pflib.AddEffect then local targetLevel = UnitLevel(unit) or 1 - local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(spellName) if pflib.debuffs then pflib.debuffs[baseName] = duration end @@ -2882,7 +2905,7 @@ function lib.ApplyCarnageRefresh(targetGUID, targetName, biteSpellID) if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff then local pflib = pfUI.api.libdebuff local ripSpellName = GetSpellRecField(ripSpellID, "name") - local baseName = ripSpellName and string.gsub(ripSpellName, "%s*%(Rank %d+%)", "") or "Rip" + local baseName = CleveRoids.StripRank(ripSpellName) or "Rip" if CleveRoids.debug then DEFAULT_CHAT_FRAME:AddMessage( @@ -2995,7 +3018,7 @@ function lib.ApplyCarnageRefresh(targetGUID, targetName, biteSpellID) if not CleveRoids.hasPfUI76 and pfUI and pfUI.api and pfUI.api.libdebuff then local pflib = pfUI.api.libdebuff local rakeSpellName = GetSpellRecField(rakeSpellID, "name") - local baseName = rakeSpellName and string.gsub(rakeSpellName, "%s*%(Rank %d+%)", "") or "Rake" + local baseName = CleveRoids.StripRank(rakeSpellName) or "Rake" if CleveRoids.debug then DEFAULT_CHAT_FRAME:AddMessage( @@ -4204,7 +4227,7 @@ ev:SetScript("OnEvent", function() local spellName = GetSpellRecField(spellID, "name") if spellName and CleveRoids.ComboPointTracking then -- Remove rank from spell name to match pfUI's format - local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(spellName) CleveRoids.ComboPointTracking[baseName] = { combo_points = comboPoints, duration = duration, @@ -5153,7 +5176,7 @@ ev:SetScript("OnEvent", function() -- Name-based combo tracking for pfUI compatibility if debuffComboPoints and debuffComboPoints > 0 and CleveRoids.ComboPointTracking then - local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(spellName) CleveRoids.ComboPointTracking[baseName] = { combo_points = debuffComboPoints, duration = debuffDuration, @@ -5708,8 +5731,8 @@ evLearn:SetScript("OnEvent", function() for biteSpellID, _ in pairs(CleveRoids.FerociousBiteSpellIDs) do local biteName = GetSpellRecField(biteSpellID, "name") if biteName then - biteName = string.gsub(biteName, "%s*%(%s*Rank%s+%d+%s*%)", "") - local messageSpellName = string.gsub(spellName, "%s*%(%s*Rank%s+%d+%s*%)", "") + biteName = CleveRoids.StripRank(biteName) + local messageSpellName = CleveRoids.StripRank(spellName) if lower(biteName) == lower(messageSpellName) then isFerociousBite = true break @@ -5736,7 +5759,7 @@ evLearn:SetScript("OnEvent", function() -- PERSONAL DEBUFFS: Cancel pending tracking if spell was dodged/parried/blocked if lib.pendingPersonalDebuffs then - local messageSpellName = string.gsub(spellName, "%s*%(%s*Rank%s+%d+%s*%)", "") + local messageSpellName = CleveRoids.StripRank(spellName) local toRemove = {} for i, pending in ipairs(lib.pendingPersonalDebuffs) do @@ -5749,12 +5772,12 @@ evLearn:SetScript("OnEvent", function() local matchesCast = false if pendingSpellName then - pendingSpellName = string.gsub(pendingSpellName, "%s*%(%s*Rank%s+%d+%s*%)", "") + pendingSpellName = CleveRoids.StripRank(pendingSpellName) matchesTriggered = lower(pendingSpellName) == lower(messageSpellName) end if castSpellName then - castSpellName = string.gsub(castSpellName, "%s*%(%s*Rank%s+%d+%s*%)", "") + castSpellName = CleveRoids.StripRank(castSpellName) matchesCast = lower(castSpellName) == lower(messageSpellName) end @@ -5787,13 +5810,13 @@ evLearn:SetScript("OnEvent", function() find(message, "resist") or find(message, "immune") if isSpellFailure and lib.pendingCCDebuffs then - local messageSpellName = string.gsub(spellName, "%s*%(%s*Rank%s+%d+%s*%)", "") + local messageSpellName = CleveRoids.StripRank(spellName) local toRemove = {} for i, pending in ipairs(lib.pendingCCDebuffs) do local pendingSpellName = GetSpellRecField(pending.spellID, "name") if pendingSpellName then - pendingSpellName = string.gsub(pendingSpellName, "%s*%(%s*Rank%s+%d+%s*%)", "") + pendingSpellName = CleveRoids.StripRank(pendingSpellName) if lower(pendingSpellName) == lower(messageSpellName) then -- Found the pending CC that was avoided/resisted - cancel it (don't record immunity) if CleveRoids.debug then @@ -5820,7 +5843,7 @@ evLearn:SetScript("OnEvent", function() -- SHARED DEBUFFS: Cancel pending tracking if spell was dodged/parried/blocked/missed/resisted/immune if isSpellFailure and lib.pendingSharedDebuffs then - local messageSpellName = string.gsub(spellName, "%s*%(%s*Rank%s+%d+%s*%)", "") + local messageSpellName = CleveRoids.StripRank(spellName) local toRemove = {} for i, pending in ipairs(lib.pendingSharedDebuffs) do @@ -5832,12 +5855,12 @@ evLearn:SetScript("OnEvent", function() local matchesCast = false if pendingSpellName then - pendingSpellName = string.gsub(pendingSpellName, "%s*%(%s*Rank%s+%d+%s*%)", "") + pendingSpellName = CleveRoids.StripRank(pendingSpellName) matchesTracking = lower(pendingSpellName) == lower(messageSpellName) end if castSpellName then - castSpellName = string.gsub(castSpellName, "%s*%(%s*Rank%s+%d+%s*%)", "") + castSpellName = CleveRoids.StripRank(castSpellName) matchesCast = lower(castSpellName) == lower(messageSpellName) end @@ -5875,10 +5898,10 @@ evLearn:SetScript("OnEvent", function() -- Get the spell name from the cast (strip rank) local castSpellName = GetSpellRecField(castSpellID, "name") if castSpellName then - castSpellName = string.gsub(castSpellName, "%s*%(%s*Rank%s+%d+%s*%)", "") + castSpellName = CleveRoids.StripRank(castSpellName) -- Strip rank from the message spell name for comparison - local messageSpellName = string.gsub(spellName, "%s*%(%s*Rank%s+%d+%s*%)", "") + local messageSpellName = CleveRoids.StripRank(spellName) -- Only remove if the spell names match (case-insensitive) if lower(castSpellName) == lower(messageSpellName) then @@ -5901,7 +5924,7 @@ evLearn:SetScript("OnEvent", function() for sid, _ in pairs(lib.personalDebuffs) do local name = GetSpellRecField(sid, "name") if name then - name = string.gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "") + name = CleveRoids.StripRank(name) if name == castSpellName then table.insert(matchingSpellIDs, sid) end @@ -5912,7 +5935,7 @@ evLearn:SetScript("OnEvent", function() for sid, _ in pairs(lib.sharedDebuffs) do local name = GetSpellRecField(sid, "name") if name then - name = string.gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "") + name = CleveRoids.StripRank(name) if name == castSpellName then table.insert(matchingSpellIDs, sid) end @@ -5988,7 +6011,7 @@ evLearn:SetScript("OnEvent", function() local pflib = pfUI.api.libdebuff local spellName = GetSpellRecField(flameShockID, "name") if spellName and pflib.AddEffect then - local baseName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + local baseName = CleveRoids.StripRank(spellName) local targetLevel = UnitLevel(targetGUID) or UnitLevel("target") or 1 pflib:AddEffect(targetName, targetLevel, baseName, rec.duration, "player") end @@ -6167,7 +6190,7 @@ evJudgement:SetScript("OnEvent", function() local spellName = GetSpellRecField(spellID, "name") if spellName and targetName then - local effectName = string.gsub(spellName, "%s*%(Rank %d+%)", "") + local effectName = CleveRoids.StripRank(spellName) pfUI.api.libdebuff:AddEffect(targetName, targetLevel, effectName, rec.duration, "player") if CleveRoids.debug then @@ -9671,7 +9694,7 @@ local function HandleDebuffFade() for spellID in pairs(lib.objects[targetGUID]) do local name = GetSpellRecField(spellID, "name") if name then - name = gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "") + name = CleveRoids.StripRank(name) if name == spellName then -- Learn duration if we have timing data if lib.learnCastTimers[targetGUID] and