mirror of
https://github.com/brues-code/SuperCleveRoidMacros.git
synced 2026-09-16 03:38:00 +00:00
update channeling to respect arcane missiles haste
This commit is contained in:
+246
-82
@@ -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
|
||||
|
||||
@@ -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
@@ -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(...)
|
||||
|
||||
Reference in New Issue
Block a user