update channeling to respect arcane missiles haste

This commit is contained in:
Jrc13245
2025-12-10 17:56:57 -05:00
parent 8d7b9086d9
commit 9c1f8649eb
3 changed files with 632 additions and 130 deletions
+246 -82
View File
@@ -5,6 +5,165 @@
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
-- Permanent cache for name normalization (underscores to spaces)
local _normalizedNames = {}
-- Cached name normalization
function CleveRoids.NormalizeName(name)
if not name then return name end
local c = _normalizedNames[name]
if c then return c end
c = string.gsub(name, "_", " ")
_normalizedNames[name] = c
return c
end
-- Direct passthrough functions (no caching overhead for normal usage)
function CleveRoids.GetCachedTime()
return GetTime()
end
function CleveRoids.GetCachedPlayerHealthPercent()
local max = UnitHealthMax("player")
return max > 0 and (100 * UnitHealth("player") / max) or 0
end
function CleveRoids.GetCachedPlayerPowerPercent()
local max = UnitManaMax("player")
return max > 0 and (100 * UnitMana("player") / max) or 0
end
function CleveRoids.GetCachedPlayerPower()
return UnitMana("player")
end
function CleveRoids.GetCachedTargetHealthPercent()
if not UnitExists("target") then return 0 end
local max = UnitHealthMax("target")
return max > 0 and (100 * UnitHealth("target") / max) or 0
end
-- Cooldown uses original function directly
function CleveRoids.GetCachedCooldown(name, ignoreGCD)
return CleveRoids._GetCooldownUncached(name, ignoreGCD)
end
-- No-ops for compatibility
function CleveRoids.ClearFrameCache() end
function CleveRoids.TrackButtonPress() end
function CleveRoids.GetCacheStats() return nil end
function CleveRoids.SetCacheTTL(ms) end
function CleveRoids.SetMacroThrottle(ms) end
function CleveRoids.GetMacroThrottle() return 0 end
-- ============================================================================
-- PERFORMANCE: Cache spell name -> spell ID mappings for debuff lookups
-- This avoids iterating personalDebuffs/sharedDebuffs and calling SpellInfo() repeatedly
local _spellNameToIDs = {} -- [spellName] = { spellID1, spellID2, ... }
local _spellNameToIDsBuilt = false
-- Build the spell name to ID cache (called once on first debuff check)
local function BuildSpellNameCache()
if _spellNameToIDsBuilt then return end
_spellNameToIDsBuilt = true
local lib = CleveRoids.libdebuff
if not lib then return end
local gsub = string.gsub
if lib.personalDebuffs then
for sid, _ in pairs(lib.personalDebuffs) do
local name = SpellInfo(sid)
if name then
name = gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "")
if not _spellNameToIDs[name] then
_spellNameToIDs[name] = {}
end
table.insert(_spellNameToIDs[name], sid)
end
end
end
if lib.sharedDebuffs then
for sid, _ in pairs(lib.sharedDebuffs) do
local name = SpellInfo(sid)
if name then
name = gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "")
if not _spellNameToIDs[name] then
_spellNameToIDs[name] = {}
end
-- Avoid duplicates
local found = false
for _, existingId in ipairs(_spellNameToIDs[name]) do
if existingId == sid then found = true; break end
end
if not found then
table.insert(_spellNameToIDs[name], sid)
end
end
end
end
end
-- Get cached spell IDs for a spell name
local function GetSpellIDsForName(spellName)
BuildSpellNameCache()
return _spellNameToIDs[spellName]
end
-- Invalidate cache (call if debuff lists change)
function CleveRoids.InvalidateSpellNameCache()
_spellNameToIDs = {}
_spellNameToIDsBuilt = false
end
-- PERFORMANCE: Equipment cache for HasGearEquipped (avoids 19-slot scan per call)
-- Invalidated on UNIT_INVENTORY_CHANGED via CleveRoids.InvalidateEquipmentCache()
local _equippedItemIDs = {} -- [slot] = itemID (number)
local _equippedItemNames = {} -- [slot] = itemName (lowercase string)
local _equipmentCacheValid = false
local function BuildEquipmentCache()
if _equipmentCacheValid then return end
_equipmentCacheValid = true
-- Clear old data
for i = 1, 19 do
_equippedItemIDs[i] = nil
_equippedItemNames[i] = nil
end
local string_find = string.find
local string_lower = string.lower
for slot = 1, 19 do
local link = GetInventoryItemLink("player", slot)
if link then
local _, _, id = string_find(link, "item:(%d+)")
local _, _, nameInBrackets = string_find(link, "%[(.+)%]")
if id then
_equippedItemIDs[slot] = tonumber(id)
end
if nameInBrackets then
_equippedItemNames[slot] = string_lower(nameInBrackets)
elseif id then
-- Fallback: resolve via GetItemInfo
local itemName = GetItemInfo(tonumber(id))
if itemName then
_equippedItemNames[slot] = string_lower(itemName)
end
end
end
end
end
-- Invalidate equipment cache (call on UNIT_INVENTORY_CHANGED)
function CleveRoids.InvalidateEquipmentCache()
_equipmentCacheValid = false
end
--This table maps stat keys to the functions that retrieve their values.
local stat_checks = {
-- Base Stats (Corrected to use the 'effective' stat with gear)
@@ -49,12 +208,14 @@ local stat_checks = {
shadow_res = function() local _, val = UnitResistance("player", 6); return val end
}
local function And(t,func)
-- PERFORMANCE: Avoid creating wrapper tables for single values
local function And(t, func)
if type(func) ~= "function" then return false end
-- PERFORMANCE: Handle non-table case without allocation
if type(t) ~= "table" then
t = { [1] = t }
return func(t) and true or false
end
for k,v in pairs(t) do
for k, v in pairs(t) do
if not func(v) then
return false
end
@@ -62,12 +223,13 @@ local function And(t,func)
return true
end
local function Or(t,func)
local function Or(t, func)
if type(func) ~= "function" then return false end
-- PERFORMANCE: Handle non-table case without allocation
if type(t) ~= "table" then
t = { [1] = t }
return func(t) and true or false
end
for k,v in pairs(t) do
for k, v in pairs(t) do
if func(v) then
return true
end
@@ -81,8 +243,10 @@ end
-- - AND separator (&) uses And() logic -> ALL values must match
local function Multi(t, func, conditionals, condition)
if type(func) ~= "function" then return false end
-- PERFORMANCE: Handle non-table case without allocation
if type(t) ~= "table" then
t = { [1] = t }
return func(t) and true or false
end
-- Check operator type from metadata
@@ -93,10 +257,16 @@ local function Multi(t, func, conditionals, condition)
if operatorType == "AND" then
-- AND separator (&): ALL must match
return And(t, func)
for k, v in pairs(t) do
if not func(v) then return false end
end
return true
else
-- OR separator (/) [default]: ANY can match
return Or(t, func)
for k, v in pairs(t) do
if func(v) then return true end
end
return false
end
end
@@ -107,8 +277,10 @@ end
-- This matches natural language: "no X or Y" intuitively means "neither X nor Y"
local function NegatedMulti(t, func, conditionals, condition)
if type(func) ~= "function" then return false end
-- PERFORMANCE: Handle non-table case without allocation
if type(t) ~= "table" then
t = { [1] = t }
return func(t) and true or false
end
-- Check operator type from metadata
@@ -120,10 +292,16 @@ local function NegatedMulti(t, func, conditionals, condition)
-- FLIPPED from positive conditionals (De Morgan's law for intuitive behavior)
if operatorType == "AND" then
-- AND separator (&): ANY negation can pass (missing at least one)
return Or(t, func)
for k, v in pairs(t) do
if func(v) then return true end
end
return false
else
-- OR separator (/) [default]: ALL negations must pass (missing all)
return And(t, func)
for k, v in pairs(t) do
if not func(v) then return false end
end
return true
end
end
@@ -342,29 +520,20 @@ end
function CleveRoids.HasGearEquipped(gearId)
if not gearId then return false end
-- PERFORMANCE: Build/refresh equipment cache if needed
BuildEquipmentCache()
-- Handle both numeric IDs and string IDs like "5196"
local wantId = tonumber(gearId)
local wantName = (type(gearId) == "string" and not wantId) and string.lower(gearId) or nil
-- PERFORMANCE: Use cached data instead of scanning all slots
for slot = 1, 19 do
local link = GetInventoryItemLink("player", slot)
if link then
local _, _, id = string.find(link, "item:(%d+)")
local _, _, nameInBrackets = string.find(link, "%[(.+)%]")
if wantId and id and tonumber(id) == wantId then
return true
end
if wantName and nameInBrackets and string.lower(nameInBrackets) == wantName then
return true
end
-- Fallback: resolve via GetItemInfo
if wantName and not nameInBrackets and id then
local itemName = GetItemInfo(tonumber(id))
if itemName and string.lower(itemName) == wantName then
return true
end
end
if wantId and _equippedItemIDs[slot] == wantId then
return true
end
if wantName and _equippedItemNames[slot] == wantName then
return true
end
end
return false
@@ -1048,50 +1217,50 @@ end
-- TODO: Look into https://github.com/Stanzilla/WoWUIBugs/issues/47 if needed
function CleveRoids.ValidateCooldown(args, ignoreGCD)
if not args then return false end
if type(args) ~= "table" then
-- Normalize the spell name by replacing underscores with spaces
local normalized = string.gsub(args, "_", " ")
-- NEW: If this is a numeric slot (1-19), resolve to the equipped item's name
local slotNum = tonumber(normalized)
local name
if type(args) ~= "table" then
-- PERFORMANCE: Use cached normalization
name = CleveRoids.NormalizeName(args)
-- If this is a numeric slot (1-19), resolve to the equipped item's name
local slotNum = tonumber(name)
if slotNum and slotNum >= 1 and slotNum <= 19 then
local link = GetInventoryItemLink("player", slotNum)
if link then
-- Extract item name from link
local _, _, itemName = string.find(link, "%[(.+)%]")
if itemName then
normalized = itemName
end
if itemName then name = itemName end
end
end
args = {name = normalized}
args = {name = name}
else
-- Also normalize if it's already a table
if args.name then
args.name = string.gsub(args.name, "_", " ")
-- PERFORMANCE: Use cached normalization
name = CleveRoids.NormalizeName(args.name)
-- NEW: If this is a numeric slot (1-19), resolve to the equipped item's name
local slotNum = tonumber(args.name)
-- If this is a numeric slot (1-19), resolve to the equipped item's name
local slotNum = tonumber(name)
if slotNum and slotNum >= 1 and slotNum <= 19 then
local link = GetInventoryItemLink("player", slotNum)
if link then
-- Extract item name from link
local _, _, itemName = string.find(link, "%[(.+)%]")
if itemName then
args.name = itemName
end
if itemName then name = itemName end
end
end
args.name = name
else
name = args.name
end
end
-- PERFORMANCE: GetCooldown is now cached per-frame
local expires = CleveRoids.GetCooldown(args.name, ignoreGCD)
local now = CleveRoids.GetCachedTime()
if not args.operator and not args.amount then
return expires > GetTime()
return expires > now
elseif CleveRoids.operators[args.operator] then
return CleveRoids.comparators[args.operator](expires - GetTime(), args.amount)
return CleveRoids.comparators[args.operator](expires - now, args.amount)
end
end
@@ -1262,33 +1431,11 @@ function CleveRoids.ValidateUnitDebuff(unit, args)
guid = CleveRoids.NormalizeGUID(guid)
if not guid then return false end
-- Find ALL spell IDs that match this name (all ranks)
local matchingSpellIDs = {}
if CleveRoids.libdebuff.personalDebuffs then
for sid, _ in pairs(CleveRoids.libdebuff.personalDebuffs) do
local name = SpellInfo(sid)
if name then
name = string.gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "")
if name == args.name then
table.insert(matchingSpellIDs, sid)
end
end
end
end
if CleveRoids.libdebuff.sharedDebuffs then
for sid, _ in pairs(CleveRoids.libdebuff.sharedDebuffs) do
local name = SpellInfo(sid)
if name then
name = string.gsub(name, "%s*%(%s*Rank%s+%d+%s*%)", "")
if name == args.name then
table.insert(matchingSpellIDs, sid)
end
end
end
end
-- PERFORMANCE: Use cached spell name -> ID mapping instead of iterating every call
local matchingSpellIDs = GetSpellIDsForName(args.name) or {}
-- Check tracking table for ANY rank of this spell: Did player cast this? Is timer valid?
if table.getn(matchingSpellIDs) > 0 then
if matchingSpellIDs and table.getn(matchingSpellIDs) > 0 then
for _, spellID in ipairs(matchingSpellIDs) do
local rec = CleveRoids.libdebuff.objects[guid] and CleveRoids.libdebuff.objects[guid][spellID]
-- For shared debuffs (Sunder, Faerie Fire, etc.), accept any caster (including nil)
@@ -1668,7 +1815,8 @@ function CleveRoids.ValidateWeaponImbue(slot, imbueName)
end
-- TODO: Look into https://github.com/Stanzilla/WoWUIBugs/issues/47 if needed
function CleveRoids.GetCooldown(name, ignoreGCD)
-- PERFORMANCE: Uncached version - called by GetCachedCooldown
function CleveRoids._GetCooldownUncached(name, ignoreGCD)
if not name then return 0 end
-- Check if it's a spell first
@@ -1684,12 +1832,17 @@ function CleveRoids.GetCooldown(name, ignoreGCD)
-- Convert remaining seconds to absolute expiry time
if remaining and remaining > 0 then
return GetTime() + remaining
return CleveRoids.GetCachedTime() + remaining
end
return 0
end
-- PERFORMANCE: Cached wrapper - use this for conditional checks
function CleveRoids.GetCooldown(name, ignoreGCD)
return CleveRoids.GetCachedCooldown(name, ignoreGCD)
end
-- TODO: Look into https://github.com/Stanzilla/WoWUIBugs/issues/47 if needed
-- Returns the cooldown of the given spellName or nil if no such spell was found
function CleveRoids.GetSpellCooldown(spellName, ignoreGCD)
@@ -2530,7 +2683,8 @@ CleveRoids.Keywords = {
-- Handle multi-comparison (e.g., >50&<80)
if args.comparisons and type(args.comparisons) == "table" then
local powerPercent = 100 / UnitManaMax("player") * UnitMana("player")
-- PERFORMANCE: Use cached player power
local powerPercent = CleveRoids.GetCachedPlayerPowerPercent()
-- ALL comparisons must pass (AND logic)
for _, comp in ipairs(args.comparisons) do
@@ -2580,7 +2734,8 @@ CleveRoids.Keywords = {
-- Handle multi-comparison (e.g., >500&<1000)
if args.comparisons and type(args.comparisons) == "table" then
local power = UnitMana("player")
-- PERFORMANCE: Use cached player power
local power = CleveRoids.GetCachedPlayerPower()
-- ALL comparisons must pass (AND logic)
for _, comp in ipairs(args.comparisons) do
@@ -2627,7 +2782,15 @@ CleveRoids.Keywords = {
if args.comparisons and type(args.comparisons) == "table" then
local unit = conditionals.target or "target"
if not UnitExists(unit) then return false end
local hp = 100 * UnitHealth(unit) / UnitHealthMax(unit)
-- PERFORMANCE: Use cached health for target
local hp
if unit == "target" then
hp = CleveRoids.GetCachedTargetHealthPercent()
else
local maxHp = UnitHealthMax(unit)
hp = maxHp > 0 and (100 * UnitHealth(unit) / maxHp) or 0
end
-- ALL comparisons must pass (AND logic)
for _, comp in ipairs(args.comparisons) do
@@ -2706,7 +2869,8 @@ CleveRoids.Keywords = {
-- Handle multi-comparison (e.g., >50&<80)
if args.comparisons and type(args.comparisons) == "table" then
local hp = 100 * UnitHealth("player") / UnitHealthMax("player")
-- PERFORMANCE: Use cached player health
local hp = CleveRoids.GetCachedPlayerHealthPercent()
-- ALL comparisons must pass (AND logic)
for _, comp in ipairs(args.comparisons) do
+124 -30
View File
@@ -53,6 +53,33 @@ local string_gsub = string.gsub
local table_insert = table.insert
local table_getn = table.getn
-- PERFORMANCE: Module-level constant for boolean conditionals (avoid per-call table creation)
local BOOLEAN_CONDITIONALS = {
combat = true,
nocombat = true,
stealth = true,
nostealth = true,
channeled = true,
nochanneled = true,
checkchanneled = true,
checkcasting = true,
dead = true,
alive = true,
help = true,
harm = true,
exists = true,
party = true,
raid = true,
resting = true,
noresting = true,
isplayer = true,
isnpc = true,
mhimbue = true,
nomhimbue = true,
ohimbue = true,
noohimbue = true,
}
local requirementCheckFrame = CreateFrame("Frame")
requirementCheckFrame:RegisterEvent("ADDON_LOADED")
requirementCheckFrame:SetScript("OnEvent", function()
@@ -1587,34 +1614,8 @@ function CleveRoids.ParseMsg(msg)
-- No args → the action is the implicit argument
if not args or args == "" then
if not conditionals[condition] then
-- Check if this is a boolean conditional (combat, stealth, channeled, etc.)
local booleanConditionals = {
combat = true,
nocombat = true,
stealth = true,
nostealth = true,
channeled = true,
nochanneled = true,
checkchanneled = true,
checkcasting = true,
dead = true,
alive = true,
help = true,
harm = true,
exists = true,
party = true,
raid = true,
resting = true,
noresting = true,
isplayer = true,
isnpc = true,
mhimbue = true,
nomhimbue = true,
ohimbue = true,
noohimbue = true,
}
if booleanConditionals[condition] then
-- PERFORMANCE: Use module-level constant instead of creating table per-call
if BOOLEAN_CONDITIONALS[condition] then
conditionals[condition] = true
else
conditionals[condition] = conditionals.action
@@ -3635,13 +3636,22 @@ function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time
-- For CHANNEL events, capture duration for checkchanneled conditional
if action == "CHANNEL" then
CleveRoids.channelStartTime = GetTime()
CleveRoids.channelDuration = cast_time / 1000 -- Convert ms to seconds
-- Try to get accurate duration from spell tooltip (reflects haste)
local tooltipDuration = CleveRoids.GetChannelDurationFromTooltipByID(spell_id)
if tooltipDuration then
CleveRoids.channelDuration = tooltipDuration
else
-- Fallback to UNIT_CASTEVENT duration (may not reflect haste)
CleveRoids.channelDuration = cast_time / 1000
end
end
-- For START events, capture cast time for checkcasting conditional
if action == "START" then
CleveRoids.castStartTime = GetTime()
CleveRoids.castDuration = cast_time / 1000 -- Convert ms to seconds
-- For cast-time spells, UNIT_CASTEVENT duration is usually accurate
-- but we could add tooltip scanning here too if needed
CleveRoids.castDuration = cast_time / 1000
end
end
@@ -3933,6 +3943,11 @@ end
function CleveRoids.Frame:UNIT_INVENTORY_CHANGED()
if arg1 ~= "player" then return end
-- PERFORMANCE: Invalidate equipment cache for HasGearEquipped
if CleveRoids.InvalidateEquipmentCache then
CleveRoids.InvalidateEquipmentCache()
end
-- In combat: Skip ALL processing - EquipBagItem already handles cache invalidation
-- This eliminates lag from IndexEquippedItems during rapid gear swapping
if UnitAffectingCombat("player") then
@@ -5219,6 +5234,85 @@ SlashCmdList["CLEVEROID"] = function(msg)
return
end
-- tooltipdebug (debug spell tooltip scanning)
if cmd == "tooltipdebug" or cmd == "ttdebug" or cmd == "spelltooltip" then
local spellName = val
if val2 and val2 ~= "" then
spellName = val .. " " .. val2
end
if not spellName or spellName == "" then
spellName = "Arcane Missiles" -- Default to Arcane Missiles
end
-- Strip underscores
spellName = string.gsub(spellName, "_", " ")
CleveRoids.Print("|cff88ff88=== Tooltip Scan Debug: " .. spellName .. " ===|r")
-- Find highest rank spell in spellbook (same logic as GetSpellSlotByName)
local slot, bookType = nil, nil
local foundRank = nil
local i = 1
while true do
local name, rank = GetSpellName(i, BOOKTYPE_SPELL)
if not name then break end
if name == spellName then
-- Keep updating to find the last (highest) rank
slot = i
bookType = BOOKTYPE_SPELL
foundRank = rank
end
i = i + 1
end
if not slot then
CleveRoids.Print("|cffff0000Spell not found in spellbook!|r")
return
end
CleveRoids.Print("Found in spellbook slot: " .. slot .. " (rank: " .. (foundRank or "none") .. ")")
-- Create tooltip if needed
if not CleveRoidsDebugTooltip then
CreateFrame("GameTooltip", "CleveRoidsDebugTooltip", nil, "GameTooltipTemplate")
CleveRoidsDebugTooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
end
CleveRoidsDebugTooltip:ClearLines()
CleveRoidsDebugTooltip:SetSpell(slot, bookType)
CleveRoids.Print("Tooltip lines (" .. CleveRoidsDebugTooltip:NumLines() .. "):")
for lineNum = 1, CleveRoidsDebugTooltip:NumLines() do
local leftText = getglobal("CleveRoidsDebugTooltipTextLeft" .. lineNum)
local rightText = getglobal("CleveRoidsDebugTooltipTextRight" .. lineNum)
local leftStr = leftText and leftText:GetText() or ""
local rightStr = rightText and rightText:GetText() or ""
if leftStr and leftStr ~= "" then
CleveRoids.Print(" L" .. lineNum .. ": " .. leftStr)
-- Check for duration pattern
local duration = string.match(leftStr, "for (%d+%.?%d*) sec")
if duration then
CleveRoids.Print(" |cff00ff00^ Found 'for X sec': " .. duration .. "s|r")
end
local duration2 = string.match(leftStr, "over (%d+%.?%d*) sec")
if duration2 then
CleveRoids.Print(" |cff00ff00^ Found 'over X sec': " .. duration2 .. "s|r")
end
end
if rightStr and rightStr ~= "" then
CleveRoids.Print(" R" .. lineNum .. ": " .. rightStr)
end
end
-- Show what GetSpellDurationFromTooltip returns
local cachedDuration = CleveRoids.GetSpellDurationFromTooltip and CleveRoids.GetSpellDurationFromTooltip(spellName)
CleveRoids.Print(" ")
CleveRoids.Print("GetSpellDurationFromTooltip result: " .. (cachedDuration and (cachedDuration .. "s") or "|cffff0000nil|r"))
return
end
-- Unknown command fallback
CleveRoids.Print("Usage:")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid - Show current settings")
+262 -18
View File
@@ -24,6 +24,131 @@ function CleveRoids.NormalizeGUID(guid)
return tostring(guid)
end
-- Hidden tooltip for scanning spell info
local SpellScanTooltip = nil
-- Create hidden tooltip for scanning (once)
local function GetSpellScanTooltip()
if not SpellScanTooltip then
SpellScanTooltip = CreateFrame("GameTooltip", "CleveRoidsSpellScanTooltip", nil, "GameTooltipTemplate")
SpellScanTooltip:SetOwner(WorldFrame, "ANCHOR_NONE")
end
return SpellScanTooltip
end
-- Cache for spell durations from tooltip
local cachedSpellDurations = {}
local spellDurationCacheTime = {}
local SPELL_CACHE_DURATION = 0.5 -- Re-scan every 0.5 seconds (haste can change mid-fight)
-- Get a spell's slot in the spellbook by spell ID
local function GetSpellSlotByID(targetSpellID)
local i = 1
while true do
local spellName = GetSpellName(i, BOOKTYPE_SPELL)
if not spellName then break end
-- Get spell ID for this slot using SpellInfo if available
if SpellInfo then
local _, _, spellID = GetSpellName(i, BOOKTYPE_SPELL)
-- Try to get ID from the spell slot
local slot, book = i, BOOKTYPE_SPELL
-- SpellInfo needs the spell name to get ID, but we can check via GetSpellTexture match
end
i = i + 1
end
return nil, nil
end
-- Get a spell's slot in the spellbook by name (finds highest rank by default)
-- If targetRank is specified (e.g., "Rank 5"), finds that specific rank
local function GetSpellSlotByName(targetSpellName, targetRank)
local foundSlot = nil
local foundRank = nil
local i = 1
while true do
local spellName, spellRank = GetSpellName(i, BOOKTYPE_SPELL)
if not spellName then break end
if spellName == targetSpellName then
if targetRank then
-- Looking for specific rank
if spellRank == targetRank then
return i, BOOKTYPE_SPELL
end
else
-- Looking for highest rank - keep scanning to find the last one
foundSlot = i
foundRank = spellRank
end
end
i = i + 1
end
-- Return highest rank found (last match in spellbook order)
return foundSlot, BOOKTYPE_SPELL
end
-- Parse channel duration from tooltip text
-- Matches patterns like "for 5 sec" or "over 8 sec" at the end of descriptions
local function ParseChannelDurationFromText(text)
if not text then return nil end
-- Match "for X sec" pattern (channels like Arcane Missiles, Drain Life)
local duration = string.match(text, "for (%d+%.?%d*) sec")
if duration then
return tonumber(duration)
end
-- Match "over X sec" pattern (some DoT/HoT tooltips)
duration = string.match(text, "over (%d+%.?%d*) sec")
if duration then
return tonumber(duration)
end
return nil
end
-- Scan spell tooltip for channel/cast duration
-- Returns duration in seconds, or nil if not found
function CleveRoids.GetSpellDurationFromTooltip(spellName)
if not spellName then return nil end
-- Check cache
local now = GetTime()
if cachedSpellDurations[spellName] and spellDurationCacheTime[spellName] then
if now - spellDurationCacheTime[spellName] < SPELL_CACHE_DURATION then
return cachedSpellDurations[spellName]
end
end
local slot, bookType = GetSpellSlotByName(spellName)
if not slot then return nil end
local tooltip = GetSpellScanTooltip()
tooltip:ClearLines()
tooltip:SetSpell(slot, bookType)
-- Scan tooltip lines for duration
for i = 1, tooltip:NumLines() do
local leftText = getglobal("CleveRoidsSpellScanTooltipTextLeft" .. i)
if leftText then
local text = leftText:GetText()
local duration = ParseChannelDurationFromText(text)
if duration then
-- Cache the result
cachedSpellDurations[spellName] = duration
spellDurationCacheTime[spellName] = now
return duration
end
end
end
return nil
end
-- Get channel duration for a spell by ID (looks up name first)
function CleveRoids.GetChannelDurationFromTooltipByID(spellID)
if not spellID or not SpellInfo then return nil end
local spellName = SpellInfo(spellID)
if not spellName then return nil end
return CleveRoids.GetSpellDurationFromTooltip(spellName)
end
if type(hooksecurefunc) ~= "function" then
function hooksecurefunc(arg1, arg2, arg3)
local tgt, fname, post
@@ -86,11 +211,31 @@ function CleveRoids.Seq(_, i)
return (i or 0) + 1
end
-- PERFORMANCE: Trim cache to avoid repeated gsub pattern matching
local _trimCache = {}
local _trimCacheSize = 0
local _TRIM_CACHE_MAX = 512
function CleveRoids.Trim(str)
if not str then
return nil
end
return string.gsub(str,"^%s*(.-)%s*$", "%1")
-- PERFORMANCE: Check cache first
local cached = _trimCache[str]
if cached ~= nil then
return cached
end
local result = string.gsub(str, "^%s*(.-)%s*$", "%1")
-- Cache the result (limit cache size)
if _trimCacheSize < _TRIM_CACHE_MAX then
_trimCache[str] = result
_trimCacheSize = _trimCacheSize + 1
end
return result
end
do
@@ -208,29 +353,114 @@ function CleveRoids.splitString(str, seperatorPattern)
return tbl
end
function CleveRoids.splitStringIgnoringQuotes(str, separator)
local result = {}
local temp = ""
local insideQuotes = false
local separators = {}
-- PERFORMANCE: Cache for splitStringIgnoringQuotes results
-- Key: str .. "|" .. separator_key, Value: result table
local _splitCache = {}
local _splitCacheSize = 0
local _SPLIT_CACHE_MAX = 256 -- Limit cache size to prevent unbounded growth
if type(separator) == "table" then
for _, s in separator do
separators[s] = s
-- PERFORMANCE: Pre-built separator tables for common cases
local _defaultSeparator = { [";"] = ";" }
local _commaSeparator = { [","] = ",", [" "] = " " }
local _colonSeparator = { [":"] = ":" }
-- PERFORMANCE: Reusable result table pool
local _splitResultPool = {}
local _splitResultPoolSize = 0
local function getSplitResult()
if _splitResultPoolSize > 0 then
local r = _splitResultPool[_splitResultPoolSize]
_splitResultPool[_splitResultPoolSize] = nil
_splitResultPoolSize = _splitResultPoolSize - 1
-- Clear the table for reuse
for k in pairs(r) do r[k] = nil end
return r
end
return {}
end
-- Return a result to the pool (call when done iterating)
function CleveRoids.ReleaseSplitResult(result)
if result and _splitResultPoolSize < 16 then
_splitResultPoolSize = _splitResultPoolSize + 1
_splitResultPool[_splitResultPoolSize] = result
end
end
function CleveRoids.splitStringIgnoringQuotes(str, separator)
if not str then return {""} end
-- PERFORMANCE: Build cache key and check cache
local sepKey
if separator == nil then
sepKey = ";"
elseif type(separator) == "table" then
-- For table separators, use a simple identifier
if separator[1] == "," and separator[2] == " " then
sepKey = ",_"
else
sepKey = "t"
end
else
separators[separator or ";"] = separator or ";"
sepKey = separator
end
for i = 1, string.len(str) do
local char = string.sub(str, i, i)
local cacheKey = str .. "|" .. sepKey
local cached = _splitCache[cacheKey]
if cached then
-- Return a copy to prevent mutation issues
local copy = getSplitResult()
for i = 1, table.getn(cached) do
copy[i] = cached[i]
end
return copy
end
local result = getSplitResult()
local temp = ""
local insideQuotes = false
local separators
-- PERFORMANCE: Use pre-built separator tables when possible
if separator == nil then
separators = _defaultSeparator
elseif type(separator) == "table" then
if separator[1] == "," and separator[2] == " " then
separators = _commaSeparator
elseif separator[1] == ":" then
separators = _colonSeparator
else
separators = {}
for _, s in separator do
separators[s] = s
end
end
else
if separator == ";" then
separators = _defaultSeparator
elseif separator == ":" then
separators = _colonSeparator
else
separators = { [separator] = separator }
end
end
-- PERFORMANCE: Use local references and avoid repeated function calls
local strlen = string.len(str)
local strsub = string.sub
local Trim = CleveRoids.Trim
local tinsert = table.insert
for i = 1, strlen do
local char = strsub(str, i, i)
if char == "\"" then
insideQuotes = not insideQuotes
temp = temp .. char
elseif char == separators[char] and not insideQuotes then
temp = CleveRoids.Trim(temp)
if temp ~= "" then table.insert(result, temp) end
elseif separators[char] and not insideQuotes then
temp = Trim(temp)
if temp ~= "" then tinsert(result, temp) end
temp = ""
else
temp = temp .. char
@@ -238,11 +468,25 @@ function CleveRoids.splitStringIgnoringQuotes(str, separator)
end
if temp ~= "" then
temp = CleveRoids.Trim(temp)
table.insert(result, temp)
temp = Trim(temp)
tinsert(result, temp)
end
return (next(result) and result or {""})
if not next(result) then
result[1] = ""
end
-- PERFORMANCE: Cache the result (store a copy)
if _splitCacheSize < _SPLIT_CACHE_MAX then
local cacheCopy = {}
for i = 1, table.getn(result) do
cacheCopy[i] = result[i]
end
_splitCache[cacheKey] = cacheCopy
_splitCacheSize = _splitCacheSize + 1
end
return result
end
function CleveRoids.Print(...)