Files
SuperCleveRoidMacros/Core.lua
T
Brues 5e27e6fe58 Name the macro slot count
Three loops walked 1..36 by hand, two of them re-arguing in comments why 36
is the right number. It is: the client addresses 18 account-wide macros at
1-18 and 18 character-specific ones at 19-36, and that range is what
GetMacroInfo and C_Macro.SetMacroDisplay index. Nothing in the API hands it
back -- GetNumMacros() returns how many of each tab are used, which cannot be
summed into a range, because the character block still starts at 19 when the
account block is empty. Blizzard's MAX_MACROS is no better: it lives in the
load-on-demand Blizzard_MacroUI and is nil until the macro window is opened.

So CleveRoids.MAX_MACRO_SLOTS in Init.lua, carrying that reasoning once.
ValidateAllMacros also dropped a numAccountMacros it computed and never read.

ReleaseDisplays keeps sweeping the full range rather than the macros it knows
it published: PublishAllDisplays clears that record on each re-parse, so a
macro claimed before one and gone after it would keep our last published value
forever. Releasing a slot we never claimed costs nothing, and it runs once.
2026-09-11 13:57:11 -05:00

6834 lines
274 KiB
Lua

--[[
Author: Dennis Werner Garske (DWG) / brian / Mewtiny / brian / Mewtiny
License: MIT License
]]
-- DEBUG: Error catcher for "attempt to index a function value"
-- Remove this block once the error is identified
local _G = _G or getfenv(0)
local originalErrorHandler = geterrorhandler and geterrorhandler()
if seterrorhandler then
seterrorhandler(function(msg)
if msg and string.find(msg, "index a function value") then
local trace = debugstack and debugstack(2, 20, 0) or "no stack available"
DEFAULT_CHAT_FRAME:AddMessage("|cFFFF0000[SCRM DEBUG] index function error:|r " .. tostring(msg))
DEFAULT_CHAT_FRAME:AddMessage("|cFFFFFF00Stack:|r " .. tostring(trace))
end
if originalErrorHandler then
return originalErrorHandler(msg)
end
end)
end
-- Setup to wrap our stuff in a table so we don't pollute the global environment
local CleveRoids = _G.CleveRoids or {}
_G.CleveRoids = CleveRoids
CleveRoids.lastItemIndexTime = 0
CleveRoids.initializationTimer = nil
CleveRoids.isActionUpdateQueued = true
CleveRoids.lastEquipTime = CleveRoids.lastEquipTime or {}
CleveRoids.lastWeaponSwapTime = 0
CleveRoids.equipInProgress = false
-- PERFORMANCE: Event throttling to reduce spam from high-frequency events
CleveRoids.lastUnitAuraUpdate = 0
CleveRoids.lastUnitHealthUpdate = 0
CleveRoids.lastUnitPowerUpdate = 0
CleveRoids.EVENT_THROTTLE = 0.1 -- 100ms throttle for high-frequency events
-- PERFORMANCE: Spell ID cache to avoid repeated GetSpellIdForName lookups
CleveRoids.spellIdCache = {}
-- PERFORMANCE: Spell name construction cache
CleveRoids.spellNameCache = {}
CleveRoids.playerGuid = UnitGUID('player')
CleveRoids.playerClass = UnitClassBase("player")
-- PERFORMANCE: Upvalues for frequently called global functions (avoid global lookups)
local GetTime = GetTime
local UnitExists = UnitExists
local UnitAffectingCombat = UnitAffectingCombat
local GetContainerItemInfo = GetContainerItemInfo
local GetContainerNumSlots = GetContainerNumSlots
local GetItemInfo = GetItemInfo
local PickupContainerItem = PickupContainerItem
local PickupInventoryItem = PickupInventoryItem
local EquipCursorItem = EquipCursorItem
local CursorHasItem = CursorHasItem
local ClearCursor = ClearCursor
local TargetUnit = TargetUnit
local pcall = pcall
local pairs = pairs
local ipairs = ipairs
local type = type
local tonumber = tonumber
local tostring = tostring
local string_lower = string.lower
local string_gsub = string.gsub
local table_insert = table.insert
local table_getn = table.getn
-- Deferred stop-attack system (CheapShot pattern)
-- Stops autoattack and clears target. OnUpdate keeps clearing target while
-- autoattack is queued to prevent any swing from landing. No retarget.
local _DeferStopFrame = CreateFrame("Frame")
local _deferStopActive = false
local _deferStartTime = 0
local DEFER_STOP_TIMEOUT = 0.5
_DeferStopFrame:SetScript("OnUpdate", function()
if not _deferStopActive then
_DeferStopFrame:Hide()
return
end
-- Timeout: stop polling
if (GetTime() - _deferStartTime) >= DEFER_STOP_TIMEOUT then
_deferStopActive = false
_DeferStopFrame:Hide()
return
end
-- While autoattack is queued, keep clearing target to prevent swings
local _,_,_,_,_,_,autoattack = GetCurrentCastingInfo()
if autoattack == 1 then
if UnitExists("target") then
ClearTarget()
end
else
-- Autoattack stopped, done
_deferStopActive = false
_DeferStopFrame:Hide()
end
end)
_DeferStopFrame:Hide()
function CleveRoids.DeferStopAttack()
CleveRoids.CurrentSpell.autoAttack = false
CleveRoids.CurrentSpell.autoAttackLock = false
_deferStartTime = GetTime()
-- Only toggle auto-attack OFF if it's currently active (avoid turning it ON)
local attackSlot = CleveRoids.GetProxyActionSlot(CleveRoids.Localized.Attack)
if attackSlot and CleveRoids.Hooks.IsCurrentAction(attackSlot) then
AttackTarget()
end
ClearTarget()
_deferStopActive = true
_DeferStopFrame:Show()
end
-- PERFORMANCE: Module-level constant for boolean conditionals (avoid per-call table creation)
local BOOLEAN_CONDITIONALS = {
combat = true,
nocombat = true,
stealth = true,
nostealth = true,
mounted = true,
nomounted = true,
standing = true,
sitting = 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,
mhenchant = true,
nomhenchant = true,
ohenchant = true,
noohenchant = true,
locked = true,
nolocked = true,
indoors = true,
noindoors = true,
outdoors = true,
nooutdoors = true,
group = true,
nogroup = true,
moving = true,
nomoving = true,
stopattack = true,
pet = true,
nopet = true,
pethappiness = true,
nopethappiness = true,
focus = true,
nofocus = true,
bg = true,
nobg = true,
-- Dispel-type conditionals (ClassicAPI C_UnitAuras)
magic = true,
nomagic = true,
curse = true,
nocurse = true,
disease = true,
nodisease = true,
poison = true,
nopoison = true,
dispellable = true,
nodispellable = true,
magicbuff = true,
nomagicbuff = true,
dispellablebuff = true,
nodispellablebuff = true,
}
local requirementCheckFrame = CreateFrame("Frame")
requirementCheckFrame:RegisterEvent("ADDON_LOADED")
requirementCheckFrame:SetScript("OnEvent", function()
if arg1 ~= "SuperCleveRoidMacros" then return end
-- Check requirements immediately when our addon loads
local hasNampower = (IsSpellInRange ~= nil)
local hasUnitXP = pcall(UnitXP, "nop", "nop")
local hasNampower30 = hasNampower and CleveRoids.NampowerAPI
and CleveRoids.NampowerAPI.HasMinimumVersion(3, 0, 0)
local hasClassicAPI = CleveRoids.ClassicAPI and CleveRoids.ClassicAPI.IsAvailable()
-- v1.15.0 added frame:RegisterUnitEvent, which Utility.lua calls at file scope.
-- On an older build that call raises, aborting the rest of the chunk -- so this
-- is not a degraded-features warning, it's "the addon did not finish loading".
local hasClassicAPI1150 = hasClassicAPI and CleveRoids.ClassicAPI.HasMinimumVersion(1, 15, 0)
if not hasNampower30 or not hasUnitXP or not hasClassicAPI or not hasClassicAPI1150 then
-- Show warnings (don't disable — tearing down a partially-initialized addon causes hangs)
if not hasNampower then
CleveRoids.Print("|cFFFF9900WARNING:|r |cFF00FFFFAvitasia's Nampower v3.0.0+|r is required:")
CleveRoids.Print("https://github.com/brues-code/nampower")
CleveRoids.Print("Many features will be unavailable without Nampower.")
elseif not hasNampower30 then
local major, minor, patch = CleveRoids.NampowerAPI.GetVersion()
CleveRoids.Print(format("|cFFFF9900WARNING:|r |cFF00FFFFNampower v3.0.0+|r is required (you have v%d.%d.%d):", major, minor, patch))
CleveRoids.Print("https://github.com/brues-code/nampower")
CleveRoids.Print("Some features may be unavailable with this version.")
end
if not hasUnitXP then
CleveRoids.Print("|cFFFF9900WARNING:|r |cFF00FFFFKonaka's UnitXP_SP3|r is required:")
CleveRoids.Print("https://codeberg.org/konaka/UnitXP_SP3")
end
if not hasClassicAPI then
CleveRoids.Print("|cFFFF9900WARNING:|r |cFF00FFFFClassicAPI|r is required:")
CleveRoids.Print("https://github.com/brues-code/ClassicAPI")
CleveRoids.Print("Dispel-type and movement conditionals will be unavailable without it.")
elseif not hasClassicAPI1150 then
local major, minor, patch = CleveRoids.ClassicAPI.GetVersion()
CleveRoids.Print(format("|cFFFF9900WARNING:|r |cFF00FFFFClassicAPI v1.15.0+|r is required (you have v%d.%d.%d):", major, minor, patch))
CleveRoids.Print("https://github.com/brues-code/ClassicAPI")
CleveRoids.Print("The addon cannot finish loading on this version -- update ClassicAPI.")
end
end
-- Requirements met - allow normal initialization
CleveRoids.Print("|cFF4477FFSuperCleveR|r|cFFFFFFFFoid Macros|r |cFF00FF00Loaded|r - See the README.")
-- Unregister this check frame
this:UnregisterAllEvents()
end)
local SLOT_TO_INVID = {
["MainHandSlot"] = 16,
["SecondaryHandSlot"] = 17,
["RangedSlot"] = 18,
["HeadSlot"] = 1,
["NeckSlot"] = 2,
["ShoulderSlot"] = 3,
["ChestSlot"] = 5,
["WaistSlot"] = 6,
["LegsSlot"] = 7,
["FeetSlot"] = 8,
["WristSlot"] = 9,
["HandsSlot"] = 10,
["Finger0Slot"] = 11,
["Finger1Slot"] = 12,
["Trinket0Slot"] = 13,
["Trinket1Slot"] = 14,
["BackSlot"] = 15,
["ShirtSlot"] = 4,
["TabardSlot"] = 19,
}
local function GetInventoryIdFromSlot(slotName)
return SLOT_TO_INVID[slotName] or GetInventorySlotInfo(slotName)
end
-- Check if slot 18 is a relic (no GCD). Backed by CleveRoids.hasRelicSlot, cached
-- at PLAYER_LOGIN -- see the note there on why it can't be sampled on demand.
local function IsRelicSlot(slot)
if slot ~= 18 then return false end
return CleveRoids.hasRelicSlot or false
end
-- PERFORMANCE: Cache GetTime() result for cooldown checks within same frame
-- Slot 18 (relic/idol/libram/totem) has no GCD for Paladin/Druid/Shaman
local function IsSlotOnCooldown(slot, now)
-- Relics have no GCD - skip cooldown check entirely
if IsRelicSlot(slot) then
return false
end
now = now or GetTime()
local slotTime = CleveRoids.lastEquipTime[slot] or 0
local globalTime = CleveRoids.lastGlobalEquipTime or 0
return (now - slotTime) < CleveRoids.EQUIP_COOLDOWN or
(now - globalTime) < CleveRoids.EQUIP_GLOBAL_COOLDOWN
end
-- PERFORMANCE: Consolidated cursor handling, fewer CursorHasItem() calls
local function PerformEquipSwap(item, inventoryId, useQueueScript)
if not item or not inventoryId then return false end
-- Check if in combat and swapping weapons
-- Slot 18 is a ranged weapon for exactly the classes with no relic slot; for
-- Paladin/Druid/Shaman it's an idol/libram/totem that can swap freely.
local isWeapon = (inventoryId == 16 or inventoryId == 17)
if inventoryId == 18 then
isWeapon = not IsRelicSlot(inventoryId)
end
if isWeapon and UnitAffectingCombat("player") then
-- Don't swap while casting
if CleveRoids.CurrentSpell.type ~= "" then
return false
end
-- Check for on-swing spells if available (Nampower)
if GetCurrentCastingInfo then
local castId, _, _, casting, channeling, onswing = GetCurrentCastingInfo()
if onswing == 1 then
return false
end
-- If casting/channeling and QueueScript available, queue the swap for after cast
if (casting == 1 or channeling == 1) and QueueScript and useQueueScript and item.name then
local script = string.format('EquipItemByName("%s",%d)', item.name, inventoryId)
QueueScript(script, 3) -- Priority 3 = after queued spells
return true
end
end
end
-- PERFORMANCE: Try EquipItemByName first if available (handles everything internally)
if item.name and EquipItemByName then
local ok = pcall(EquipItemByName, item.name, inventoryId)
if ok then return true end
end
-- Fallback: Manual pickup and equip
if item.bagID and item.slot then
PickupContainerItem(item.bagID, item.slot)
elseif item.inventoryID then
PickupInventoryItem(item.inventoryID)
else
return false
end
-- Single cursor check after pickup attempt
if not CursorHasItem() then
return false
end
EquipCursorItem(inventoryId)
local success = not CursorHasItem()
ClearCursor()
return success
end
-- PERFORMANCE: Get a queue entry from pool or create new
local function GetQueueEntry()
local pool = CleveRoids.queueEntryPool
local n = pool and table_getn(pool) or 0
if n > 0 then
local entry = pool[n]
pool[n] = nil
return entry
end
return {}
end
-- PERFORMANCE: Return entry to pool for reuse
local function ReleaseQueueEntry(entry)
if not entry then return end
-- Clear the entry (set to nil, don't create new table)
entry.item = nil
entry.slotName = nil
entry.inventoryId = nil
entry.queueTime = nil
entry.retries = nil
entry.maxRetries = nil
entry.itemId = nil
-- Add to pool (max 10 pooled entries)
local pool = CleveRoids.queueEntryPool
if table_getn(pool) < 10 then
table_insert(pool, entry)
end
end
-- PERFORMANCE: Swap-and-pop removal (O(1) instead of O(n))
local function RemoveQueueEntry(i)
local queue = CleveRoids.equipmentQueue
local n = CleveRoids.equipmentQueueLen
local entry = queue[i]
if i < n then
queue[i] = queue[n]
end
queue[n] = nil
CleveRoids.equipmentQueueLen = n - 1
ReleaseQueueEntry(entry)
end
-- Queue equipment swap function
function CleveRoids.QueueEquipItem(item, slotName)
if not item or not slotName then return false end
local inventoryId = GetInventoryIdFromSlot(slotName)
if not inventoryId then return false end
local now = GetTime()
local itemId = item.id
-- PERFORMANCE: Check for duplicate queue entries before adding
local queue = CleveRoids.equipmentQueue
local queueLen = CleveRoids.equipmentQueueLen
for i = 1, queueLen do
local q = queue[i]
if q and q.inventoryId == inventoryId and q.itemId == itemId then
return false -- Already queued
end
end
-- Try immediate equip if not on cooldown (use QueueScript for smoother mid-cast swaps)
if not IsSlotOnCooldown(inventoryId, now) then
local success = PerformEquipSwap(item, inventoryId, true)
if success then
-- Don't set cooldowns for relic slots (no GCD)
if not IsRelicSlot(inventoryId) then
CleveRoids.lastEquipTime[inventoryId] = now
CleveRoids.lastGlobalEquipTime = now
end
return true
end
end
-- Queue for later using pooled entry
local entry = GetQueueEntry()
entry.item = item
entry.slotName = slotName
entry.inventoryId = inventoryId
entry.queueTime = now
entry.retries = 0
entry.maxRetries = 5
entry.itemId = itemId
CleveRoids.equipmentQueueLen = queueLen + 1
queue[CleveRoids.equipmentQueueLen] = entry
-- PERFORMANCE: Start the queue processing frame
if CleveRoids.equipQueueFrame then
CleveRoids.equipQueueFrame:Show()
end
return false
end
-- PERFORMANCE: Process equipment queue (called from self-disabling frame)
function CleveRoids.ProcessEquipmentQueue()
local queueLen = CleveRoids.equipmentQueueLen
if queueLen == 0 then return end
local now = GetTime()
local queue = CleveRoids.equipmentQueue
local i = 1
while i <= CleveRoids.equipmentQueueLen do
local queued = queue[i]
-- Remove expired entries first (>10 seconds old)
if (now - queued.queueTime) > 10 then
RemoveQueueEntry(i)
-- Don't increment i, we swapped in the last element
elseif not IsSlotOnCooldown(queued.inventoryId, now) then
-- Try to equip (use QueueScript for smoother mid-cast swaps)
local success = PerformEquipSwap(queued.item, queued.inventoryId, true)
if success then
-- Don't set cooldowns for relic slots (no GCD)
if not IsRelicSlot(queued.inventoryId) then
CleveRoids.lastEquipTime[queued.inventoryId] = now
CleveRoids.lastGlobalEquipTime = now
end
RemoveQueueEntry(i)
else
queued.retries = queued.retries + 1
if queued.retries >= queued.maxRetries then
RemoveQueueEntry(i)
else
i = i + 1
end
end
else
i = i + 1
end
end
end
-- PERFORMANCE: Self-disabling frame for queue processing with throttling
-- Only processes every EQUIP_QUEUE_INTERVAL instead of every frame
CleveRoids.EQUIP_QUEUE_INTERVAL = 0.1 -- 100ms between queue checks (was 50ms)
CleveRoids.equipQueueLastUpdate = 0
CleveRoids.equipQueueFrame = CreateFrame("Frame")
CleveRoids.equipQueueFrame:Hide()
-- PERFORMANCE: Upvalue for faster access in OnUpdate
local equipQueueFrame = CleveRoids.equipQueueFrame
local equipQueueInterval = CleveRoids.EQUIP_QUEUE_INTERVAL
equipQueueFrame:SetScript("OnUpdate", function()
-- Prevent operations during shutdown (crash prevention)
if CleveRoids.isShuttingDown then return end
-- PERFORMANCE: Use arg1 (elapsed time) if available, otherwise GetTime()
local elapsed = arg1
if elapsed then
CleveRoids.equipQueueLastUpdate = (CleveRoids.equipQueueLastUpdate or 0) + elapsed
if CleveRoids.equipQueueLastUpdate < equipQueueInterval then
return -- Throttle: skip this frame
end
CleveRoids.equipQueueLastUpdate = 0
else
local now = GetTime()
if (now - (CleveRoids.equipQueueLastUpdate or 0)) < equipQueueInterval then
return
end
CleveRoids.equipQueueLastUpdate = now
end
CleveRoids.ProcessEquipmentQueue()
if CleveRoids.equipmentQueueLen == 0 then
this:Hide()
end
end)
-- Improved DisableAddon function
function CleveRoids.DisableAddon(reason)
-- Prevent multiple disable calls
if CleveRoids.disabled then return end
-- Mark state
CleveRoids.disabled = true
-- Hand macro display back to ClassicAPI's own #showtooltip parser before we go
-- quiet, or every claimed macro would keep the last value we published.
if CleveRoids.ReleaseDisplays then
CleveRoids.ReleaseDisplays()
end
-- Stop main frame activity
if CleveRoids.Frame then
if CleveRoids.Frame.UnregisterAllEvents then
CleveRoids.Frame:UnregisterAllEvents()
end
if CleveRoids.Frame.SetScript then
CleveRoids.Frame:SetScript("OnEvent", nil)
CleveRoids.Frame:SetScript("OnUpdate", nil)
end
end
-- Neuter all slash commands
if SlashCmdList then
local disabledMsg = function()
CleveRoids.Print("|cffff0000SuperCleveRoidMacros is disabled|r" ..
(reason and (": " .. tostring(reason)) or ""))
end
SlashCmdList.CLEVEROID = disabledMsg
SlashCmdList.CAST = disabledMsg
SlashCmdList.USE = disabledMsg
SlashCmdList.FEEDPET = disabledMsg
SlashCmdList.EQUIP = disabledMsg
SlashCmdList.EQUIPMH = disabledMsg
SlashCmdList.EQUIPOH = disabledMsg
end
-- Try to disable for next login
local addonName = "SuperCleveRoidMacros"
if type(DisableAddOn) == "function" then
pcall(DisableAddOn, addonName)
end
-- Final notice
CleveRoids.Print("|cffff0000Disabled|r - " .. (reason or "Unknown reason"))
CleveRoids.Print("Please install required dependencies and /reload")
end
local frame = CreateFrame("Frame")
frame:RegisterEvent("VARIABLES_LOADED")
frame:SetScript("OnEvent", function()
if not CleveRoidMacros then CleveRoidMacros = {} end
if type(CleveRoidMacros.realtime) ~= "number" then
CleveRoidMacros.realtime = 0
end
if type(CleveRoidMacros.refresh) ~= "number" then
CleveRoidMacros.refresh = 5
end
if type(CleveRoidMacros.macrocheck) ~= "number" then
CleveRoidMacros.macrocheck = 1 -- enabled by default
end
-- The saved realtime setting is only readable now; the unit streams were
-- registered at load assuming event-driven mode.
CleveRoids.ApplyUnitStreamEvents()
end)
-- Queues a full update of all action bars.
-- This is called by game event handlers to avoid running heavy logic inside the event itself.
function CleveRoids.QueueActionUpdate()
if CleveRoidMacros.realtime == 0 then
CleveRoids.isActionUpdateQueued = true
end
end
-- Count of `itemId` in *bags only* (not bank), matched by itemID throughout --
-- locale-independent, no hardcoded item names.
-- PERFORMANCE: uses the CleveRoids.Items cache (itemId -> name -> data) for an
-- O(1) lookup when available, falling back to a bag scan on a cache miss.
function CleveRoids.GetReagentCount(itemId)
if not itemId then return 0 end
local Items = CleveRoids.Items
if Items then
local itemName = Items[itemId]
if itemName then
local itemData = Items[itemName]
if itemData and itemData.count and not itemData.inventoryID then
-- Item is in bags (not equipped), return cached count
return itemData.count
end
end
end
-- Slow path: scan bags by itemID (only on a cache miss)
local total = 0
for bag = 0, 4 do
local slots = GetContainerNumSlots(bag) or 0
for slot = 1, slots do
-- Base itemID straight off the slot (no link string, no regex).
if C_Container.GetContainerItemID(bag, slot) == itemId then
local _, count = GetContainerItemInfo(bag, slot)
total = total + (count or 0)
end
end
end
return total
end
-- Live bag scan for item count, bypassing Items cache to avoid stale counts
-- during bag moves (especially backpack ↔ other bag transitions)
function CleveRoids.GetLiveItemCount(itemName)
if not itemName or itemName == "" then return 0 end
-- Escape Lua pattern special chars in item name to avoid crashes
local wantLower = string.lower(itemName)
local total = 0
for bag = 0, 4 do
local slots = GetContainerNumSlots(bag) or 0
for slot = 1, slots do
local id = C_Container.GetContainerItemID(bag, slot)
if id then
local name = C_Item.GetItemNameByID(id)
if name and string.lower(name) == wantLower then
local _, count = GetContainerItemInfo(bag, slot)
total = total + (count or 0)
end
end
end
end
return total
end
-- Power cost and (first) reagent for a spellbook slot, read from Spell.dbc via
-- ClassicAPI -- no GameTooltip scan, no locale-dependent string parsing. Pass
-- spellId to skip the slot->id resolution when the caller already has it.
-- cost: C_Spell.GetSpellPowerCost returns the *effective* (talent-modified)
-- cost the engine actually charges -- matches the old tooltip value,
-- unlike GetSpellInfo().cost which is base-only.
-- reagent: C_Spell.GetSpellReagents returns { {itemID, count}, ... }; take the
-- first reagent's itemID (Spell.dbc covers reagent spells like Vanish).
-- Returns cost, reagentName (localized, for display), reagentId (for counting).
function CleveRoids.GetSpellCost(spellSlot, bookType, spellId)
if not spellId then
spellId = select(10, GetSpellInfo(spellSlot, bookType))
end
local cost = 0
local reagent, reagentId
if spellId then
local costs = C_Spell.GetSpellPowerCost(spellId)
if costs and costs[1] and costs[1].cost then
cost = costs[1].cost
end
local reagents = C_Spell.GetSpellReagents(spellId)
if reagents and reagents[1] and reagents[1].itemID then
reagentId = reagents[1].itemID
reagent = C_Item.GetItemNameByID(reagentId)
end
end
return cost, (reagent and reagent ~= "" and tostring(reagent) or nil), reagentId
end
function CleveRoids.GetProxyActionSlot(slot)
if not slot then return end
return CleveRoids.actionSlots[slot] or CleveRoids.actionSlots[slot.."()"]
end
-- Resolves nested macro references for #showtooltip propagation
-- If an action is a {MacroName} reference and that macro has #showtooltip,
-- returns the inner macro's active action; otherwise returns nil
-- depth parameter prevents infinite recursion
function CleveRoids.ResolveNestedMacroActive(action, depth)
if not action or not action.action then return nil end
depth = depth or 0
if depth > 5 then return nil end -- prevent infinite recursion
-- Check if this action is a macro reference {MacroName}
local macroName = CleveRoids.GetMacroNameFromAction(action.action)
if not macroName then return nil end
-- Get or parse the inner macro
local innerMacro = CleveRoids.GetMacro(macroName)
if not innerMacro then
innerMacro = CleveRoids.ParseMacro(macroName)
end
if not innerMacro or not innerMacro.actions then return nil end
-- Check if inner macro has #showtooltip (indicated by having a tooltip or action list)
if not innerMacro.actions.tooltip and table.getn(innerMacro.actions.list or {}) == 0 then
return nil
end
-- Run TestForActiveAction on inner macro to get its current active
CleveRoids.TestForActiveAction(innerMacro.actions)
-- If inner macro has an active action, check if it's also a nested macro
if innerMacro.actions.active then
local deeperActive = CleveRoids.ResolveNestedMacroActive(innerMacro.actions.active, depth + 1)
if deeperActive then
return deeperActive
end
return innerMacro.actions.active
end
return nil
end
function CleveRoids.TestForActiveAction(actions)
if not actions then return end
local currentActive = actions.active
local currentSequence = actions.sequence
local hasActive = false
local newActiveAction = nil
local newSequence = nil
local firstUnconditional = nil
-- Use tooltip-only branch when:
-- 1. #showtooltip has an explicit argument (explicitTooltip = true), OR
-- 2. #showtooltip exists but action list is empty (bare "#showtooltip")
-- This ensures explicit tooltips always show their icon/usability correctly.
if actions.tooltip and (actions.explicitTooltip or table.getn(actions.list) == 0) then
-- For explicit tooltips, directly use the tooltip as active action
-- (actions.cmd/args are empty, so TestAction would fail)
if actions.explicitTooltip then
hasActive = true
newActiveAction = actions.tooltip
elseif CleveRoids.TestAction(actions.cmd or "", actions.args or "") then
-- Bare #showtooltip (no arg) - validate with TestAction
hasActive = true
newActiveAction = actions.tooltip
end
-- Resolve nested macro references for #showtooltip propagation
if hasActive and newActiveAction then
local macroName = CleveRoids.GetMacroNameFromAction(actions.tooltip.action)
if macroName then
local resolved = CleveRoids.ResolveNestedMacroActive(actions.tooltip, 0)
if resolved then
newActiveAction = resolved
else
-- Inner macro didn't resolve - no active action
hasActive = false
newActiveAction = nil
end
end
end
else
-- First pass: find first action with conditionals that passes
for _, action in ipairs(actions.list) do
local result, passedClause = CleveRoids.TestAction(action.cmd, action.args)
-- Check if action has conditionals
local _, conditionals = CleveRoids.GetParsedMsg(action.args)
-- Skip this action for icon display if it has the '?' prefix
-- Note: CleveRoids._ignoretooltip is a side-effect flag set by GetParsedMsg
local shouldSkipForIcon = CleveRoids._ignoretooltip == 1
-- break on first action that passes tests (unless it should be skipped for icon)
if result and not shouldSkipForIcon then
hasActive = true
if action.sequence then
newSequence = action.sequence
newActiveAction = CleveRoids.GetCurrentSequenceAction(newSequence)
-- Check if current step is a stop step - if so, skip this sequence for icon
if newActiveAction and newActiveAction.action then
local stepLower = string.lower(CleveRoids.Trim(newActiveAction.action))
if stepLower == "nil" or stepLower == "null" or stepLower == "pass" or stepLower == "noop" then
hasActive = false
newActiveAction = nil
newSequence = nil
end
end
if not newActiveAction then hasActive = false end
else
newActiveAction = action
-- Range/usable checks read the passing group's @unit, which for a
-- multi-group clause need not be group 1's (action.conditionals).
action.activeConditionals = nil
if passedClause and passedClause ~= action.args then
local _, passedConds = CleveRoids.GetParsedMsg(passedClause)
action.activeConditionals = passedConds
end
-- Resolve nested macro references for #showtooltip propagation
-- If inner macro doesn't resolve, continue to next action
local macroName = CleveRoids.GetMacroNameFromAction(action.action)
if macroName then
local resolved = CleveRoids.ResolveNestedMacroActive(action, 0)
if resolved then
newActiveAction = resolved
else
-- Inner macro didn't resolve - try next action
hasActive = false
newActiveAction = nil
end
end
end
if hasActive then break end
end
-- Track first unconditional non-macro action as fallback
if not conditionals and not firstUnconditional and not shouldSkipForIcon then
-- Don't use unresolvable macro refs as fallback
local macroName = CleveRoids.GetMacroNameFromAction(action.action)
if not macroName then
firstUnconditional = action
end
end
end
-- If no conditional action passed, use first unconditional action
if not hasActive and firstUnconditional then
hasActive = true
newActiveAction = firstUnconditional
end
end
-- If the active action has no texture (item wasn't in bags at parse time),
-- resolve it now via live bag lookup so the icon updates immediately
if newActiveAction and not newActiveAction.texture
and not newActiveAction.spell and not newActiveAction.petSpell then
local freshItem = CleveRoids.GetItem(newActiveAction.action)
if freshItem and freshItem.texture then
newActiveAction.item = freshItem
newActiveAction.texture = freshItem.texture
newActiveAction.type = newActiveAction.type or "item"
end
end
local changed = false
if currentActive ~= newActiveAction or currentSequence ~= newSequence then
actions.active = newActiveAction
actions.sequence = newSequence
changed = true
end
if not hasActive then
if actions.active ~= nil or actions.sequence ~= nil then
actions.active = nil
actions.sequence = nil
changed = true
end
return changed
end
if actions.active then
local previousUsable = actions.active.usable
local previousOom = actions.active.oom
local previousInRange = actions.active.inRange
if actions.active.spell then
-- Default to -1 (unknown) so we fall through to proxy slot or original function
-- Only set to 1/0 when we have a definitive answer from IsSpellInRange
actions.active.inRange = -1
-- Enhanced nampower range check with spell ID support
if IsSpellInRange then
local conds = actions.active.activeConditionals or actions.active.conditionals
local unit = conds and conds.target or "target"
if unit == "focus" then
unit = CleveRoids.GetFocusUnitId()
elseif unit == "focustarget" then
local focusUnit = CleveRoids.GetFocusUnitId()
unit = focusUnit and (focusUnit .. "target") or nil
end
local resolvedMark = CleveRoids.ResolveRaidMarkUnit(unit)
if resolvedMark then unit = resolvedMark end
-- PERFORMANCE: Cache spell name construction using two-level cache (no string concat for lookup)
local castName = actions.active.action
if actions.active.spell and actions.active.spell.name then
local spellName = actions.active.spell.name
local rank = actions.active.spell.rank
or (actions.active.spell.highest and actions.active.spell.highest.rank)
if rank and rank ~= "" then
-- Two-level cache: spellNameCache[spellName][rank] = "SpellName(Rank X)"
local nameCache = CleveRoids.spellNameCache[spellName]
if not nameCache then
nameCache = {}
CleveRoids.spellNameCache[spellName] = nameCache
end
castName = nameCache[rank]
if not castName then
castName = spellName .. "(" .. rank .. ")"
nameCache[rank] = castName
end
end
end
-- PERFORMANCE: Try to get spell ID with caching
local spellId = nil
if GetSpellIdForName then
-- Check cache first
local cachedId = CleveRoids.spellIdCache[castName]
if cachedId then
spellId = cachedId
else
spellId = GetSpellIdForName(castName)
if spellId and spellId > 0 then
CleveRoids.spellIdCache[castName] = spellId
end
end
end
-- Check range using API wrapper (handles all cases properly)
local API = CleveRoids.NampowerAPI
local checkValue = spellId and spellId > 0 and spellId or castName
if UnitExists(unit) then
-- We have a target - use API wrapper for proper range checking
-- API.IsSpellInRange handles: native check, -1 for self-cast, UnitXP fallback
if API then
local r = API.IsSpellInRange(checkValue, unit)
if r ~= nil then
actions.active.inRange = r
end
elseif IsSpellInRange then
-- No API, use native directly
local nativeResult = IsSpellInRange(checkValue, unit)
if nativeResult == 0 or nativeResult == 1 then
actions.active.inRange = nativeResult
elseif nativeResult == -1 then
-- Self-cast/ground-targeted spell, always in range
actions.active.inRange = 1
end
end
end
-- No target: don't set inRange, let proxy slot / original behavior handle it
end
-- Check if spell is usable first (handles forms, stances, and power type correctly)
local isUsableBySpell, notEnoughPower = nil, nil
if IsSpellUsable then
-- pcall to handle spells not in spellbook (Nampower throws error)
local ok, usable, oom = pcall(IsSpellUsable, actions.active.action)
if ok then
isUsableBySpell, notEnoughPower = usable, oom
end
end
-- For OOM check, use proper mana source
if notEnoughPower ~= nil then
-- Prefer IsSpellUsable result if available (Nampower)
actions.active.oom = (notEnoughPower == 1)
else
-- Read caster mana directly for druids (the mana slot survives
-- shapeshift, 0 = Enum.PowerType.Mana); other classes use their
-- primary power. Replaces the SuperWoW UnitMana 2nd-return hack.
local manaToCheck
if CleveRoids.playerClass == "DRUID" then
manaToCheck = CleveRoids.ClassicAPI.UnitPower("player", 0)
else
manaToCheck = CleveRoids.ClassicAPI.UnitPower("player")
end
actions.active.oom = (manaToCheck < actions.active.spell.cost)
end
local start, duration = 0, 0
if actions.active.spell.spellSlot then
start, duration = GetSpellCooldown(actions.active.spell.spellSlot, actions.active.spell.bookType)
end
local onCooldown = (start > 0 and duration > 0)
if actions.active.isReactive then
-- For Overpower, Revenge, Riposte: ONLY use combat log tracking
local spellName = actions.active.action
local useCombatLogOnly = (spellName == "Overpower" or spellName == "Revenge" or spellName == "Riposte")
if useCombatLogOnly then
-- Only trust HasReactiveProc for these spells
local hasProc = CleveRoids.HasReactiveProc and CleveRoids.HasReactiveProc(spellName)
if hasProc then
-- Proc is active, show as usable if in range and have enough rage/mana
if actions.active.inRange ~= 0 and not actions.active.oom then
actions.active.usable = 1
elseif pfUI and pfUI.bars and actions.active.oom then
actions.active.usable = 2 -- pfUI: out of mana/rage
else
actions.active.usable = nil
end
else
-- No proc = not usable
actions.active.usable = nil
end
CleveRoids.DebugChanged("usable_" .. spellName,
string.format("|cff00ff00[UPDATE USABLE]|r %s: hasProc=%s, usable=%s",
spellName, tostring(hasProc), tostring(actions.active.usable)))
else
-- For other reactive spells, use the original fallback logic
-- Check combat log-based proc tracking first (stance-independent)
if CleveRoids.HasReactiveProc and CleveRoids.HasReactiveProc(actions.active.action) then
-- Proc is active, show as usable if in range and have enough rage/mana
if actions.active.inRange ~= 0 and not actions.active.oom then
actions.active.usable = 1
elseif pfUI and pfUI.bars and actions.active.oom then
actions.active.usable = 2 -- pfUI: out of mana/rage
else
actions.active.usable = nil
end
-- Use Nampower's IsSpellUsable if available (stance-aware fallback)
elseif IsSpellUsable then
-- pcall to handle spells not in spellbook (Nampower throws error)
local ok, usable, oom = pcall(IsSpellUsable, actions.active.action)
if ok and usable == 1 and oom ~= 1 then
actions.active.usable = (pfUI and pfUI.bars) and nil or 1
else
actions.active.usable = nil
end
actions.active.oom = false
elseif not CleveRoids.IsReactiveUsable(actions.active.action) then
actions.active.oom = false
actions.active.usable = nil
else
actions.active.usable = (pfUI and pfUI.bars) and nil or 1
end
end
elseif isUsableBySpell == 1 and actions.active.inRange ~= 0 then
-- Use IsUsableSpell result if available (handles forms/stances correctly)
actions.active.usable = 1
elseif isUsableBySpell == 0 then
-- IsSpellUsable returned 0 = wrong stance/form, not enough power type, etc.
actions.active.usable = nil
elseif actions.active.inRange ~= 0 and not actions.active.oom then
-- Fallback to mana check ONLY if IsSpellUsable not available
actions.active.usable = 1
-- pfUI:actionbar.lua -- update usable [out-of-range = 1, oom = 2, not-usable = 3, default = 0]
elseif pfUI and pfUI.bars and actions.active.oom then
actions.active.usable = 2
else
actions.active.usable = nil
end
-- Check if this is a toggled buff ability (Prowl, Shadowmeld) and darken if buff is active
-- This must come AFTER all other usability checks to have final say
-- PERFORMANCE: Cache normalized spell name on the action object to avoid gsub per-frame
local spellName = actions.active._normalizedName
if not spellName then
spellName = string.gsub(actions.active.action, "%s*%(.-%)%s*$", "")
spellName = string.gsub(spellName, "_", " ")
actions.active._normalizedName = spellName
end
-- PERFORMANCE: Use cached lookup instead of creating table per-call
if CleveRoids.IsToggledBuffAbility(spellName) then
if CleveRoids.ValidatePlayerBuff(spellName) then
-- Buff is active, darken the icon like "wrong stance" (grayed out, not red)
actions.active.usable = nil
actions.active.oom = false -- Make sure we don't show red tint
end
end
else
actions.active.inRange = 1
actions.active.usable = 1
end
if actions.active.usable ~= previousUsable or
actions.active.oom ~= previousOom or
actions.active.inRange ~= previousInRange then
changed = true
if CleveRoids.debug and actions.active.isReactive then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff00ff[STATE CHANGED]|r %s: usable %s->%s, will send ACTIONBAR_SLOT_CHANGED",
actions.active.action, tostring(previousUsable), tostring(actions.active.usable))
)
end
end
end
return changed
end
--------------------------------------------------------------------------------
-- ClassicAPI macro display
--------------------------------------------------------------------------------
-- The value to hand C_Macro.SetMacroDisplay for an active action. The nested hop
-- mirrors the {MacroName} fallback the GameTooltip.SetAction override used: when
-- `action` is a macro reference, `action.macro` holds the inner macro's
-- #showtooltip action info (see CreateActionInfo), and `action.action` is the
-- literal "{Name}" text, which names no spell or item.
local function DisplayValueFor(action)
if not action then return nil end
if type(action.macro) == "table" and action.macro.action then
return action.macro.action
end
return action.action
end
-- Last value handed to SetMacroDisplay, per macro index.
--
-- Publishing is NOT free. The client repaints every slot holding the macro through
-- its own notifier, and that comes back to us as ACTIONBAR_SLOT_CHANGED ->
-- ClearAction + IndexActionSlot -> TestForActiveAction -> publish. Publishing a value
-- that has not changed therefore re-indexes slots for nothing and feeds itself.
-- GetAction and IndexActionSlot both call in on paths that fire constantly, so the
-- no-op check below is what keeps that from turning into a churn loop.
local publishedDisplay = {}
-- Publish one macro's resolved action. Nothing is re-evaluated for us, so this must
-- be called whenever the answer changes; the published value stands until replaced.
--
-- `false` is deliberate when there is no active action: it means "this macro is mine,
-- nothing matched" and shows the question mark. Skipping the call instead would hand
-- the macro back to ClassicAPI's own #showtooltip parser.
function CleveRoids.PublishDisplay(actions)
if not CleveRoids.ClassicAPIMacroDisplay then return end
local macroID = actions and actions.macroID
if not macroID then return end -- SuperMacro macros have no Blizzard index
local value = DisplayValueFor(actions.active) or false
if publishedDisplay[macroID] == value then return end
publishedDisplay[macroID] = value
C_Macro.SetMacroDisplay(macroID, value)
end
-- Republish every parsed macro. Used after login and after a re-parse, since
-- ClassicAPI re-evaluates nothing for us. Covers macros that aren't on a bar too,
-- which is what keeps the macro window grid's icons correct.
function CleveRoids.PublishAllDisplays()
if not CleveRoids.ClassicAPIMacroDisplay then return end
-- Forget what we published so every macro republishes once. Callers reach here
-- after login and after a re-parse, where a cached value could otherwise
-- suppress the publish a freshly rebuilt macro still needs.
publishedDisplay = {}
for _, macro in pairs(CleveRoids.Macros) do
if type(macro) == "table" and macro.actions and macro.actions.macroID then
CleveRoids.TestForActiveAction(macro.actions)
CleveRoids.PublishDisplay(macro.actions)
end
end
end
-- Hand every macro back to ClassicAPI's own parser and stop claiming ownership.
function CleveRoids.ReleaseDisplays()
if not CleveRoids.ClassicAPIMacroDisplay then return end
-- Sweep the whole index space rather than the macros we know we published:
-- PublishAllDisplays clears that record on every re-parse, so a macro claimed
-- before one and gone after it would keep our last value forever. Releasing a
-- slot we never claimed costs nothing, and this runs once.
for i = 1, CleveRoids.MAX_MACRO_SLOTS do
C_Macro.SetMacroDisplay(i, nil)
end
publishedDisplay = {}
CleveRoids.ClassicAPIMacroDisplay = false
end
-- PERFORMANCE: Static buffer references for hot path
local _actionsToSlotsBuffer = CleveRoids._actionsToSlotsBuffer
local _slotsBuffer = CleveRoids._slotsBuffer
local _actionsListBuffer = CleveRoids._actionsListBuffer
function CleveRoids.TestForAllActiveActions()
-- PERFORMANCE: Reuse static buffers instead of creating new tables each call
-- Group slots by their actions object to handle shared macro references
local actionsToSlots = _actionsToSlotsBuffer
local actionsList = _actionsListBuffer
local actionsCount = 0
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
local Actions = CleveRoids.Actions
local slot, actions = next(Actions)
while slot do
if not actionsToSlots[actions] then
-- Reuse or create slots array from pool
local slots = _slotsBuffer[actions]
if not slots then
slots = {}
_slotsBuffer[actions] = slots
end
slots[1] = slot
slots._count = 1
actionsToSlots[actions] = slots
actionsCount = actionsCount + 1
actionsList[actionsCount] = actions
else
local slots = actionsToSlots[actions]
local count = slots._count + 1
slots[count] = slot
slots._count = count
end
slot, actions = next(Actions, slot)
end
-- Test each unique actions object once and send events to ALL slots that share it
for i = 1, actionsCount do
local actions = actionsList[i]
local slots = actionsToSlots[actions]
local stateChanged = CleveRoids.TestForActiveAction(actions)
if stateChanged then
-- Publishing repaints every slot holding this macro through the client's
-- own notifier, so there is no per-slot fan-out to do here. It no-ops once
-- ReleaseDisplays has handed the macros back.
CleveRoids.PublishDisplay(actions)
end
-- Clear for reuse (reset count and clear buffer reference)
for j = 1, slots._count do
slots[j] = nil
end
slots._count = 0
actionsToSlots[actions] = nil
actionsList[i] = nil -- Clear actionsList entry for reuse
end
end
function CleveRoids.ClearAction(slot)
if not CleveRoids.Actions[slot] then return end
CleveRoids.Actions[slot].active = nil
CleveRoids.Actions[slot] = nil
end
function CleveRoids.GetAction(slot)
if not slot or not CleveRoids.ready then return end
local actions = CleveRoids.Actions[slot]
if actions then return actions end
-- Identify the macro by its index via ClassicAPI's GetActionInfo so the
-- slot is mapped without going through the macro name (no unique-name
-- requirement for action-bar macros).
local macro
local kind, macroID = CleveRoids.ClassicAPI.GetActionInfo(slot)
if kind == "macro" and macroID then
macro = CleveRoids.GetMacroByIndex(macroID)
end
-- Fallback for SuperMacro (and anything GetActionInfo doesn't tag as a
-- macro): resolve by name.
if not macro then
local text = GetActionText(slot)
if text then
macro = CleveRoids.GetMacro(text)
end
end
if macro then
actions = macro.actions
CleveRoids.TestForActiveAction(actions)
CleveRoids.PublishDisplay(actions)
CleveRoids.Actions[slot] = actions
CleveRoids.SendEventForAction(slot, "ACTIONBAR_SLOT_CHANGED", slot)
return actions
end
end
function CleveRoids.GetActiveAction(slot)
local action = CleveRoids.GetAction(slot)
return action and action.active
end
-- PERFORMANCE: Static buffer for arg backup
local _originalArgsBuffer = CleveRoids._originalArgsBuffer
-- Prebuilt global names. Building "arg" .. i inline cost a concatenation per
-- iteration, three ten-iteration loops per call, on a function that runs once per
-- action slot (up to 120 per full index).
local ARG_GLOBALS = {
"arg1", "arg2", "arg3", "arg4", "arg5",
"arg6", "arg7", "arg8", "arg9", "arg10",
}
-- Takes eventArg explicitly rather than `...`: in 1.12's Lua 5.0 a vararg function
-- allocates an `arg` table on every call, and every caller passes exactly one extra
-- value (the slot). The old arg.n fan-out below collapsed to a single case.
function CleveRoids.SendEventForAction(slot, event, eventArg)
local old_this = this
-- PERFORMANCE: Reuse static buffer instead of creating table each call
local original_global_args = _originalArgsBuffer
for i = 1, 10 do
original_global_args[i] = _G[ARG_GLOBALS[i]]
end
_G.arg1 = eventArg
for i = 2, 10 do
_G[ARG_GLOBALS[i]] = nil
end
local button_to_call_event_on
local page = floor((slot - 1) / NUM_ACTIONBAR_BUTTONS) + 1
local pageSlot = slot - (page - 1) * NUM_ACTIONBAR_BUTTONS
if slot >= 73 then
button_to_call_event_on = _G["BonusActionButton" .. pageSlot]
elseif slot >= 61 then
button_to_call_event_on = _G["MultiBarBottomLeftButton" .. pageSlot]
elseif slot >= 49 then
button_to_call_event_on = _G["MultiBarBottomRightButton" .. pageSlot]
elseif slot >= 37 then
button_to_call_event_on = _G["MultiBarLeftButton" .. pageSlot]
elseif slot >= 25 then
button_to_call_event_on = _G["MultiBarRightButton" .. pageSlot]
end
if button_to_call_event_on then
this = button_to_call_event_on
ActionButton_OnEvent(event)
end
if page == CURRENT_ACTIONBAR_PAGE then
local main_bar_button = _G["ActionButton" .. pageSlot]
if main_bar_button and main_bar_button ~= button_to_call_event_on then
this = main_bar_button
ActionButton_OnEvent(event)
elseif not button_to_call_event_on and main_bar_button then
this = main_bar_button
ActionButton_OnEvent(event)
end
end
this = old_this
for i = 1, 10 do
_G[ARG_GLOBALS[i]] = original_global_args[i]
end
-- FIX: Skip addon handler calls during initial indexing to prevent 120 rapid calls
if CleveRoids._suppressActionHandlers then
return
end
local handlers = CleveRoids.actionEventHandlers
for i = 1, table.getn(handlers) do
handlers[i](slot, event, eventArg)
end
end
-- Executes the given Macro's body
-- body: The Macro's body
-- Note on macro flags:
-- stopMacroFlag: Stops ALL remaining lines (propagates to parent macros)
-- skipMacroFlag: Stops only current macro (cleared on return, parent continues)
function CleveRoids.ExecuteMacroBody(body,inline)
local lines = CleveRoids.splitString(body, "\n")
if inline then lines = CleveRoids.splitString(body, "\\n"); end
-- Save parent's stopMacroFlag state (in case we need to preserve it)
local parentStopFlag = CleveRoids.stopMacroFlag
-- Clear flags at start of this macro execution
-- Note: We DON'T clear if parent already set stopMacroFlag (shouldn't execute at all)
if not parentStopFlag then
CleveRoids.stopMacroFlag = false
end
CleveRoids.skipMacroFlag = false -- Always start fresh for skipmacro
if CleveRoids.macroRefDebug then
CleveRoids.Print("|cff00ffff[MacroRef]|r ExecuteMacroBody called with " .. table.getn(lines) .. " lines")
end
for k,v in pairs(lines) do
local trimmed = CleveRoids.Trim(v)
local cmdHandled = false
-- Skip #showtooltip lines - they're only meaningful for action bar display,
-- not for execution. This allows inner macros to have #showtooltip without
-- causing issues when called from other macros.
if string.find(trimmed, "^#showtooltip") then
if CleveRoids.macroRefDebug then
CleveRoids.Print("|cff888888[MacroRef]|r Skipping #showtooltip line in inner macro")
end
cmdHandled = true
end
-- IMPORTANT: Check for /nofirstaction BEFORE the stop flag check
-- This allows /nofirstaction to clear the stopMacroFlag set by /firstaction
local _, _, nofirstactionArgs = string.find(trimmed, "^/nofirstaction%s*(.*)")
if nofirstactionArgs then
-- Capture stopOnCastFlag BEFORE DoNoFirstAction clears it
-- If it was true, stopMacroFlag was set by /firstaction, not /stopmacro
local wasFirstActionActive = CleveRoids.stopOnCastFlag
CleveRoids.DoNoFirstAction(nofirstactionArgs)
-- Also clear stopMacroFlag if it was set by firstaction mechanism
-- (but NOT if it was set by explicit /stopmacro)
if wasFirstActionActive and CleveRoids.stopMacroFlag then
-- stopOnCastFlag was true before we cleared it, and stopMacroFlag is set
-- This means stopMacroFlag was set by the firstaction mechanism, clear it
CleveRoids.stopMacroFlag = false
if CleveRoids.macroRefDebug then
CleveRoids.Print("|cff00ff00[MacroRef]|r /nofirstaction cleared stopMacroFlag - resuming macro")
end
end
if CleveRoids.macroRefDebug then
CleveRoids.Print("|cff88ff88[MacroRef]|r Executing line " .. k .. ": " .. string.sub(v, 1, 60))
end
cmdHandled = true
end
-- Check both macro stop flags before each line (but skip if we just handled /nofirstaction)
-- stopMacroFlag: stop this AND parent macros
-- skipMacroFlag: stop only this macro (parent continues)
if not cmdHandled and (CleveRoids.stopMacroFlag or CleveRoids.skipMacroFlag) then
-- If stopOnCastFlag is true, the stop was caused by the firstaction mechanism
-- Don't break the loop - just skip this line (allows /nofirstaction to be reached)
if CleveRoids.stopOnCastFlag and CleveRoids.stopMacroFlag and not CleveRoids.skipMacroFlag then
if CleveRoids.macroRefDebug then
CleveRoids.Print("|cffff8800[MacroRef]|r Skipping line " .. k .. " (firstaction mode)")
end
cmdHandled = true -- Skip this line but continue loop to allow /nofirstaction
else
-- This is a /stopmacro or /skipmacro stop - actually break
if CleveRoids.macroRefDebug then
local reason = CleveRoids.stopMacroFlag and "/stopmacro" or "/skipmacro"
CleveRoids.Print("|cffff8800[MacroRef]|r Stopped at line " .. k .. " due to " .. reason)
end
break
end
end
if not cmdHandled then
if CleveRoids.macroRefDebug then
CleveRoids.Print("|cff88ff88[MacroRef]|r Executing line " .. k .. ": " .. string.sub(v, 1, 60))
end
-- IMPORTANT: Handle macro control commands directly to bypass Blizzard's built-in /stopmacro
-- Blizzard intercepts bare /stopmacro before it reaches our SlashCmdList handler
-- Check for /stopmacro (with or without conditionals)
local _, _, stopmacroArgs = string.find(trimmed, "^/stopmacro%s*(.*)")
if stopmacroArgs then
CleveRoids.DoStopMacro(stopmacroArgs)
cmdHandled = true
end
-- Check for /skipmacro (with or without conditionals)
if not cmdHandled then
local _, _, skipmacroArgs = string.find(trimmed, "^/skipmacro%s*(.*)")
if skipmacroArgs then
CleveRoids.DoSkipMacro(skipmacroArgs)
cmdHandled = true
end
end
-- Check for /firstaction (with or without conditionals)
if not cmdHandled then
local _, _, firstactionArgs = string.find(trimmed, "^/firstaction%s*(.*)")
if firstactionArgs then
CleveRoids.DoFirstAction(firstactionArgs)
cmdHandled = true
end
end
-- FIX: Handle /startattack directly - not a native vanilla WoW command
-- Without SuperMacro, ChatEdit_SendText may not route addon slash commands properly
if not cmdHandled then
local _, _, startattackArgs = string.find(trimmed, "^/startattack%s*(.*)")
if startattackArgs then
if SlashCmdList.STARTATTACK then
SlashCmdList.STARTATTACK(startattackArgs)
end
cmdHandled = true
end
end
-- FIX: Handle /stopattack directly - not a native vanilla WoW command
if not cmdHandled then
local _, _, stopattackArgs = string.find(trimmed, "^/stopattack%s*(.*)")
if stopattackArgs then
if SlashCmdList.STOPATTACK then
SlashCmdList.STOPATTACK(stopattackArgs)
end
cmdHandled = true
end
end
end
-- For all other commands, use ChatEdit_SendText
if not cmdHandled then
ChatFrameEditBox:SetText(v)
ChatEdit_SendText(ChatFrameEditBox)
end
end
-- Clear skipMacroFlag after this macro completes (don't propagate to parent)
-- stopMacroFlag is intentionally LEFT SET so it propagates up
CleveRoids.skipMacroFlag = false
return true
end
-- Gets the body of the Macro with the given name
-- name: The name of the Macro
-- returns: The body of the macro
function CleveRoids.GetMacroBody(name)
local macro = CleveRoids.GetMacro(name)
return macro and macro.body
end
-- Attempts to execute a macro by the given name or index (Blizzard or Super tab)
-- Returns: true if something was executed, false otherwise
-- Supports: macro name (string), macro index (number or numeric string like "19")
-- Uses CleveRoids' own ExecuteMacroBody for processing (supports enhanced syntax)
function CleveRoids.ExecuteMacroByName(name)
if not name or name == "" then
if CleveRoids.macroRefDebug then
CleveRoids.Print("|cffff0000[MacroRef]|r ExecuteMacroByName called with empty name")
end
return false
end
if CleveRoids.macroRefDebug then
CleveRoids.Print("|cff00ffff[MacroRef]|r ExecuteMacroByName called with: '" .. name .. "'")
end
local body
local source = nil
-- Check if input is a numeric index (e.g., "19" or 19)
local numericIndex = tonumber(name)
-- 1) Try Blizzard macro (by index or name)
if numericIndex then
-- Numeric index - get body directly (supports character macros 19-36)
local _n, _tex, b = GetMacroInfo(numericIndex)
if b and b ~= "" then
body = b
source = "Blizzard (index " .. numericIndex .. ")"
end
else
-- String name - look up by name
local id = GetMacroIndexByName(name)
if CleveRoids.macroRefDebug then
CleveRoids.Print("|cff888888[MacroRef]|r GetMacroIndexByName('" .. name .. "') = " .. tostring(id))
end
if id and id ~= 0 then
local _n, _tex, b = GetMacroInfo(id)
if b and b ~= "" then
body = b
source = "Blizzard (slot " .. id .. ")"
end
end
end
-- 2) SuperMacro's Super macros (by name only, not index)
-- These are the 7000-char extended macros stored in SM_SUPER
if not body and not numericIndex then
if type(GetSuperMacroInfo) == "function" then
local _n2, _t2, b2 = GetSuperMacroInfo(name)
if b2 and b2 ~= "" then
body = b2
source = "SuperMacro"
end
end
end
-- 3) CRM cache fallback (by name only)
if not body and not numericIndex then
if type(CleveRoids.GetMacro) == "function" then
local m = CleveRoids.GetMacro(name)
if m and m.body and m.body ~= "" then
body = m.body
source = "CRM cache"
end
end
end
if not body or body == "" then
if CleveRoids.macroRefDebug then
CleveRoids.Print("|cffff0000[MacroRef]|r Macro '" .. name .. "' not found in any source")
end
return false
end
if CleveRoids.macroRefDebug then
CleveRoids.Print("|cff00ff00[MacroRef]|r Found macro '" .. name .. "' from " .. source)
end
-- Execute using CleveRoids' processor (supports conditionals, extended syntax)
return CleveRoids.ExecuteMacroBody(body)
end
function CleveRoids.SetHelp(conditionals)
if conditionals.harm then
conditionals.help = false
end
end
function CleveRoids.FixEmptyTarget(conditionals)
if not conditionals.target then
if UnitExists("target") then
conditionals.target = "target"
elseif GetCVar("autoSelfCast") == "1" and not conditionals.target == "help" then
conditionals.target = "player"
end
end
return false
end
-- Fixes the conditionals' target by targeting the target with the given name
-- conditionals: The conditionals containing the current target
-- name: The name of the player to target
-- hook: The target hook
-- returns: Whether or not we've changed the player's current target
function CleveRoids.FixEmptyTargetSetTarget(conditionals, name, hook)
if not conditionals.target then
hook(name)
conditionals.target = "target"
return true
end
return false
end
-- Returns the resolved token, or nil when no focus is set so @focus clauses
-- silently fall through to the next macro alternative (no warning spam).
function CleveRoids.GetFocusUnitId()
if UnitExists("focus") then
return "focus"
end
return nil
end
function CleveRoids.GetMacroNameFromAction(text)
if string.sub(text, 1, 1) == "{" and string.sub(text, -1) == "}" then
local name
if string.sub(text, 2, 2) == "\"" and string.sub(text, -2, -2) == "\"" then
return string.sub(text, 3, -3)
else
return string.sub(text, 2, -2)
end
end
end
function CleveRoids.CreateActionInfo(action, conditionals)
local _, _, text = string.find(action, "!?%??~?(.*)")
local spell = CleveRoids.GetSpell(text)
local petSpell -- Add this line
local item, macroName, macro, macroTooltip, actionType, texture
-- NEW: Check if the action is a slot number
local slotId = tonumber(text)
if slotId and slotId >= 1 and slotId <= 19 then
actionType = "item"
local itemTexture = GetInventoryItemTexture("player", slotId)
if itemTexture then
texture = itemTexture
else
texture = CleveRoids.unknownTexture
end
else
-- Original logic for named items and spells
if not spell then
petSpell = CleveRoids.GetPetSpell(text) -- Add this line
end
if not spell and not petSpell then -- Modify this line
item = CleveRoids.GetItem(text)
end
if not item and not petSpell then -- Modify this line
macroName = CleveRoids.GetMacroNameFromAction(text)
macro = CleveRoids.GetMacro(macroName)
macroTooltip = (macro and macro.actions) and macro.actions.tooltip
end
if spell then
actionType = "spell"
texture = spell.texture or CleveRoids.unknownTexture
elseif petSpell then -- Add this block
actionType = "petspell"
texture = petSpell.texture or CleveRoids.unknownTexture
elseif item then
actionType = "item"
texture = (item and item.texture) or CleveRoids.unknownTexture
elseif macro then
actionType = "macro"
texture = (macro.actions and macro.actions.tooltip and macro.actions.tooltip.texture)
or (macro and macro.texture)
or CleveRoids.unknownTexture
end
end
local info = {
action = text,
item = item,
spell = spell,
petSpell = petSpell, -- Add this line
macro = macroTooltip,
type = actionType,
texture = texture,
conditionals = conditionals,
}
return info
end
function CleveRoids.SplitCommandAndArgs(text)
local _, _, cmd, args = string.find(text, "(/%w+%s?)(.*)")
if cmd and args then
cmd = CleveRoids.Trim(cmd)
text = CleveRoids.Trim(args)
end
return cmd, args
end
function CleveRoids.ParseSequence(text)
if not text or text == "" then return end
-- normalize commas
local args = string.gsub(text, "(%s*,%s*)", ",")
-- optional [conditionals] block
local _, condEnd, condBlock = string.find(args, "(%[.*%])")
-- accept reset= anywhere; no trailing space required; strip it out once
local _, _, resetVal = string.find(args, "[Rr][Ee][Ss][Ee][Tt]=([%w/]+)")
if resetVal then
args = string.gsub(args, "%s*[Rr][Ee][Ss][Ee][Tt]=[%w/]+%s*", " ", 1)
end
-- actions are what's left after any ] (if present)
local actionSeq = CleveRoids.Trim((condEnd and string.sub(args, condEnd + 1)) or args)
args = (condBlock or "") .. actionSeq
if actionSeq == "" then return end
local sequence = {
index = 1,
reset = {},
status = 0,
lastUpdate = 0,
args = args,
cond = condBlock or "", -- Store conditional block for evaluation in DoCastSequence
list = {},
perTargetState = {}, -- [guid] = {index = N, lastUpdate = T} for reset=target
}
-- fill reset rules: seconds or flags (target/combat/alt/ctrl/shift)
if resetVal then
for _, rule in pairs(CleveRoids.Split(resetVal, "/")) do
rule = string.lower(CleveRoids.Trim(rule))
local secs = tonumber(rule)
if secs and secs > 0 then
sequence.reset.secs = secs
else
sequence.reset[rule] = true
end
end
end
-- build steps
for _, a in ipairs(CleveRoids.Split(actionSeq, ",")) do
local sa = CleveRoids.CreateActionInfo(CleveRoids.GetParsedMsg(a))
table.insert(sequence.list, sa)
end
CleveRoids.Sequences[text] = sequence
return sequence
end
-- Shared: build, parse, and cache a macro under `cacheKey`. For action-bar
-- macros the key is the Blizzard macro index (unique), so duplicate or blank
-- macro names no longer collide; SuperMacro macros (no index) are keyed by name.
-- macroID is the Blizzard index if known, else nil.
local function BuildMacro(cacheKey, macroID, name, texture, body)
local macro = {
id = macroID,
name = name,
texture = texture,
body = body,
actions = {},
}
-- Back-reference so the update loop, which walks `actions` objects rather than
-- macros, can name the macro to C_Macro.SetMacroDisplay. nil for SuperMacro
-- macros, which have no Blizzard index and so cannot be published.
macro.actions.macroID = macroID
macro.actions.list = {}
-- build a list of testable actions for the macro
local hasShowTooltip = false
local showTooltipHasArg = false
for i, line in ipairs(CleveRoids.splitString(body, "\n")) do
line = CleveRoids.Trim(line)
local cmd, args = CleveRoids.SplitCommandAndArgs(line)
-- check for #showtooltip on first line
if i == 1 then
local _, _, st, _, tt = string.find(line, "(#showtooltip)(%s?(.*))")
if st then
hasShowTooltip = true
tt = CleveRoids.Trim(tt)
-- #showtooltip with explicit item/spell/macro specified
if tt ~= "" then
showTooltipHasArg = true
for _, arg in ipairs(CleveRoids.splitStringIgnoringQuotes(tt)) do
-- Parse the arg to extract the spell name from any conditionals
local parsedArg = CleveRoids.GetParsedMsg(arg)
macro.actions.tooltip = CleveRoids.CreateActionInfo(parsedArg)
local action = CleveRoids.CreateActionInfo(parsedArg)
action.cmd = "/cast"
action.args = arg
action.isReactive = CleveRoids.reactiveSpells[action.action]
table.insert(macro.actions.list, action)
end
end
-- Continue parsing remaining lines for action list (needed for nested macro resolution)
end
-- No #showtooltip: still continue parsing to build action list for inner macro resolution
else
-- make sure we have a testable action
if line ~= "" and args ~= "" and CleveRoids.dynamicCmds[cmd] then
for _, arg in ipairs(CleveRoids.splitStringIgnoringQuotes(args)) do
local action = CleveRoids.CreateActionInfo(CleveRoids.GetParsedMsg(arg))
if cmd == "/castsequence" then
local sequence = CleveRoids.GetSequence(args)
if sequence then
action.sequence = sequence
end
end
action.cmd = cmd
action.args = arg
action.isReactive = CleveRoids.reactiveSpells[action.action]
table.insert(macro.actions.list, action)
end
end
end
end
-- If #showtooltip was present but had no argument, use the first action as the tooltip
if hasShowTooltip and not showTooltipHasArg and table.getn(macro.actions.list) > 0 then
macro.actions.tooltip = macro.actions.list[1]
end
-- Store whether #showtooltip had an explicit argument (for icon fallback logic)
macro.actions.explicitTooltip = showTooltipHasArg
CleveRoids.Macros[cacheKey] = macro
return macro
end
-- Parse a macro by its Blizzard macro index (1-36), caching by index. This is
-- the action-bar path: a slot's macro is identified by index via ClassicAPI's
-- GetActionInfo, so duplicate/blank macro names no longer collide.
function CleveRoids.ParseMacroByIndex(macroID)
if not macroID or macroID == 0 then return end
local name, texture, body = GetMacroInfo(macroID)
if not texture or not body then return end
return BuildMacro(macroID, macroID, name, texture, body)
end
-- Parse a macro by name (explicit {MacroName} / runmacro references, and
-- SuperMacro macros that have no Blizzard index). Resolves to the index path
-- when possible so the cache stays index-keyed.
function CleveRoids.ParseMacro(name)
if not name then return end
local macroID = GetMacroIndexByName(name)
if macroID and macroID ~= 0 then
return CleveRoids.ParseMacroByIndex(macroID)
end
-- SuperMacro has no Blizzard macro index; cache by name.
if GetSuperMacroInfo then
local _, texture, body = GetSuperMacroInfo(name)
if texture and body then
return BuildMacro(name, nil, name, texture, body)
end
end
end
function CleveRoids.ParseMsg(msg)
if not msg then return end
local conditionals = {}
-- reset side flag for this parse
CleveRoids._ignoretooltip = 0
-- strip optional leading '?' and remember how many we stripped
local ignorecount
msg, ignorecount = string.gsub(CleveRoids.Trim(msg), "^%?", "")
conditionals.ignoretooltip = ignorecount
CleveRoids._ignoretooltip = ignorecount
-- Leading [group] blocks and the action after them. A clause with several
-- groups (`[a][b] Spell`) parses here as group 1 plus the shared action:
-- DoWithConditionals and TestAction expand such a clause into one-group
-- variants before parsing, so the callers that do parse a multi-group
-- clause directly only rely on the action and on conditionals being non-nil.
local _, groups, restStart = CleveRoids.ScanBracketGroups(msg)
local conditionBlock = nil
if groups.n > 0 then
conditionBlock = string.sub(groups[1], 2, -2)
end
-- split off flags/action after the groups (or from the start if there are none)
local _, _, noSpam, cancelAura, action = string.find(
groups.n > 0 and string.sub(msg, restStart) or msg,
"^%s*(!?)(~?)([^!~]+.*)"
)
action = CleveRoids.Trim(action or "")
-- Also check for ? at start of action (after conditional block)
-- This handles "[cond] ?action" syntax where ? appears after conditionals
if string.sub(action, 1, 1) == "?" then
action = CleveRoids.Trim(string.sub(action, 2))
conditionals.ignoretooltip = 1
CleveRoids._ignoretooltip = 1
end
-- store the raw action for callers and strip trailing "(Rank X)" for comparisons
conditionals.action = action
action = CleveRoids.StripRank(action)
-- IMPORTANT: if there's NO conditional block, return nil conditionals so
-- DoWithConditionals will hit the {macroName} execution branch.
if not conditionBlock then
local hasFlag = (noSpam and noSpam ~= "") or (cancelAura and cancelAura ~= "")
if hasFlag and action ~= "" then
if noSpam ~= "" then
-- ! prefix: handled at execution time via CastSpellNoToggle
-- (see DoWithConditionals), no gate conditional needed.
conditionals.noSpam = true
end
if cancelAura ~= "" then
conditionals.cancelaura = action
end
return conditionals.action, conditionals
end
return conditionals.action, nil
end
-- With a condition block present, build out the conditionals table
-- ! prefix with explicit conditionals: store as a flag for execution-time anti-toggle,
-- rather than injecting an implicit conditional that gets AND'd with user conditions.
-- The anti-toggle check happens in DoWithConditionals before CastSpellByName.
if noSpam and noSpam ~= "" then
conditionals.noSpam = true
end
if cancelAura and cancelAura ~= "" then
conditionals.cancelaura = action
end
-- Set the action's target to @unitid if found (e.g., @mouseover)
local _, _, target = string.find(conditionBlock, "(@[^%s,]+)")
if target then
conditionBlock = CleveRoids.Trim(string.gsub(conditionBlock, target, ""))
conditionals.target = string.sub(target, 2)
end
if conditionBlock and conditionals.action then
-- Split the conditional block by comma or space
for _, conditionGroups in CleveRoids.splitStringIgnoringQuotes(conditionBlock, {",", " "}) do
if conditionGroups ~= "" then
-- Split conditional groups by colon (rejoin extra parts for multi-part args
-- e.g., distance:30:facing>1 → condition="distance", args="30:facing>1")
local conditionGroup = CleveRoids.splitStringIgnoringQuotes(conditionGroups, ":")
local condition = conditionGroup[1]
-- Rewrite deprecated names (e.g. mouseuse -> cursor) so old macros
-- keep working and only the current keyword exists downstream.
condition = CleveRoids.conditionalAliases[condition] or condition
local args = conditionGroup[2]
for _cgi = 3, table.getn(conditionGroup) do
args = (args or "") .. ":" .. conditionGroup[_cgi]
end
-- No args → the action is the implicit argument
if not args or args == "" then
if not conditionals[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
-- Create first group for non-boolean conditionals
if not conditionals._groups then
conditionals._groups = {}
end
conditionals._groups[condition] = { { values = { conditionals.action }, n = 1, operator = "OR" }, n = 1 }
end
else
-- existing code for when conditionals[condition] already exists
if type(conditionals[condition]) ~= "table" then
conditionals[condition] = { conditionals[condition] }
end
table.insert(conditionals[condition], conditionals.action)
-- Multiple instances of same conditional = AND logic
if not conditionals._operators then
conditionals._operators = {}
end
conditionals._operators[condition] = "AND"
-- Add new group for this instance
if not conditionals._groups then
conditionals._groups = {}
end
if not conditionals._groups[condition] then
conditionals._groups[condition] = { n = 0 }
end
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.
-- Track if this conditional already existed (repeated via comma)
local conditionAlreadyExists = conditionals[condition] ~= nil
if not conditionals[condition] then
conditionals[condition] = {}
elseif type(conditionals[condition]) ~= "table" then
conditionals[condition] = { conditionals[condition] }
end
-- Detect which separator is used: / (OR) or & (AND)
-- Initialize metadata tables if needed
if not conditionals._operators then
conditionals._operators = {}
end
if not conditionals._groups then
conditionals._groups = {}
end
-- Check which separator is present in the CURRENT args
local hasSlash = string.find(args, "/")
local hasAmpersand = string.find(args, "&")
-- Detect if & is part of a multi-comparison pattern (e.g., >0&<10)
-- Pattern: operator+number followed by & (with optional whitespace) followed by operator
-- IMPORTANT: Only whitespace allowed around &, NOT letters (to distinguish from Rip>3&Rake>3)
local isMultiComparison = hasAmpersand and string.find(args, "[>~=<]+%d+%.?%d*%s*&%s*[>~=<]")
local separator = "/"
local operatorType = "OR"
if hasAmpersand and not hasSlash and not isMultiComparison then
separator = "&"
operatorType = "AND"
elseif hasAmpersand and hasSlash then
-- Both separators present - default to / (OR) and warn
-- Could add a warning here in the future
separator = "/"
operatorType = "OR"
end
-- Store the operator type for this conditional (for backwards compat)
-- Note: when groups exist, Multi/NegatedMulti will use per-group operators
conditionals._operators[condition] = operatorType
-- Create a new group for this conditional instance
-- Structure: { values = { ... }, operator = "OR" or "AND" }
if not conditionals._groups[condition] then
conditionals._groups[condition] = { n = 0 }
end
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
local processed_arg = CleveRoids.Trim(arg_item)
processed_arg = string.gsub(processed_arg, '"', "")
processed_arg = string.gsub(processed_arg, "_", " ")
processed_arg = CleveRoids.Trim(processed_arg)
-- normalize "name#N" → "name=#N" and "#N" → "=#N"
local arg_for_find = processed_arg
arg_for_find = string.gsub(arg_for_find, "^#(%d+)$", "=#%1")
arg_for_find = string.gsub(arg_for_find, "([^>~=<]+)#(%d+)", "%1=#%2")
-- accept decimals too; capture name/op/amount
local _, _, name, operator, amount = string.find(arg_for_find, "([^>~=<]*)([>~=<]+)(#?%d*%.?%d+)")
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"|cffff9900[Parse]|r cond=%s arg='%s' find='%s' name=%s op=%s amt=%s",
tostring(condition), tostring(processed_arg), tostring(arg_for_find),
tostring(name), tostring(operator), tostring(amount)
))
end
if not operator or not amount then
-- No operator found, treat as simple string argument
table.insert(conditionals[condition], 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, "#", "")
local should_check_stacks = (num_replacements == 1)
-- SPECIAL HANDLING FOR CONDITIONALS WITH MULTIPLE COMPARISONS
-- Detect if this conditional has multiple operators
-- Example: "ap>1800/<2200" or "Recently_Bandaged>0&<10" or "health>50&<80"
-- Works with ANY conditional that supports numeric operators
-- Pattern: only whitespace or separators allowed between comparisons, NOT letters
if string.find(processed_arg, "[>~=<]+%d+%.?%d*%s*[/&]%s*[>~=<]") then
-- This arg has multiple comparisons, parse them all
local stat_name = name_to_use
local comparisons = {}
-- Extract all operator+number pairs
-- Pattern matches operator followed by optional decimal number
for op, num in string.gfind(processed_arg, "([>~=<]+)(#?%d*%.?%d+)") do
local clean_num = string.gsub(num, "#", "")
local check_stacks = (string.find(num, "#") ~= nil)
table.insert(comparisons, {
operator = op,
amount = tonumber(clean_num),
checkStacks = check_stacks
})
end
if table.getn(comparisons) > 0 then
local entry = {
name = CleveRoids.Trim(stat_name),
comparisons = comparisons -- Store all comparisons
}
table.insert(conditionals[condition], entry)
currentGroup.n = currentGroup.n + 1
currentGroup.values[currentGroup.n] = entry
else
-- Fallback to single comparison if parsing failed
local entry = {
name = CleveRoids.Trim(name_to_use),
operator = operator,
amount = tonumber(final_amount_str),
checkStacks = should_check_stacks
}
table.insert(conditionals[condition], entry)
currentGroup.n = currentGroup.n + 1
currentGroup.values[currentGroup.n] = entry
end
else
-- Normal single-comparison conditional (existing behavior)
local entry = {
name = CleveRoids.Trim(name_to_use),
operator = operator,
amount = tonumber(final_amount_str),
checkStacks = should_check_stacks
}
table.insert(conditionals[condition], entry)
currentGroup.n = currentGroup.n + 1
currentGroup.values[currentGroup.n] = entry
end
end
end
end
end
end
return conditionals.action, conditionals
end
end
-- Get previously parsed or parse, store and return
function CleveRoids.GetParsedMsg(msg)
if not msg then return end
-- PERFORMANCE: Check cache first before doing string operations
local cached = CleveRoids.ParsedMsg[msg]
if cached then
-- Use cached ignoretooltip value (already computed during first parse)
CleveRoids._ignoretooltip = cached.ignoretooltip or 0
return cached.action, cached.conditionals
end
-- Only compute ignoretooltip for new messages (not cache hits)
-- PERFORMANCE: Use string.sub for simple prefix check instead of gsub
local trimmed = CleveRoids.Trim(msg)
local ignorecount = (string.sub(trimmed, 1, 1) == "?") and 1 or 0
CleveRoids._ignoretooltip = ignorecount
local action, conditionals = CleveRoids.ParseMsg(msg)
-- Use ParseMsg's final _ignoretooltip value for cache, not the pre-computed one.
-- ParseMsg may detect ? after conditional blocks (e.g., "[cond] ?action")
-- which the prefix check above would miss.
local finalIgnore = CleveRoids._ignoretooltip
CleveRoids.ParsedMsg[msg] = {
action = action,
conditionals = conditionals,
ignoretooltip = finalIgnore,
}
return action, conditionals
end
function CleveRoids.GetMacro(name)
return CleveRoids.Macros[name] or CleveRoids.ParseMacro(name)
end
-- Get a parsed macro by its Blizzard macro index (1-36). Used by the action-bar
-- path so macros are identified by slot/index rather than name.
function CleveRoids.GetMacroByIndex(macroID)
if not macroID then return end
return CleveRoids.Macros[macroID] or CleveRoids.ParseMacroByIndex(macroID)
end
function CleveRoids.GetSequence(args)
return CleveRoids.Sequences[args] or CleveRoids.ParseSequence(args)
end
function CleveRoids.GetCurrentSequenceAction(sequence)
return sequence.list[sequence.index]
end
function CleveRoids.ResetSequence(sequence)
-- Save the GUID before clearing it
local targetGuid = sequence.lastTargetGuid
sequence.index = 1
sequence.lastTargetGuid = nil -- Clear stored GUID so next use captures fresh target
-- Also reset per-target state for current target if tracking
if sequence.reset and sequence.reset.target and targetGuid then
if sequence.perTargetState then
sequence.perTargetState[targetGuid] = nil
end
end
end
function CleveRoids.AdvanceSequence(sequence)
if sequence.index < table.getn(sequence.list) then
-- Not at the end yet, just advance normally
local oldIndex = sequence.index
sequence.index = sequence.index + 1
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"|cff00ff00[Sequence Advanced]|r %d -> %d (of %d)",
oldIndex, sequence.index, table.getn(sequence.list)
))
end
-- Sync per-target state if tracking by target
if sequence.reset and sequence.reset.target and sequence.lastTargetGuid then
sequence.perTargetState = sequence.perTargetState or {}
sequence.perTargetState[sequence.lastTargetGuid] = {
index = sequence.index,
lastUpdate = GetTime(),
}
end
else
-- Sequence completed all steps - always wrap back to step 1.
-- reset=secs/target/combat only apply as mid-sequence idle resets
-- (matching standard WoW castsequence behavior).
CleveRoids.ResetSequence(sequence)
end
end
function CleveRoids.TestAction(cmd, args)
-- `[a][b] Spell`: first passing group wins. The second return names the
-- clause that passed so the display path can read that group's @unit.
local variants = CleveRoids.ExpandBracketGroups(args)
if variants then
for i = 1, variants.n do
local r, passed = CleveRoids.TestAction(cmd, variants[i])
if r then return r, passed end
end
return
end
local msg, conditionals = CleveRoids.GetParsedMsg(args)
-- Nil-safe guards
local hasShowTooltip = (type(msg) == "string") and string.find(msg, "#showtooltip")
local ignoreTooltip = ((type(conditionals) == "table") and (conditionals.ignoretooltip == 1))
or (CleveRoids._ignoretooltip == 1)
-- If the line is explicitly ignored for tooltip (via '?'),
-- or it is a '#showtooltip' token, do not contribute icon/tooltip.
if hasShowTooltip or ignoreTooltip then
CleveRoids._ignoretooltip = 0 -- clear for next parse
return
end
-- No [] block → return a testable token so the UI can pick a texture
if not conditionals then
if not msg or msg == "" then
return
else
return CleveRoids.GetMacroNameFromAction(msg) or msg
end
end
local origTarget = conditionals.target
if cmd == "" or not CleveRoids.dynamicCmds[cmd] then
return
end
-- ClassicAPI's native focus token. No focus set: the clause fails quietly,
-- exactly as the cast path does, so display and execution agree.
if conditionals.target == "focus" or conditionals.target == "focustarget" then
local focusUnitId = CleveRoids.GetFocusUnitId()
if not focusUnitId then return end
conditionals.target = focusUnitId .. (conditionals.target == "focustarget" and "target" or "")
end
if conditionals.target == "mouseover" then
if not CleveRoids.IsValidTarget("mouseover", conditionals.help) then
return false
end
end
-- Resolve named raid marks (skull/cross/etc.) and mark1-mark8 to native mark# unit tokens
if conditionals.target then
local resolvedMark = CleveRoids.ResolveRaidMarkUnit(conditionals.target)
if resolvedMark then conditionals.target = resolvedMark end
end
CleveRoids.FixEmptyTarget(conditionals)
-- Flag for tooltip evaluation. CountEnemiesMatching skips expensive
-- nameplate scan and known-enemy cache, using only target + unit tokens.
CleveRoids._isTestingAction = true
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
local k, v = next(conditionals)
while k do
if not CleveRoids.ignoreKeywords[k] then
if not CleveRoids.Keywords[k] or not CleveRoids.Keywords[k](conditionals) then
CleveRoids._isTestingAction = false
conditionals.target = origTarget
return
end
end
k, v = next(conditionals, k)
end
CleveRoids._isTestingAction = false
conditionals.target = origTarget
return CleveRoids.GetMacroNameFromAction(msg) or msg, args
end
function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBeforeAction, action)
-- Ensure TestAction flag is cleared for actual execution (safety net for error recovery)
CleveRoids._isTestingAction = false
-- Check macro stop flags (skip non-control commands when flag is set)
-- This enables /stopmacro, /skipmacro, /firstaction, /nofirstaction to work without SuperMacro for vanilla macros
if (CleveRoids.stopMacroFlag or CleveRoids.skipMacroFlag) and action ~= "STOPMACRO" and action ~= "SKIPMACRO" and action ~= "FIRSTACTION" and action ~= "NOFIRSTACTION" then
return false
end
-- `[a][b] Spell`: try each group as its own clause, first pass wins -- the
-- same walk DoCast makes over `[a] Spell; [b] Spell`.
local variants = CleveRoids.ExpandBracketGroups(msg)
if variants then
for i = 1, variants.n do
local r = CleveRoids.DoWithConditionals(variants[i], hook, fixEmptyTargetFunc, targetBeforeAction, action)
if r then return r end
end
return false
end
local msg, conditionals = CleveRoids.GetParsedMsg(msg)
-- Debug: Log parsed msg and action type
if CleveRoids.equipDebugLog and action and action ~= CastSpellByName then
CleveRoids.Print("|cff888888[EquipLog] DoWithConditionals: parsed msg='" .. tostring(msg) .. "' action=" .. tostring(action) .. "|r")
end
-- No conditionals. Just exit.
if not conditionals then
if not msg then -- if not even an empty string
return false
else
if string.sub(msg, 1, 1) == "{" and string.sub(msg, -1) == "}" then
if string.sub(msg, 2, 2) == "\"" and string.sub(msg, -2, -2) == "\"" then
return CleveRoids.ExecuteMacroBody(string.sub(msg, 3, -3), true)
else
return CleveRoids.ExecuteMacroByName(string.sub(msg, 2, -2))
end
end
-- Handle STOPMACRO/SKIPMACRO/FIRSTACTION/NOFIRSTACTION without conditionals (bare command)
if action == "STOPMACRO" then
CleveRoids.stopMacroFlag = true
return true
elseif action == "SKIPMACRO" then
CleveRoids.skipMacroFlag = true
return true
elseif action == "FIRSTACTION" then
CleveRoids.stopOnCastFlag = true
return true
elseif action == "NOFIRSTACTION" then
-- Clear flags - same logic as DoNoFirstAction
local wasFirstActionActive = CleveRoids.stopOnCastFlag
CleveRoids.stopOnCastFlag = false
if wasFirstActionActive and CleveRoids.stopMacroFlag then
CleveRoids.stopMacroFlag = false
end
return true
end
if hook then
hook(msg)
end
return true
end
end
if conditionals.cancelaura then
if CleveRoids.CancelAura(conditionals.cancelaura) then
return true
end
end
local origTarget = conditionals.target
if conditionals.target == "mouseover" then
if UnitExists("mouseover") then
conditionals.target = "mouseover"
elseif CleveRoids.mouseoverUnit and UnitExists(CleveRoids.mouseoverUnit) then
conditionals.target = CleveRoids.mouseoverUnit
else
conditionals.target = "mouseover"
end
end
local needRetarget = false
if fixEmptyTargetFunc then
needRetarget = fixEmptyTargetFunc(conditionals, msg, hook)
end
-- CleveRoids.SetHelp(conditionals)
-- ClassicAPI's native focus token. No focus set: fail the clause quietly so
-- `[@focus,...] X; X` and `[@focus][] X` fall through like @mouseover does,
-- instead of announcing "Invalid target" on the way past.
if conditionals.target == "focus" or conditionals.target == "focustarget" then
local focusUnitId = CleveRoids.GetFocusUnitId()
if not focusUnitId then
conditionals.target = origTarget
return false
end
conditionals.target = focusUnitId .. (conditionals.target == "focustarget" and "target" or "")
needRetarget = false
end
-- Resolve named raid marks (skull/cross/etc.) and mark1-mark8 to native mark# unit tokens
if conditionals.target then
local resolvedMark = CleveRoids.ResolveRaidMarkUnit(conditionals.target)
if resolvedMark then conditionals.target = resolvedMark end
end
-- Handle [multiscan:priority] - scan enemies and find best target
-- Must be processed BEFORE the Keywords loop since it sets conditionals.target
-- Pass origTarget so @unit syntax makes that unit exempt from combat check
if conditionals.multiscan then
local scanResult = CleveRoids.ResolveMultiscanTarget(conditionals, origTarget)
if not scanResult then
-- No valid target found - fail this conditional line (try next)
conditionals.target = origTarget
return false
end
-- Set target to the found GUID for soft-casting via SuperWoW or Nampower v2.37+
conditionals.target = scanResult
-- Don't need to retarget since we're using GUID directly
needRetarget = false
end
for k, v in pairs(conditionals) do
if not CleveRoids.ignoreKeywords[k] then
local result = CleveRoids.Keywords[k] and CleveRoids.Keywords[k](conditionals)
-- Debug logging for equipped conditional when equipDebugLog is enabled
if CleveRoids.equipDebugLog and (k == "equipped" or k == "noequipped") then
local valStr = type(v) == "table" and table.concat(v, ", ") or tostring(v)
CleveRoids.Print("|cff888888[EquipLog] Conditional [" .. k .. ":" .. valStr .. "] = " ..
(result and "|cff00ff00PASS|r" or "|cffff0000FAIL|r") .. "|r")
end
-- Debug logging for macro reference debug
if CleveRoids.macroRefDebug then
local valStr = (v == true) and "" or (type(v) == "table" and table.concat(v, ", ") or tostring(v))
CleveRoids.Print("|cff888888[MacroRef]|r [" .. k .. (valStr ~= "" and (":" .. valStr) or "") .. "] = " ..
(result and "|cff00ff00PASS|r" or "|cffff0000FAIL|r"))
end
if not result then
if needRetarget then
TargetLastTarget()
needRetarget = false
end
conditionals.target = origTarget
return false
end
end
end
if conditionals.target ~= nil and targetBeforeAction and not (action == CastSpellByName and (CleveRoids.hasSuperwow or CleveRoids.hasCastSpellByNameUnitToken)) then
if not UnitIsUnit("target", conditionals.target) then
if SpellIsTargeting() then
SpellStopCasting()
end
TargetUnit(conditionals.target)
needRetarget = true
else
if needRetarget then needRetarget = false end
end
elseif needRetarget then
TargetLastTarget()
needRetarget = false
end
if action == "STOPMACRO" then
-- Set flag to stop subsequent lines in current macro AND parent macros
CleveRoids.stopMacroFlag = true
return true
elseif action == "SKIPMACRO" then
-- Set flag to skip remaining lines in current submacro only
CleveRoids.skipMacroFlag = true
return true
elseif action == "FIRSTACTION" then
-- Set flag to stop on first successful cast/use
CleveRoids.stopOnCastFlag = true
return true
elseif action == "NOFIRSTACTION" then
-- Clear flags to re-enable multi-queue behavior
-- Also clear stopMacroFlag if it was set by the firstaction mechanism
local wasFirstActionActive = CleveRoids.stopOnCastFlag
CleveRoids.stopOnCastFlag = false
if wasFirstActionActive and CleveRoids.stopMacroFlag then
CleveRoids.stopMacroFlag = false
end
return true
end
local result = true
if string.sub(msg, 1, 1) == "{" and string.sub(msg, -1) == "}" then
if CleveRoids.macroRefDebug then
CleveRoids.Print("|cff00ffff[MacroRef]|r Detected macro reference: " .. msg)
end
if string.sub(msg, 2, 2) == "\"" and string.sub(msg, -2,-2) == "\"" then
result = CleveRoids.ExecuteMacroBody(string.sub(msg, 3, -3), true)
else
result = CleveRoids.ExecuteMacroByName(string.sub(msg, 2, -2))
end
elseif msg == "" or msg == nil then
-- Empty action (conditionals passed but no spell to cast)
-- For non-spell actions (pet commands, etc.), still execute the action
if CleveRoids.equipDebugLog and action and action ~= CastSpellByName then
CleveRoids.Print("|cffff8800[EquipLog] Empty msg branch - calling action() with no args|r")
end
if action and action ~= CastSpellByName then
action()
result = true
else
result = false
end
else
local castMsg = msg
-- FLEXIBLY check for any rank text like "(Rank 9)" before adding the highest rank
-- Use specific "(Rank" check instead of any parentheses, so spells like
-- "Faerie Fire (Feral)" still get their rank appended automatically
if action == CastSpellByName and not string.find(msg, "%(Rank") then
local sp = CleveRoids.GetSpell(msg)
local rank = sp and (sp.rank or (sp.highest and sp.highest.rank))
if rank and rank ~= "" then
castMsg = msg .. "(" .. rank .. ")"
end
end
if conditionals.cursor then
-- [cursor]: place a ground-target spell/item at the cursor via
-- ClassicAPI (no AoE-reticle click). Both functions fall back to a
-- normal cast/use for non-ground actions.
if action == CastSpellByName then
local sp = CleveRoids.GetSpell(msg)
local spellId = sp and sp.id
if not spellId and GetSpellIdForName then
spellId = GetSpellIdForName(castMsg)
end
if spellId then
C_Spell.CastAtCursor(spellId)
else
CastSpellByName(castMsg)
end
else
C_Item.UseAtCursor(msg)
end
elseif action == CastSpellByName then
if msg == CleveRoids.Localized.Attack and (conditionals.noSpam or conditionals.checkchanneled) then
-- Melee Attack toggle: use the non-toggling AttackTarget (CastSpellNoToggle
-- covers auto-repeat + self-auras, not the melee swing).
AttackTarget()
elseif conditionals.noSpam then
-- ! prefix anti-spam. CastSpellNoToggle handles engine toggles
-- (auto-repeat Shoot/Auto Shot/Wand, forms/stances/aspects/seals):
-- it no-ops when already active instead of toggling off. It only
-- covers true toggle auras, though, so also skip re-applying an
-- already-active regular self-buff (e.g. Mark of the Wild, Renew).
if CleveRoids.ValidatePlayerBuff(msg) then
result = false
else
CastSpellNoToggle(castMsg)
end
elseif (CleveRoids.hasSuperwow or CleveRoids.hasCastSpellByNameUnitToken) and conditionals.target and origTarget then
CastSpellByName(castMsg, conditionals.target)
else
CastSpellByName(castMsg)
end
else
-- For other actions like item use etc. Pass the resolved unit token so
-- item-use can target it directly (DoUse -> C_Item.UseItemByName); action
-- closures that only take (msg) simply ignore the extra arg.
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff00ff00[EquipLog] Calling action('" .. tostring(msg) .. "')|r")
end
action(msg, conditionals.target)
end
end
if needRetarget then
TargetLastTarget()
end
-- [stopattack]: Deferred to OnUpdate (next frame) using CheapShot pattern.
-- AttackTarget()+ClearTarget() on first frame, enforce clear for 0.2s,
-- then retarget via stored GUID.
if conditionals.stopattack then
CleveRoids.DeferStopAttack()
end
conditionals.target = origTarget
return result
end
function CleveRoids.DoCast(msg)
if CleveRoids.ChannelTimeDebug then
if msg and string.find(msg, "Arcane") then
DEFAULT_CHAT_FRAME:AddMessage("|cffff00ff[/cast BUTTON PRESS]|r " .. msg)
end
end
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
local v = parts[i]
if CleveRoids.DoWithConditionals(v, CleveRoids.Hooks.CAST_SlashCmd, CleveRoids.FixEmptyTarget, false, CastSpellByName) then
-- If /firstaction was used, stop macro evaluation after first successful cast
if CleveRoids.stopOnCastFlag then
CleveRoids.stopMacroFlag = true
end
return true
end
end
return false
end
-- Resolve unit for /pfcast: mouseover → GetMouseFocus label+id → mouseoverUnit → target → player
-- Mirrors pfUI's own unit resolution order so /pfcast conditionals evaluate against the same unit
-- that pfUI would cast on.
local function ResolvePfCastUnit()
if UnitExists("mouseover") then
return "mouseover"
end
local frame = GetMouseFocus and GetMouseFocus()
if frame and frame.label and frame.id then
return frame.label .. frame.id
elseif CleveRoids.mouseoverUnit and UnitExists(CleveRoids.mouseoverUnit) then
return CleveRoids.mouseoverUnit
elseif UnitExists("target") then
return "target"
elseif GetCVar and GetCVar("autoSelfCast") == "1" then
return "player"
end
return nil
end
-- One /pfcast clause. If conditionals are present but no explicit @unit, inject the
-- pfUI-resolved unit so all conditionals ([help], [nodebuff:X], etc.) evaluate against
-- the same unit pfUI would cast on, and the final CastSpellByName gets the correct
-- unit token. Module-level to avoid a closure per call.
local function PfCastClause(v)
if string.find(v, "%[") and not string.find(v, "@") then
local unit = ResolvePfCastUnit()
if unit then
v = string.gsub(v, "%[", "[@" .. unit .. ",", 1)
end
end
return CleveRoids.DoWithConditionals(v, CleveRoids.Hooks.PFCAST_SlashCmd, CleveRoids.FixEmptyTarget, false, CastSpellByName)
end
-- /pfcast with CleveRoids conditionals: evaluate conditionals then cast via pfUI's mouseover chain.
-- Called from the SlashCmdList.PFCAST hook (set up by Extensions/Mouseover/pfUI.lua after pfUI loads).
function CleveRoids.DoPfCast(msg)
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
-- Expand `[a][b] Spell` here rather than leaving it to DoWithConditionals so
-- every group without its own @unit gets the injection, not just the first.
local variants = CleveRoids.ExpandBracketGroups(parts[i])
local handled
if variants then
for j = 1, variants.n do
handled = PfCastClause(variants[j])
if handled then break end
end
else
handled = PfCastClause(parts[i])
end
if handled then
if CleveRoids.stopOnCastFlag then
CleveRoids.stopMacroFlag = true
end
return true
end
end
return false
end
-- PERFORMANCE: Module-level pet cast action to avoid closure allocation per call
local function _petCastAction(spellName)
local petSpell = CleveRoids.GetPetSpell(spellName)
if petSpell and petSpell.slot then
CastPetAction(petSpell.slot)
return true
end
return false
end
function CleveRoids.DoCastPet(msg)
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
local v = parts[i]
if CleveRoids.DoWithConditionals(v, _petCastAction, CleveRoids.FixEmptyTarget, false, _petCastAction) then
return true
end
end
return false
end
function CleveRoids.DoTarget(msg)
-- Check macro stop flags
if CleveRoids.stopMacroFlag or CleveRoids.skipMacroFlag then
return false
end
-- Conditional /target takes `;` clauses and `[a][b]` groups like /cast does:
-- the first clause or group that finds a unit wins. A single-group clause
-- falls through to the resolution below.
if msg and string.find(msg, "%[") then
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
local n = table.getn(parts)
if n > 1 then
for i = 1, n do
if CleveRoids.DoTarget(parts[i]) then return true end
end
return false
end
local variants = CleveRoids.ExpandBracketGroups(parts[1])
if variants then
for i = 1, variants.n do
if CleveRoids.DoTarget(variants[i]) then return true end
end
return false
end
end
local action, conditionals = CleveRoids.GetParsedMsg(msg)
if action ~= "" or type(conditionals) ~= "table" or not next(conditionals) then
CleveRoids.Hooks.TARGET_SlashCmd(msg)
return true
end
local function IsGuidValid(unitTok, conds)
if not unitTok or not UnitExists(unitTok) or CleveRoids.IsUnitDeadOrGhost(unitTok) then
return false
end
local orig = conds.target
conds.target = unitTok
local ok = true
for k, _ in pairs(conds) do
if not CleveRoids.ignoreKeywords[k] then
local fn = CleveRoids.Keywords[k]
if not fn or not fn(conds) then ok = false; break end
end
end
conds.target = orig
return ok
end
-- Save original target GUID for potential restoration (SuperWoW returns GUID as 2nd value)
local originalTargetGuid = CleveRoids.GetGUID("target")
-- Handle [multiscan:priority] - use ResolveMultiscanTarget for enemy scanning
-- ResolveMultiscanTarget handles its own target save/restore internally
if conditionals.multiscan then
local scanResult = CleveRoids.ResolveMultiscanTarget(conditionals, conditionals.target)
if scanResult then
TargetUnit(scanResult)
return true
end
-- No match found - restore original target if scanning changed it
if originalTargetGuid and UnitExists(originalTargetGuid) then
TargetUnit(originalTargetGuid)
elseif not originalTargetGuid then
ClearTarget()
end
return false
end
-- Track if an explicit target was specified via @unit syntax
local explicitTarget = conditionals.target
do
local unitTok = conditionals.target
if unitTok == "mouseover" then
if UnitExists("mouseover") then
unitTok = "mouseover"
elseif CleveRoids.mouseoverUnit and UnitExists(CleveRoids.mouseoverUnit) then
unitTok = CleveRoids.mouseoverUnit
elseif pfUI and pfUI.uf and pfUI.uf.mouseover and pfUI.uf.mouseover.unit
and UnitExists(pfUI.uf.mouseover.unit) then
unitTok = pfUI.uf.mouseover.unit
else
unitTok = nil
end
end
if unitTok == "focus" or unitTok == "focustarget" then
local isFocusTarget = unitTok == "focustarget"
local fTok = CleveRoids.GetFocusUnitId()
if fTok then
unitTok = fTok .. (isFocusTarget and "target" or "")
if not UnitExists(unitTok) then unitTok = nil end
else
unitTok = nil
end
end
-- Resolve named raid marks (skull/cross/etc.) and mark1-mark8 to native mark# unit tokens
local resolvedMark = CleveRoids.ResolveRaidMarkUnit(unitTok)
if resolvedMark then unitTok = resolvedMark end
if unitTok and UnitExists(unitTok) and IsGuidValid(unitTok, conditionals) then
TargetUnit(unitTok)
return true
end
-- If an explicit target was specified via @unit syntax but doesn't exist or isn't valid,
-- return false instead of falling through to candidate search.
-- This ensures "/target [@mouseover,harm,alive]" does nothing when mouse is over empty ground.
if explicitTarget then
return false
end
end
if UnitExists("target") and IsGuidValid("target", conditionals) then
return true
end
local candidates = {}
local wantsHelp = conditionals.help
local wantsHarm = conditionals.harm
local function addCandidate(unitId)
if not UnitExists(unitId) then return end
if wantsHelp and not UnitCanAssist("player", unitId) then return end
if wantsHarm and not UnitCanAttack("player", unitId) then return end
table.insert(candidates, { unitId = unitId })
end
addCandidate("mouseover")
addCandidate("target")
if not wantsHarm then
table.insert(candidates, { unitId = "player" })
end
addCandidate("pet")
for i = 1, 4 do
addCandidate("party"..i)
addCandidate("partypet"..i)
addCandidate("party"..i.."target")
end
for i = 1, 40 do
addCandidate("raid"..i)
addCandidate("raidpet"..i)
addCandidate("raid"..i.."target")
end
addCandidate("targettarget")
addCandidate("targettargettarget")
addCandidate("pettarget")
addCandidate("focus")
addCandidate("focustarget")
-- Visible nameplates as targeting candidates, via ClassicAPI's nameplateN
-- unit tokens -- no SuperWoW WorldFrame child-walk / GUID scrape, and covers
-- default vanilla nameplates too. Slots are sparse (a removed plate leaves
-- its slot vacant until reused), so scan the full range rather than breaking
-- on the first gap; UnitExists returns false cleanly for a free/out-of-range
-- slot.
for i = 1, 40 do
local plate = "nameplate" .. i
if UnitExists(plate) then
addCandidate(plate)
end
end
for _, c in ipairs(candidates) do
if IsGuidValid(c.unitId, conditionals) then
TargetUnit(c.unitId)
return true
end
end
-- UnitXP 3D enemy scanning: cycles through enemies in world space (no nameplate required)
-- This is the most powerful scan - finds enemies by line of sight and distance
-- Always enabled unless explicitly looking for friendlies only ([help] without [harm])
-- UnitXP only finds enemies, so this is safe for any /target with conditionals
local wantsFriendlyOnly = wantsHelp and not wantsHarm
if not wantsFriendlyOnly and CleveRoids.hasUnitXP then
-- Try nearestEnemy first - most common case and most efficient
local found = UnitXP("target", "nearestEnemy")
if found and UnitExists("target") and IsGuidValid("target", conditionals) then
return true
end
-- Determine scan mode: use distance-priority for melee conditionals
local wantsMelee = conditionals.meleerange or conditionals.nomeleerange
local scanMode = wantsMelee and "nextEnemyConsideringDistance" or "nextEnemyInCycle"
local seenGuids = {}
local firstGuid = nil
local maxIterations = 50 -- Safety limit
for i = 1, maxIterations do
found = UnitXP("target", scanMode)
if not found then break end
local currentGuid = CleveRoids.GetGUID("target")
if not currentGuid then break end
-- Check if we've cycled back to start
if firstGuid == nil then
firstGuid = currentGuid
elseif currentGuid == firstGuid then
break -- Completed full cycle
end
-- Skip already-seen targets
if not seenGuids[currentGuid] then
seenGuids[currentGuid] = true
-- Test this target against all conditionals
if IsGuidValid("target", conditionals) then
return true -- Found matching target
end
end
end
-- No match found via UnitXP scan - restore original target if we had one
if originalTargetGuid and UnitExists(originalTargetGuid) then
TargetUnit(originalTargetGuid)
elseif not originalTargetGuid then
ClearTarget()
end
end
-- TargetNearestEnemy() fallback: cycles through enemies via tab-targeting
-- Finds enemy players that UnitXP may miss (e.g. [class:Rogue harm])
if not wantsFriendlyOnly then
TargetNearestEnemy()
if UnitExists("target") then
local firstGuid2 = CleveRoids.GetGUID("target")
if firstGuid2 then
if IsGuidValid("target", conditionals) then
return true
end
for i = 1, 50 do
TargetNearestEnemy()
if not UnitExists("target") then break end
local guid = CleveRoids.GetGUID("target")
if not guid or guid == firstGuid2 then break end
if IsGuidValid("target", conditionals) then
return true
end
end
end
end
-- No match found - restore original target
if originalTargetGuid and UnitExists(originalTargetGuid) then
TargetUnit(originalTargetGuid)
elseif not originalTargetGuid then
ClearTarget()
end
end
-- No matching target found after all scans - restore and fail
if originalTargetGuid and UnitExists(originalTargetGuid) then
TargetUnit(originalTargetGuid)
elseif not originalTargetGuid then
ClearTarget()
end
return false
end
-- Attempts to attack a unit by a set of conditionals
-- msg: The raw message intercepted from a /petattack command
function CleveRoids.DoPetAction(action, msg)
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
if CleveRoids.DoWithConditionals(parts[i], action, CleveRoids.FixEmptyTarget, true, action) then
return true
end
end
return false
end
-- PERFORMANCE: Module-level action to avoid closure allocation per call
local function _startAttackAction()
if not UnitExists("target") or CleveRoids.IsUnitDead("target") then TargetNearestEnemy() end
-- Ground truth is the real action-bar state, NOT the cached autoAttack flag.
-- The flag can drift stale-true (e.g. set optimistically after AttackTarget below,
-- or a target dying without PLAYER_LEAVE_COMBAT firing), which would make us wrongly
-- believe we're already swinging and skip the attack. Use the ORIGINAL API here:
-- the overridden global IsCurrentAction just echoes the cached flag for the attack
-- slot, so it can't detect drift. Fall back to the flag only if the slot is unknown.
local isAttacking = CleveRoids.CurrentSpell.autoAttack
local slot = CleveRoids.GetProxyActionSlot(CleveRoids.Localized.Attack)
if slot then
isAttacking = CleveRoids.Hooks.IsCurrentAction(slot) and true or false
CleveRoids.CurrentSpell.autoAttack = isAttacking
end
if not isAttacking and not CleveRoids.CurrentSpell.autoAttackLock and UnitExists("target") and UnitCanAttack("player", "target") then
CleveRoids.CurrentSpell.autoAttackLock = true
CleveRoids.autoAttackLockElapsed = GetTime()
AttackTarget()
-- FIX: Immediately set autoAttack flag so subsequent macro lines know attack started
-- Don't wait for PLAYER_ENTER_COMBAT event which has a delay
CleveRoids.CurrentSpell.autoAttack = true
-- FIX: Queue icon update so action bars reflect the new state
if CleveRoidMacros and CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
end
-- Attempts to conditionally start an attack. Returns false if no conditionals are found.
function CleveRoids.DoConditionalStartAttack(msg)
if not string.find(msg, "%[") then return false end
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
-- We pass 'nil' for the hook, so DoWithConditionals does nothing if it fails to parse conditionals.
if CleveRoids.DoWithConditionals(parts[i], nil, CleveRoids.FixEmptyTarget, false, _startAttackAction) then
return true
end
end
return false
end
-- Stop channeling on the next tick (nampower v2.18+), behind /stopchanneling.
-- The underlying API no-ops in two cases and a command that does nothing
-- without saying why reads as broken, so each gets a one-shot warning -- the
-- same treatment the [rooted] conditional gives a too-old nampower.
local _stopChannelingWarned = {}
local function warnStopChanneling(key, reason)
if _stopChannelingWarned[key] then return end
_stopChannelingWarned[key] = true
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r /stopchanneling "
.. reason .. ".", 1, 0.5, 0.5)
end
function CleveRoids.StopChanneling()
local API = CleveRoids.NampowerAPI
-- Nothing was sent: nampower is absent or predates ChannelStopCastingNextTick.
if not API.StopChannelNextTick() then
warnStopChanneling("version", "requires Nampower v2.18.0 or newer")
return false
end
-- The call went through, but nampower only acts on it while channel
-- queueing is on, so the channel would run to completion regardless.
if not API.IsQueueingEnabled("channeling") then
warnStopChanneling("setting", "requires Nampower's NP_QueueChannelingSpells setting to be enabled")
return false
end
return true
end
-- PERFORMANCE: Module-level actions to avoid closure allocation per call
local function _stopAttackAction()
CleveRoids.DeferStopAttack()
end
local function _stopCastingAction()
SpellStopCasting()
end
local function _stopChannelingAction()
CleveRoids.StopChanneling()
end
-- Attempts to conditionally stop an attack. Returns false if no conditionals are found.
function CleveRoids.DoConditionalStopAttack(msg)
if not string.find(msg, "%[") then return false end
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
if CleveRoids.DoWithConditionals(parts[i], nil, CleveRoids.FixEmptyTarget, false, _stopAttackAction) then
return true
end
end
return false
end
-- Attempts to conditionally stop casting. Returns false if no conditionals are found.
function CleveRoids.DoConditionalStopCasting(msg)
if not string.find(msg, "%[") then return false end
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
if CleveRoids.DoWithConditionals(parts[i], nil, CleveRoids.FixEmptyTarget, false, _stopCastingAction) then
return true
end
end
return false
end
-- Attempts to conditionally interrupt channeling. Returns false if no conditionals are found.
function CleveRoids.DoConditionalStopChanneling(msg)
if not string.find(msg, "%[") then return false end
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
if CleveRoids.DoWithConditionals(parts[i], nil, CleveRoids.FixEmptyTarget, false, _stopChannelingAction) then
return true
end
end
return false
end
local function _clearTargetAction()
ClearTarget()
end
-- Attempts to conditionally clear target. Returns false if no conditionals are found.
function CleveRoids.DoConditionalClearTarget(msg)
if not string.find(msg, "%[") then return false end
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
if CleveRoids.DoWithConditionals(parts[i], nil, CleveRoids.FixEmptyTarget, false, _clearTargetAction) then
return true
end
end
return false
end
-- Resolve an EQUIPPED inventory slot (1-19) holding the item, or nil.
-- C_Item.UseItemByName only searches bags, so equipped-only items (trinkets,
-- weapons) must be fired via their slot. Prefer nampower's fast lookup; fall
-- back to a manual scan for clients without FindPlayerItemSlot (< v2.18).
local function FindEquippedItemSlot(msg, itemId)
local API = CleveRoids.NampowerAPI
if type(API) == "table" and API.FindItemFast then
local info = API.FindItemFast(itemId or msg)
if info and info.inventoryID then return info.inventoryID end
-- Found only in bags (or not at all) -> not an equipped-only item.
if info then return nil end
end
-- Fallback scan of equipped slots by ID or name, read straight from each
-- slot via ClassicAPI (GetInventoryItemID / C_Item.GetItemNameByID) -- no
-- link string built, no "item:"/"|h[..]|h" parse. Slots are 1-19.
local wantLower = (not itemId) and string_lower(msg) or nil
for slot = 1, 19 do
local id = GetInventoryItemID("player", slot)
if id then
if itemId then
if id == itemId then return slot end
else
local nm = C_Item.GetItemNameByID(id)
if nm and string_lower(nm) == wantLower then return slot end
end
end
end
return nil
end
-- Attempts to use or equip an item by a set of conditionals
-- Also checks if a condition is a spell so that you can mix item and spell use
-- msg: The raw message intercepted from a /use or /equip command
function CleveRoids.DoUse(msg)
-- Check macro stop flags
if CleveRoids.stopMacroFlag or CleveRoids.skipMacroFlag then
return false
end
local handled = false
local action = function(msg, unit)
-- Defensive: make sure we are not in "split stack" mode and nothing is on the cursor
if type(CloseStackSplitFrame) == "function" then CloseStackSplitFrame() end
if CursorHasItem and CursorHasItem() then ClearCursor() end
-- Only pass a cast target when it actually exists, so a stale @unit doesn't
-- waste a consumable. Self-use items (potions/food/hearth) ignore it anyway.
local useUnit = (unit and UnitExists(unit)) and unit or nil
-- Direct equipped inventory slot ID (1-19): use it in place.
local slotId = tonumber(msg)
if slotId and slotId >= 1 and slotId <= 19 then
ClearCursor() -- extra safety before using equipped items
UseInventoryItem(slotId)
return
end
-- Equipped items (trinkets, weapons) live outside bags, so C_Item.UseItemByName
-- can't reach them. Fire an equipped match through its slot first.
local invSlot = FindEquippedItemSlot(msg, slotId)
if invSlot then
ClearCursor()
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via UseInventoryItem(" .. invSlot .. ")|r")
end
UseInventoryItem(invSlot)
return
end
-- Bag path: one ClassicAPI call finds the item in bags and dispatches by
-- type (potion/food/scroll/on-use), honoring `useUnit` for targeted-spell
-- items. itemIDs pass as numbers; names/links pass through unchanged.
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff888888[UseLog] /use " .. msg .. " via C_Item.UseItemByName(" .. tostring(slotId or msg) .. ", " .. tostring(useUnit) .. ")|r")
end
C_Item.UseItemByName(slotId or msg, useUnit)
end
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
if CleveRoids.DoWithConditionals(parts[i], action, CleveRoids.FixEmptyTarget, false, action) then
-- If /firstaction was used, stop macro evaluation after first successful use
if CleveRoids.stopOnCastFlag then
CleveRoids.stopMacroFlag = true
end
return true
end
end
return false
end
-- Find an item in bags by name (ignoring equipped items)
-- Used to prefer bag copies over swapping from paired equipped slots
-- (e.g., dual-wielding the same weapon with /equipmh + /equipoh)
local function FindItemInBagsByName(itemName)
if not itemName then return nil, nil end
local lowerName = string_lower(itemName)
for bag = 0, 4 do
local numSlots = GetContainerNumSlots(bag) or 0
for slot = 1, numSlots do
local name = C_Item.GetItemName({ bagID = bag, slotIndex = slot })
if name and string_lower(name) == lowerName then
return bag, slot
end
end
end
return nil, nil
end
function CleveRoids.EquipBagItem(msg, slotOrOffhand)
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff00ffff[EquipLog] EquipBagItem called: '" .. tostring(msg) .. "' slot=" .. tostring(slotOrOffhand) .. "|r")
end
if CleveRoids.equipInProgress then
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cffff0000[EquipLog] Equip already in progress, skipping|r")
end
return false
end
-- Accept slot number directly, or boolean for backward compatibility (false=16/MH, true=17/OH)
local invslot
if type(slotOrOffhand) == "number" then
invslot = slotOrOffhand
else
invslot = slotOrOffhand and 17 or 16
end
local API = CleveRoids.NampowerAPI
-- v2.18+: Use native fast lookup for item ID or name
-- Guard against API being a function instead of table (addon conflict protection)
if type(API) == "table" and type(API.features) == "table" and API.features.hasFindPlayerItemSlot then
local searchTerm = msg
local itemId = tonumber(msg)
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff888888[EquipLog] Searching for '" .. tostring(searchTerm) .. "' (slot " .. invslot .. ")|r")
end
-- Check if already equipped in target slot
if API.IsItemInSlot(searchTerm, invslot) then
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff00ff00[EquipLog] '" .. tostring(searchTerm) .. "' already in slot " .. invslot .. "|r")
end
return true
end
-- Find the item (works with both ID and name)
local itemInfo = API.FindItemFast(searchTerm)
if CleveRoids.equipDebugLog then
if itemInfo then
CleveRoids.Print("|cff888888[EquipLog] FindItemFast found: invID=" .. tostring(itemInfo.inventoryID) .. " bag=" .. tostring(itemInfo.bagID) .. " slot=" .. tostring(itemInfo.slot) .. "|r")
else
CleveRoids.Print("|cffff8800[EquipLog] FindItemFast returned nil|r")
end
end
if itemInfo then
-- Already equipped in different slot - need to swap
if itemInfo.inventoryID then
if itemInfo.inventoryID == invslot then
return true -- Already in correct slot
end
-- Before swapping from another equipped slot, check bags for another copy
-- This handles dual-wielding the same weapon (e.g., /equipmh Scimitar + /equipoh Scimitar)
local bagCopyBag, bagCopySlot = FindItemInBagsByName(itemInfo.name or searchTerm)
if bagCopyBag then
ClearCursor()
PickupContainerItem(bagCopyBag, bagCopySlot)
if CursorHasItem and CursorHasItem() then
EquipCursorItem(invslot)
ClearCursor()
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff00ff00[EquipLog] Equipped bag copy of '" .. tostring(msg) .. "' from bag " .. bagCopyBag .. " slot " .. bagCopySlot .. " (preferred over swap)|r")
end
return true
end
end
-- No bag copy found - swap from equipped slot
ClearCursor()
PickupInventoryItem(itemInfo.inventoryID)
if CursorHasItem and CursorHasItem() then
EquipCursorItem(invslot)
ClearCursor()
return true
end
elseif itemInfo.bagID and itemInfo.slot then
-- In bag - equip to target slot
ClearCursor()
PickupContainerItem(itemInfo.bagID, itemInfo.slot)
if CursorHasItem and CursorHasItem() then
EquipCursorItem(invslot)
ClearCursor()
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff00ff00[EquipLog] Equipped '" .. tostring(msg) .. "' from bag " .. itemInfo.bagID .. " slot " .. itemInfo.slot .. "|r")
end
return true
end
end
end
-- Item not found via v2.18 lookup - fall through to legacy path
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cffff8800[EquipLog] Item '" .. tostring(msg) .. "' not found via v2.18 FindPlayerItemSlot, trying legacy fallback|r")
-- Debug: Try direct FindPlayerItemSlot call
if FindPlayerItemSlot then
local bag, slot = FindPlayerItemSlot(msg)
CleveRoids.Print("|cff888888[EquipLog] Direct FindPlayerItemSlot('" .. msg .. "') = bag:" .. tostring(bag) .. " slot:" .. tostring(slot) .. "|r")
end
end
end
-- Fallback for older Nampower versions
-- Try to interpret as item ID (numbers > 19)
local itemId = tonumber(msg)
if itemId and itemId > 19 then
local itemName = nil
if API and API.GetItemName then
itemName = API.GetItemName(itemId)
end
if not itemName and GetItemInfo then
itemName = GetItemInfo(itemId)
end
if itemName then
msg = itemName
else
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cffff8800[EquipLog] Can't resolve item ID " .. tostring(itemId) .. "|r")
end
return false -- Can't resolve item ID
end
end
-- PERFORMANCE: Fast check if already equipped (single function call)
if CleveRoids.IsItemEquipped and CleveRoids.IsItemEquipped(msg, invslot) then
return true
end
-- Note what's currently in the target slot so we can invalidate its cache
local oldSlotName = C_Item.GetItemName({ equipmentSlotIndex = invslot })
-- Helper to invalidate displaced item's cache
local function InvalidateDisplacedItem()
if oldSlotName and CleveRoids.Items then
CleveRoids.Items[oldSlotName] = nil
CleveRoids.Items[string_lower(oldSlotName)] = nil
end
end
-- Check if item is already equipped in the paired slot (swap case)
-- EquipItemByName doesn't handle swapping equipped items, so we must do it manually
-- Paired slots: trinkets (13<->14), weapons (16<->17), rings (11<->12)
-- BUT: check bags first for another copy (e.g., dual-wielding same weapon)
local pairedSlots = {[13] = 14, [14] = 13, [16] = 17, [17] = 16, [11] = 12, [12] = 11}
local checkSlot = pairedSlots[invslot]
if checkSlot then
local slotItemName = C_Item.GetItemName({ equipmentSlotIndex = checkSlot })
if slotItemName then
if string_lower(slotItemName) == string_lower(msg) then
-- Item found in paired slot - but prefer a bag copy if one exists
local bagCopyBag, bagCopySlot = FindItemInBagsByName(msg)
if bagCopyBag then
-- Bag copy available - equip from bag instead of swapping
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff00ffff[EquipLog] Found bag copy, equipping from bag " .. bagCopyBag .. " slot " .. bagCopySlot .. " instead of swapping from slot " .. checkSlot .. "|r")
end
ClearCursor()
PickupContainerItem(bagCopyBag, bagCopySlot)
if CursorHasItem and CursorHasItem() then
EquipCursorItem(invslot)
ClearCursor()
if CleveRoids.Items then
CleveRoids.Items[msg] = nil
CleveRoids.Items[string_lower(msg)] = nil
end
InvalidateDisplacedItem()
return true
end
ClearCursor()
else
-- No bag copy - swap from paired slot
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cff00ffff[EquipLog] Swapping from slot " .. checkSlot .. " to slot " .. invslot .. "|r")
end
ClearCursor()
PickupInventoryItem(checkSlot)
if CursorHasItem and CursorHasItem() then
EquipCursorItem(invslot)
ClearCursor()
if CleveRoids.Items then
CleveRoids.Items[msg] = nil
CleveRoids.Items[string_lower(msg)] = nil
end
InvalidateDisplacedItem()
return true
end
ClearCursor()
end
end
end
end
-- PERFORMANCE: Try EquipItemByName for bag items (fast path)
-- This is the fastest path - no item lookup, no cursor operations
if EquipItemByName then
local ok = pcall(EquipItemByName, msg, invslot)
if ok then
-- Verify the item actually landed in the target slot
-- EquipItemByName may silently no-op for same-named items in paired slots
-- (e.g., dual-wielding Scimitar: MH copy found first, "equipped" to same slot)
local newName = C_Item.GetItemName({ equipmentSlotIndex = invslot })
if newName and string_lower(newName) == string_lower(msg) then
if CleveRoids.Items then
CleveRoids.Items[msg] = nil
CleveRoids.Items[string_lower(msg)] = nil
end
InvalidateDisplacedItem()
return true
end
-- Verification failed - EquipItemByName didn't place item in target slot
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cffff8800[EquipLog] EquipItemByName('" .. msg .. "', " .. invslot .. ") succeeded but item not in target slot, trying fallback|r")
end
end
end
-- PERFORMANCE: Use fast lookup first, fall back to full scan
-- Full scan is now optimized with GetNameFromLink() instead of GetItemInfo()
local item = CleveRoids.GetItemFast and CleveRoids.GetItemFast(msg)
if not item then
-- Try quick targeted scan (stops when found)
item = CleveRoids.FindItemQuick and CleveRoids.FindItemQuick(msg)
end
if not item then
-- Full scan fallback (now optimized, safe during combat)
item = CleveRoids.GetItem(msg)
end
if not item or not item.name then
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cffff8800[EquipLog] Item '" .. tostring(msg) .. "' not found in bags or equipped|r")
end
return false
end
-- Already equipped check (by item ID from lookup)
if item.inventoryID == invslot then
return true
end
if not item.bagID and not item.inventoryID then
return false
end
-- Try EquipItemByName with resolved name (in case msg was partial/different case)
if item.name and EquipItemByName and item.name ~= msg then
local ok = pcall(EquipItemByName, item.name, invslot)
if ok then
-- Verify the item actually landed in the target slot (same guard as above)
local newName = C_Item.GetItemName({ equipmentSlotIndex = invslot })
if newName then
if string_lower(newName) == string_lower(item.name) then
if CleveRoids.Items then
CleveRoids.Items[item.name] = nil
CleveRoids.Items[string_lower(item.name)] = nil
end
InvalidateDisplacedItem()
return true
end
end
-- Verification failed - fall through to manual cursor-based equip
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cffff8800[EquipLog] EquipItemByName('" .. item.name .. "', " .. invslot .. ") succeeded but item not in target slot, trying manual fallback|r")
end
end
end
-- Fallback: Manual pickup and equip
CleveRoids.equipInProgress = true
-- PERFORMANCE: Single cursor check at start
if CursorHasItem and CursorHasItem() then
ClearCursor()
end
local pickupSuccess = false
if item.bagID and item.slot then
PickupContainerItem(item.bagID, item.slot)
pickupSuccess = CursorHasItem and CursorHasItem()
elseif item.inventoryID then
PickupInventoryItem(item.inventoryID)
pickupSuccess = CursorHasItem and CursorHasItem()
end
if not pickupSuccess then
ClearCursor()
CleveRoids.equipInProgress = false
return false
end
-- Verify the cursor holds the item we meant to pick up. The bag can shift
-- between the lookup and the pickup (item consumed/moved/reorganized), in
-- which case we'd otherwise equip whatever landed on the cursor. Only abort
-- on a definitive mismatch (false); nil = can't tell -> trust CursorHasItem.
if item.id and CleveRoids.ClassicAPI.CursorHoldsItemID(item.id) == false then
if CleveRoids.equipDebugLog then
CleveRoids.Print("|cffff8800[EquipLog] Cursor holds wrong item after pickup; aborting equip|r")
end
ClearCursor()
CleveRoids.equipInProgress = false
return false
end
EquipCursorItem(invslot)
ClearCursor()
if CleveRoids.Items and item.name then
CleveRoids.Items[item.name] = nil
CleveRoids.Items[string_lower(item.name)] = nil
end
InvalidateDisplacedItem()
CleveRoids.equipInProgress = false
return true
end
-- PERFORMANCE: Module-level actions to avoid closure allocation per call
local function _equipMainhandAction(msg)
return CleveRoids.EquipBagItem(msg, false)
end
local function _equipOffhandAction(msg)
return CleveRoids.EquipBagItem(msg, true)
end
local function _equipTrinket1Action(msg)
return CleveRoids.EquipBagItem(msg, 13)
end
local function _equipTrinket2Action(msg)
return CleveRoids.EquipBagItem(msg, 14)
end
local function _equipRing1Action(msg)
return CleveRoids.EquipBagItem(msg, 11)
end
local function _equipRing2Action(msg)
return CleveRoids.EquipBagItem(msg, 12)
end
local function _cancelFormAction()
CancelShapeshiftForm()
end
function CleveRoids.DoEquipMainhand(msg)
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
local v = string.gsub(parts[i], "^%?", "")
if CleveRoids.DoWithConditionals(v, _equipMainhandAction, CleveRoids.FixEmptyTarget, false, _equipMainhandAction) then
return true
end
end
return false
end
function CleveRoids.DoEquipOffhand(msg)
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
local v = string.gsub(parts[i], "^%?", "")
if CleveRoids.DoWithConditionals(v, _equipOffhandAction, CleveRoids.FixEmptyTarget, false, _equipOffhandAction) then
return true
end
end
return false
end
function CleveRoids.DoEquipTrinket1(msg)
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
local v = string.gsub(parts[i], "^%?", "")
if CleveRoids.DoWithConditionals(v, _equipTrinket1Action, CleveRoids.FixEmptyTarget, false, _equipTrinket1Action) then
return true
end
end
return false
end
function CleveRoids.DoEquipTrinket2(msg)
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
local v = string.gsub(parts[i], "^%?", "")
if CleveRoids.DoWithConditionals(v, _equipTrinket2Action, CleveRoids.FixEmptyTarget, false, _equipTrinket2Action) then
return true
end
end
return false
end
function CleveRoids.DoEquipRing1(msg)
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
local v = string.gsub(parts[i], "^%?", "")
if CleveRoids.DoWithConditionals(v, _equipRing1Action, CleveRoids.FixEmptyTarget, false, _equipRing1Action) then
return true
end
end
return false
end
function CleveRoids.DoEquipRing2(msg)
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
local v = string.gsub(parts[i], "^%?", "")
if CleveRoids.DoWithConditionals(v, _equipRing2Action, CleveRoids.FixEmptyTarget, false, _equipRing2Action) then
return true
end
end
return false
end
-- Equip a ClassicAPI equipment set by name. msg is the resolved set name (after
-- conditional parsing). ClassicAPI's own /equipset is a bare
-- GetEquipmentSetID -> UseEquipmentSet with no conditional support; routing it
-- through DoWithConditionals adds [combat]/[mod]/@unit/etc. and ";"-separated
-- fallthrough (first set whose conditionals pass wins). Nil-guarded so a client
-- without the EquipmentSet API just no-ops instead of erroring.
local function _equipSetAction(msg)
if not msg or msg == "" then return false end
if type(C_EquipmentSet) ~= "table" then return false end
local setID = C_EquipmentSet.GetEquipmentSetID(msg)
if not setID then return false end
C_EquipmentSet.UseEquipmentSet(setID)
return true
end
function CleveRoids.DoEquipSet(msg)
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
local v = string.gsub(parts[i], "^%?", "")
if CleveRoids.DoWithConditionals(v, _equipSetAction, CleveRoids.FixEmptyTarget, false, _equipSetAction) then
return true
end
end
return false
end
function CleveRoids.DoCancelForm(msg)
local handled
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
handled = false
if CleveRoids.DoWithConditionals(parts[i], _cancelFormAction, CleveRoids.FixEmptyTarget, false, _cancelFormAction) then
handled = true
break
end
end
if handled == nil then
_cancelFormAction()
end
return handled
end
CleveRoids.DoUnshift = CleveRoids.DoCancelForm
function CleveRoids.DoRetarget()
if UnitName("target") == nil
or UnitHealth("target") == 0
or not UnitCanAttack("player", "target")
then
ClearTarget()
TargetNearestEnemy()
end
end
-- Attempts to stop macro (stops ALL remaining lines including parent macros)
function CleveRoids.DoStopMacro(msg)
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
local parts = CleveRoids.splitStringIgnoringQuotes(CleveRoids.Trim(msg))
for i = 1, table.getn(parts) do
if CleveRoids.DoWithConditionals(parts[i], nil, nil, false, "STOPMACRO") then
return true
end
end
return false
end
-- Attempts to skip remaining lines in current submacro ONLY (returns to parent)
function CleveRoids.DoSkipMacro(msg)
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
local parts = CleveRoids.splitStringIgnoringQuotes(CleveRoids.Trim(msg))
for i = 1, table.getn(parts) do
if CleveRoids.DoWithConditionals(parts[i], nil, nil, false, "SKIPMACRO") then
return true
end
end
return false
end
-- Enables "stop on first successful cast" mode for the rest of the macro
-- When set, successful /cast or /use commands will set stopMacroFlag automatically
-- This allows priority-based macro evaluation: first matching cast wins
-- Example usage:
-- /firstaction
-- /cast [myrawpower:>48] Shred
-- /cast [myrawpower:>40] Claw
-- In this case, if Shred casts, Claw will NOT be queued
function CleveRoids.DoFirstAction(msg)
-- Check if there are conditionals
if msg and CleveRoids.Trim(msg) ~= "" then
-- Has conditionals - use DoWithConditionals to evaluate them
local parts = CleveRoids.splitStringIgnoringQuotes(CleveRoids.Trim(msg))
for i = 1, table.getn(parts) do
if CleveRoids.DoWithConditionals(parts[i], nil, nil, false, "FIRSTACTION") then
return true
end
end
return false
else
-- No conditionals - just set the flag
CleveRoids.stopOnCastFlag = true
return true
end
end
-- Disables "stop on first successful cast" mode, re-enabling multi-queue behavior
-- Use this to restore normal evaluation after /firstaction
-- Example usage:
-- /firstaction
-- /cast [myrawpower:>48] Shred -- Priority section
-- /cast [myrawpower:>40] Claw
-- /nofirstaction
-- /cast Tiger's Fury -- Can queue alongside above
-- /startattack
function CleveRoids.DoNoFirstAction(msg)
-- Check if there are conditionals
if msg and CleveRoids.Trim(msg) ~= "" then
-- Has conditionals - use DoWithConditionals to evaluate them
local parts = CleveRoids.splitStringIgnoringQuotes(CleveRoids.Trim(msg))
for i = 1, table.getn(parts) do
if CleveRoids.DoWithConditionals(parts[i], nil, nil, false, "NOFIRSTACTION") then
return true
end
end
return false
else
-- No conditionals - clear the flags
-- Capture stopOnCastFlag BEFORE clearing it to know if firstaction was active
local wasFirstActionActive = CleveRoids.stopOnCastFlag
CleveRoids.stopOnCastFlag = false
-- Also clear stopMacroFlag if it was set by the firstaction mechanism
-- (stopOnCastFlag being true means firstaction mode caused stopMacroFlag to be set)
if wasFirstActionActive and CleveRoids.stopMacroFlag then
CleveRoids.stopMacroFlag = false
end
return true
end
end
function CleveRoids.DoCastSequence(sequence)
if type(sequence) == "string" then
sequence = CleveRoids.GetSequence(sequence)
if not sequence then return end
end
if CleveRoids.currentSequence and not CleveRoids.CheckSpellCast("player") then
CleveRoids.currentSequence = nil
elseif CleveRoids.currentSequence then
return
end
-- If sequence is complete, don't execute - let macro continue to next line
if sequence.complete then
return
end
if sequence.index > 1 and sequence.reset then
for k,_ in pairs(sequence.reset) do
if CleveRoids.kmods[k] and CleveRoids.kmods[k]() then
CleveRoids.ResetSequence(sequence)
break
end
end
end
local active = CleveRoids.GetCurrentSequenceAction(sequence)
if not (active and active.action) then return end
-- Debug: Show sequence state
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"|cff00ffff[Sequence]|r Step %d/%d: '%s'",
sequence.index, table.getn(sequence.list), active.action
))
end
-- Check for stop steps - does nothing, allows macro to fall through
-- Sequence stays on this step until a reset condition fires (target/combat/time/modifier)
-- Usage: /castsequence reset=target Sunder Armor, nil
local actionLower = string.lower(CleveRoids.Trim(active.action))
if actionLower == "nil" or actionLower == "null" or actionLower == "pass" or actionLower == "noop" then
sequence.status = 0
sequence.lastUpdate = GetTime()
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[Sequence]|r Hit stop step, falling through")
end
-- Don't cast anything, just return - allows macro to continue to next line
return
end
sequence.status = 0
sequence.lastUpdate = GetTime()
sequence.expires = 0
-- Capture target GUID for reset=target (only resets on NEW target, not same target)
if sequence.reset and sequence.reset.target and UnitExists("target") then
local targetGuid = CleveRoids.GetGUID("target")
sequence.lastTargetGuid = targetGuid
end
local prevSeq = CleveRoids.currentSequence
CleveRoids.currentSequence = sequence
local actionText = (sequence.cond or "") .. active.action
local resolvedText, conds = CleveRoids.GetParsedMsg(actionText)
-- Check if this is a macro execution {macroname}
local macroName = CleveRoids.GetMacroNameFromAction(active.action)
if macroName then
-- Execute the macro
local success = CleveRoids.ExecuteMacroByName(macroName)
if not success then
CleveRoids.currentSequence = prevSeq
end
return
end
local function cast_by_name(msg)
msg = msg or ""
-- Check specifically for "(Rank" to allow spells like "Faerie Fire (Feral)"
-- to still get their rank appended automatically
if not string.find(msg, "%(Rank") then
local sp = CleveRoids.GetSpell(msg)
local r = (sp and sp.rank) or (sp and sp.highest and sp.highest.rank)
if r and r ~= "" then msg = msg .. "(" .. r .. ")" end
end
-- Let Nampower DLL handle queuing natively via its CastSpellByName hook
CastSpellByName(msg)
return true
end
local attempted = false
if not conds then
attempted = cast_by_name(resolvedText or active.action)
else
local final = CleveRoids.DoWithConditionals(actionText, nil, CleveRoids.FixEmptyTarget, false, CastSpellByName)
if final then
-- DoWithConditionals returns true (boolean) when it already cast the spell,
-- or a string spell name when conditionals were skipped. Only call cast_by_name
-- if we got a string back (spell wasn't cast yet).
if type(final) == "string" then
attempted = cast_by_name(final)
else
attempted = true -- Spell was already cast inside DoWithConditionals
end
end
end
if not attempted then
CleveRoids.currentSequence = prevSeq
end
end
CleveRoids.DoConditionalCancelAura = function(msg)
-- Check stopmacro flag
if CleveRoids.stopMacroFlag then return false end
local s = CleveRoids.Trim(msg or "")
if s == "" then return false end
-- No conditionals? cancel immediately.
if not string.find(s, "%[") then
return CleveRoids.CancelAura(s)
end
-- Has conditionals? Let the framework evaluate them, then run CancelAura.
return CleveRoids.DoWithConditionals(s, nil, CleveRoids.FixEmptyTarget, false, CleveRoids.CancelAura) or false
end
-- PERFORMANCE: Separate periodic cleanup timer (runs every 5 seconds instead of every frame)
CleveRoids.lastCleanupTime = 0
CleveRoids.CLEANUP_INTERVAL = 5 -- Run cleanup every 5 seconds
-- PERFORMANCE: Upvalues for OnUpdate hot path
local GetTime = GetTime
local UnitAffectingCombat = UnitAffectingCombat
local pairs = pairs
local next = next
local IsAltKeyDown = IsAltKeyDown
local IsShiftKeyDown = IsShiftKeyDown
local IsControlKeyDown = IsControlKeyDown
-- PERFORMANCE: Module-level constants (avoid re-creation per frame)
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 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 CR.stopMacroFlag then
CR.stopMacroFlag = false
end
if CR.skipMacroFlag then
CR.skipMacroFlag = false
end
if CR.stopOnCastFlag then
CR.stopOnCastFlag = false
end
-- PERFORMANCE: Single GetTime() call per frame
local time = GetTime()
-- Coalesced macro/spell/action-bar rebuild (armed by UPDATE_MACROS,
-- SPELLS_CHANGED and PLAYER_LOGIN). Debounced so the login burst -
-- SPELLS_CHANGED fires several times as the spellbook populates - collapses
-- into a single rebuild. Runs even before `ready` so spells/talents are
-- indexed before the init timer's first action-bar pass; RebuildMacros
-- itself skips the 120-slot action-bar rebuild until ready.
if CR.macroRebuildTime and time >= CR.macroRebuildTime then
CR.macroRebuildTime = nil
CR.RebuildMacros()
end
-- PERFORMANCE: Early exit if not ready (before any other checks)
if not CR.ready then
-- Handle initialization timer only when not ready
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
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.
CR._suppressActionHandlers = true
CR.IndexActionBars()
CR._suppressActionHandlers = false
CR.TestForAllActiveActions()
-- First publish of the session. Deferred to here rather than done at
-- load because SetMacroDisplay returns false until the player exists
-- in the world.
CR.PublishAllDisplays()
CR.lastUpdate = time
-- FIX: Force refresh ALL Blizzard action buttons after initialization
-- Blizzard queries IsUsableAction during login before we're ready,
-- caching the wrong state. We must force a re-query for all buttons.
-- SKIP for pfUI users - pfUI handles its own button updates and this
-- would trigger 120 rapid handler calls causing issues.
if not (pfUI and pfUI.bars) then
for slot = 1, 120 do
if CR.Actions[slot] then
CR.SendEventForAction(slot, "ACTIONBAR_SLOT_CHANGED", slot)
end
end
end
end
return
end
-- Coalesced re-index after async item data arrives (GET_ITEM_INFO_RECEIVED).
-- ClassicAPI warms the item cache asynchronously, so items that were still
-- uncached during an earlier index pass land here once their
-- SMSG_ITEM_QUERY_SINGLE response resolves. Bursts are debounced into one
-- re-index via CR.itemInfoReindexTime (armed by the event handler).
if CR.itemInfoReindexTime and time >= CR.itemInfoReindexTime then
CR.itemInfoReindexTime = nil
-- In combat: drop it; PLAYER_LEAVE_COMBAT does a full re-index once safe.
if not UnitAffectingCombat("player") then
CR.lastItemIndexTime = GetTime()
CR.IndexItems()
CR.Actions = {}
CR.Macros = {}
CR.IndexActionBars()
end
end
-- PERFORMANCE: Cache refresh rate calculation (avoid per-frame division)
local refreshRate = CR.cachedRefreshRate
if not refreshRate then
refreshRate = 1 / (CRM.refresh or 5)
CR.cachedRefreshRate = refreshRate
end
-- PERFORMANCE: Throttle check FIRST - skip most work on non-throttled frames
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
CR.lastUpdate = time
-- LeafVillageAchievements/Legends also hook SendChatMessage and can bump ours
-- off the top of the chain (orphaning it / decorating #showtooltip). Reclaim
-- the top when they're present so #showtooltip stays filtered.
if not CR._watchChatHooks then
CR._watchChatHooks = (C_AddOns.IsAddOnLoaded("LeafVillageAchievements")
or C_AddOns.IsAddOnLoaded("LeafVillageLegends"))
end
if CR._watchChatHooks then
CR.EnsureSendChatMessageHook()
end
-- PERFORMANCE: Check for expired reactive procs only if we have any
-- Use statically allocated removal buffer to avoid per-frame allocation
local reactiveProcs = CR.reactiveProcs
if reactiveProcs then
local hasExpiredProc = false
local toRemove = CR._procRemovalBuffer
local removeCount = 0
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
local spellName, procData = next(reactiveProcs)
while spellName do
if procData and procData.expiry and time >= procData.expiry then
removeCount = removeCount + 1
toRemove[removeCount] = spellName
hasExpiredProc = true
end
spellName, procData = next(reactiveProcs, spellName)
end
-- Remove expired procs using indexed array (no pairs() overhead)
for i = 1, removeCount do
reactiveProcs[toRemove[i]] = nil
toRemove[i] = nil -- Clear for next use
end
-- If any proc expired, immediately update all actions
if hasExpiredProc then
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 CR.NampowerAPI.features.hasKeyEvents then
local altDown = IsAltKeyDown()
local shiftDown = IsShiftKeyDown()
local ctrlDown = IsControlKeyDown()
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 CRM.realtime == 0 then
CR.isActionUpdateQueued = true
end
end
end
-- Check the saved variable to decide which update mode to use.
if CRM.realtime == 1 then
-- Realtime Mode: Force an update on every throttled tick for maximum responsiveness.
CR.TestForAllActiveActions()
else
-- Event-Driven Mode (Default): Only update if a relevant game event has queued it.
if CR.isActionUpdateQueued then
CR.TestForAllActiveActions()
CR.isActionUpdateQueued = false
end
end
-- The rest of this function handles time-based logic that must always run.
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 = 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
CR.ResetSequence(sequence)
end
seqKey, sequence = next(Sequences, seqKey)
end
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
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
if cast.expires and time > cast.expires then
spell_tracking[guid] = nil
end
guid, cast = nextGuid, nextGuid and spell_tracking[nextGuid]
end
-- Clean stale castTracking entries. Instant casts are stored with endTime = nil
-- (SPELL_START sets it only when the cast has a duration), so an endTime-only
-- check could never evict them and every instant cast by every unit in range
-- accumulated forever. Fall back to startTime for those: the matching SPELL_GO
-- lands within a frame or two, so anything still here after the grace period is
-- an entry whose completion we never observed.
local ct = CR.castTracking
local ctGuid, ctEntry = next(ct)
while ctGuid do
local nextCtGuid = next(ct, ctGuid)
if ctEntry then
if ctEntry.endTime then
if time > ctEntry.endTime + 0.5 then
ct[ctGuid] = nil
end
elseif ctEntry.startTime and time > ctEntry.startTime + 1 then
-- No endTime: an instant cast. Its SPELL_GO lands within a frame or
-- two, so a second is generous, and anything older is an entry whose
-- completion we never saw.
ct[ctGuid] = nil
end
end
ctGuid = nextCtGuid
ctEntry = nextCtGuid and ct[nextCtGuid]
end
-- 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 - 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 = CR.carnageDurationOverrides
if carnageOverrides then
local spellID, data = next(carnageOverrides)
while spellID do
local nextID = next(carnageOverrides, spellID)
if data.timestamp and (time - data.timestamp) > 30 then
carnageOverrides[spellID] = nil
end
spellID, data = nextID, nextID and carnageOverrides[nextID]
end
end
-- MEMORY: Clean up old ComboPointTracking entries (older than 60 seconds)
-- PERFORMANCE: Use next() directly instead of pairs() to avoid iterator allocation
local comboTracking = CR.ComboPointTracking
if comboTracking then
local trackName, data = next(comboTracking)
while trackName do
local nextName = next(comboTracking, trackName)
if data.cast_time and (time - data.cast_time) > 60 then
comboTracking[trackName] = nil
end
trackName, data = nextName, nextName and comboTracking[nextName]
end
end
-- MEMORY: Clear spell name caches every 60 seconds (12 cleanup cycles)
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
-- 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
-- Not a display override: this invalidates our per-slot caches when an action is
-- dragged off a slot, so it stays regardless of who draws the button.
CleveRoids.Hooks.PickupAction = PickupAction
function PickupAction(slot)
if not slot then return end
CleveRoids.ClearAction(slot)
CleveRoids.ClearSlot(CleveRoids.actionSlots, slot)
CleveRoids.ClearAction(CleveRoids.reactiveSlots, slot)
return CleveRoids.Hooks.PickupAction(slot)
end
-- These two aliases outlived the overrides they were saved for: ClassicAPI now
-- draws the buttons, but callers still need the unmodified globals, which is all
-- these names are now. IsReactiveUsable (Conditionals.lua) reads the raw usable
-- state of a proxy slot; DeferStopAttack and the auto-attack drift check read the
-- true current-action state, which the old override could not report because it
-- only echoed our cached flag.
CleveRoids.Hooks.OriginalIsUsableAction = IsUsableAction
CleveRoids.Hooks.IsCurrentAction = IsCurrentAction
local function IsAutoAttackSpell(spell)
if not spell then return false end
if C_Spell and C_Spell.IsAutoAttackSpell and spell.id then
return C_Spell.IsAutoAttackSpell(spell.id)
end
if C_SpellBook and C_SpellBook.IsAutoAttackSpellBookItem and spell.spellSlot then
return C_SpellBook.IsAutoAttackSpellBookItem(spell.spellSlot, spell.bookType)
end
return false
end
CleveRoids.IsAutoAttackSpell = IsAutoAttackSpell
-- ============================================================================
-- RunMacro Hook - Use our own macro execution system
-- ============================================================================
-- This allows /stopmacro, /skipmacro, /firstaction to work across parent/child
-- macro boundaries without requiring SuperMacro addon.
--
-- When SuperMacro is installed, its RunLine hook takes precedence for extended
-- macro features. This hook handles vanilla Blizzard macros.
-- ============================================================================
if not CleveRoids.RunMacroHooked then
CleveRoids.Hooks.RunMacro = RunMacro
function RunMacro(indexOrName)
-- Skip if SuperMacro is handling macro execution (it has its own hooks)
if CleveRoids.SM_RunLineHooked then
return CleveRoids.Hooks.RunMacro(indexOrName)
end
-- Resolve macro index
local macroIndex
if type(indexOrName) == "number" then
macroIndex = indexOrName
elseif type(indexOrName) == "string" then
-- Could be a name or a numeric string
local num = tonumber(indexOrName)
if num then
macroIndex = num
else
macroIndex = GetMacroIndexByName(indexOrName)
end
end
if not macroIndex or macroIndex == 0 then
-- Fallback to original if we can't resolve
return CleveRoids.Hooks.RunMacro(indexOrName)
end
-- Get macro body
local name, icon, body = GetMacroInfo(macroIndex)
if not body or body == "" then
return CleveRoids.Hooks.RunMacro(indexOrName)
end
-- Clear macro flags at the start of top-level macro execution
CleveRoids.stopMacroFlag = false
CleveRoids.skipMacroFlag = false
-- Note: stopOnCastFlag is intentionally NOT cleared here - it persists
-- within a frame and is cleared by OnUpdate
if CleveRoids.macroRefDebug then
CleveRoids.Print("|cff00ff00[RunMacro Hook]|r Executing macro '" .. (name or macroIndex) .. "' via CleveRoids")
end
-- Execute through our system
CleveRoids.ExecuteMacroBody(body)
-- Don't call original - we've handled it
return
end
CleveRoids.RunMacroHooked = true
end
-- This single frame drives the addon's event loop and OnUpdate. (Spell cost /
-- reagent lookups now read Spell.dbc directly via ClassicAPI -- no tooltip scan.)
CleveRoids.Frame = CreateFrame("Frame")
CleveRoids.Frame:SetScript("OnUpdate", CleveRoids.OnUpdate)
-- Declared with no parameters on purpose. In 1.12's Lua 5.0 a function written
-- `function(...)` builds a fresh `arg` table on every single call, and this fires
-- for all ~48 registered events -- including the UNIT_HEALTH / UNIT_AURA /
-- UNIT_*_GUID power streams, which tick continuously for every unit in range. That
-- was a table per event, purely as garbage: the handler reads the event globals
-- (event, this, arg1..arg10), never the vararg.
CleveRoids.Frame:SetScript("OnEvent", function()
CleveRoids.Frame[event](this,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9,arg10)
end)
-- == SHUTDOWN PROTECTION ==
-- Flag to prevent SuperWoW API calls during logout (crash prevention)
CleveRoids.isShuttingDown = false
-- == CORE EVENT REGISTRATION ==
CleveRoids.Frame:RegisterEvent("PLAYER_LOGIN")
CleveRoids.Frame:RegisterEvent("ADDON_LOADED")
CleveRoids.Frame:RegisterEvent("PLAYER_LOGOUT")
CleveRoids.Frame:RegisterEvent("PLAYER_LEAVING_WORLD")
CleveRoids.Frame:RegisterEvent("PLAYER_ENTERING_WORLD")
CleveRoids.Frame:RegisterEvent("UPDATE_MACROS")
CleveRoids.Frame:RegisterEvent("SPELLS_CHANGED")
CleveRoids.Frame:RegisterEvent("ACTIONBAR_SLOT_CHANGED")
CleveRoids.Frame:RegisterEvent("BAG_UPDATE_DELAYED")
CleveRoids.Frame:RegisterEvent("GET_ITEM_INFO_RECEIVED")
CleveRoids.Frame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
CleveRoids.Frame:RegisterEvent("UNIT_PET")
CleveRoids.Frame:RegisterEvent("UNIT_HAPPINESS")
-- == STATE CHANGE EVENT REGISTRATION (for performance) ==
CleveRoids.Frame:RegisterEvent("PLAYER_TARGET_CHANGED")
CleveRoids.Frame:RegisterEvent("PLAYER_FOCUS_CHANGED") -- For focus addons
CleveRoids.Frame:RegisterEvent("PLAYER_ENTER_COMBAT") -- Auto-attack started
CleveRoids.Frame:RegisterEvent("PLAYER_LEAVE_COMBAT") -- Auto-attack stopped
CleveRoids.Frame:RegisterEvent("PLAYER_REGEN_DISABLED") -- Entered actual combat (has threat)
CleveRoids.Frame:RegisterEvent("PLAYER_REGEN_ENABLED") -- Left actual combat (no threat)
CleveRoids.Frame:RegisterEvent("UPDATE_SHAPESHIFT_FORM")
CleveRoids.Frame:RegisterEvent("SPELL_UPDATE_COOLDOWN")
CleveRoids.Frame:RegisterEvent("PLAYER_STARTED_MOVING")
-- ClassicAPI loss-of-control (school-interrupt lockout) for [locked]/[nolocked].
-- Gated on the namespace so an older ClassicAPI without it doesn't error on an
-- unknown event.
if type(C_LossOfControl) == "table" then
CleveRoids.Frame:RegisterEvent("LOSS_OF_CONTROL_ADDED")
CleveRoids.Frame:RegisterEvent("LOSS_OF_CONTROL_UPDATE")
end
-- The unit state streams that drive icon refresh: GUID events when Nampower
-- provides them (v2.39+, one event per unit change rather than one per token),
-- else the stock per-token events. These cannot become RegisterUnitEvent calls
-- -- the handlers ignore the unit and refresh every macro, because a conditional
-- may name any unit ([@party3,hp:<50], @focus, @mouseover), so narrowing the
-- token set would leave those icons stale.
local unitStreamEvents
if CleveRoids.NampowerAPI.features.hasUnitGuidEvents then
unitStreamEvents = { "UNIT_AURA_GUID", "UNIT_HEALTH_GUID", "UNIT_MANA_GUID", "UNIT_RAGE_GUID", "UNIT_ENERGY_GUID" }
else
unitStreamEvents = { "UNIT_AURA", "UNIT_HEALTH", "UNIT_POWER" }
end
-- They do fire continuously for every unit in range, and in realtime mode their
-- handlers do nothing at all: the OnUpdate refreshes on every throttled tick and
-- QueueActionUpdate no-ops. Rather than pay a Lua dispatch per event to return
-- early, drop the registrations entirely while realtime is on. Re-applied at
-- VARIABLES_LOADED (when the saved value is first known) and whenever
-- `/cleveroid realtime` flips it.
function CleveRoids.ApplyUnitStreamEvents()
local eventDriven = not CleveRoidMacros or CleveRoidMacros.realtime == 0
for i = 1, table.getn(unitStreamEvents) do
if eventDriven then
CleveRoids.Frame:RegisterEvent(unitStreamEvents[i])
else
CleveRoids.Frame:UnregisterEvent(unitStreamEvents[i])
end
end
end
CleveRoids.ApplyUnitStreamEvents()
if CleveRoids.hasSuperwow then
CleveRoids.Frame:RegisterEvent("UNIT_CASTEVENT")
end
if QueueSpellByName then
CleveRoids.Frame:RegisterEvent("SPELL_QUEUE_EVENT")
CleveRoids.Frame:RegisterEvent("SPELL_CAST_EVENT")
end
CleveRoids.Frame:RegisterEvent("START_AUTOREPEAT_SPELL")
CleveRoids.Frame:RegisterEvent("STOP_AUTOREPEAT_SPELL")
CleveRoids.Frame:RegisterEvent("SPELLCAST_CHANNEL_START")
CleveRoids.Frame:RegisterEvent("SPELLCAST_CHANNEL_STOP")
CleveRoids.Frame:RegisterEvent("SPELLCAST_START")
CleveRoids.Frame:RegisterEvent("SPELLCAST_STOP")
CleveRoids.Frame:RegisterEvent("SPELLCAST_FAILED")
CleveRoids.Frame:RegisterEvent("SPELLCAST_INTERRUPTED")
-- Nampower SPELL_CAST_EVENT for reliable channel tracking + cast sequence + spell_tracking
if GetCurrentCastingInfo then
CleveRoids.Frame:RegisterEvent("SPELL_CAST_EVENT")
end
-- Nampower v2.25+: SPELL_START_SELF for spell_tracking and cast sequence (Nampower fallback for UNIT_CASTEVENT)
if CleveRoids.NampowerAPI and CleveRoids.NampowerAPI.features and CleveRoids.NampowerAPI.features.hasSpellStartEvents then
CleveRoids.Frame:RegisterEvent("SPELL_START_SELF")
CleveRoids.Frame:RegisterEvent("SPELL_FAILED_SELF")
end
-- Nampower v2.41+: keyboard input events for [keydown:X] conditional
if CleveRoids.NampowerAPI.features.hasKeyEvents then
CleveRoids.Frame:RegisterEvent("KEY_DOWN")
CleveRoids.Frame:RegisterEvent("KEY_UP")
end
-- NOTE: SuperMacro hook installation is handled by Compatibility/SuperMacro.lua
-- which has the complete implementation including the INTERCEPT path for all commands
function CleveRoids.Frame:UNIT_PET()
if arg1 == "player" then
CleveRoids.IndexPetSpells()
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
end
function CleveRoids.Frame:UNIT_HAPPINESS()
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
-- Handle shutdown to prevent SuperWoW API crashes during logout
function CleveRoids.Frame:PLAYER_LOGOUT()
CleveRoids.isShuttingDown = true
this:UnregisterAllEvents()
this:SetScript("OnUpdate", nil)
this:SetScript("OnEvent", nil)
-- Cancel periodic C_Timer tickers
if CleveRoids._cleanupTicker then
CleveRoids._cleanupTicker:Cancel()
end
if CleveRoids._auraCleanupTicker then
CleveRoids._auraCleanupTicker:Cancel()
end
-- Clear spell caches to prevent stale data
if CleveRoids.spellIdCache then
for k in pairs(CleveRoids.spellIdCache) do
CleveRoids.spellIdCache[k] = nil
end
end
end
function CleveRoids.Frame:PLAYER_LEAVING_WORLD()
-- FIX: Only set loading flag, don't unregister events
-- PLAYER_LEAVING_WORLD fires for both logout AND zone transitions
-- We need to stay alive for zone transitions; PLAYER_LOGOUT handles actual logout
CleveRoids.isLoading = true
end
-- FIX: Handle zone transitions (instance entry, etc.)
-- Refresh action button state after loading screen completes
function CleveRoids.Frame:PLAYER_ENTERING_WORLD()
CleveRoids.isLoading = false
-- Skip if not yet initialized
if not CleveRoids.ready then return end
-- Refresh all action states after zone transition
CleveRoids.TestForAllActiveActions()
-- FIX: Force Blizzard buttons to re-query IsUsableAction
-- Loading screen causes UI rebuild which can cache stale button states
-- SKIP for pfUI users - pfUI handles its own button updates
if not (pfUI and pfUI.bars) then
for slot = 1, 120 do
if CleveRoids.Actions[slot] then
CleveRoids.SendEventForAction(slot, "ACTIONBAR_SLOT_CHANGED", slot)
end
end
end
end
-- Simplified PLAYER_LOGIN - requirements already checked
function CleveRoids.Frame:PLAYER_LOGIN()
-- Skip if already disabled
if CleveRoids.disabled then return end
_, CleveRoids.playerClass = UnitClass("player")
CleveRoids.hasRelicSlot = UnitHasRelicSlot("player") and true or false
CleveRoids.IndexSpells()
CleveRoids.IndexPetSpells()
CleveRoids.initializationTimer = GetTime() + 1.5
-- Guarantee a full index (talents + macros + action bars) even if
-- UPDATE_MACROS / SPELLS_CHANGED happen not to fire before the init timer.
-- Coalesces with those events' arming; RebuildMacros runs once for the burst.
CleveRoids.macroRebuildTime = GetTime() + 0.3
-- PERFORMANCE: Initialize event-driven cache states
CleveRoids._cachedPlayerInCombat = UnitAffectingCombat("player") and true or false
end
function CleveRoids.Frame:ADDON_LOADED(addon)
-- keep your existing init for CRM:
if addon == "CleveRoidMacros" or addon == "SuperCleveRoidMacros" then
CleveRoids.InitializeExtensions()
end
-- NOTE: SuperMacro hook installation is handled by Compatibility/SuperMacro.lua
end
function CleveRoids.Frame:UNIT_CASTEVENT(caster,target,action,spell_id,cast_time)
-- Handle melee swings for judgement refresh
if action == "MAINHAND" or action == "OFFHAND" then
-- Only process if this is the player's melee swing
if caster == CleveRoids.playerGuid and CleveRoids.playerClass == "PALADIN" then
-- Refresh judgements on the target
-- Defensive: verify libdebuff is a table before accessing properties
local lib = type(CleveRoids.libdebuff) == "table" and CleveRoids.libdebuff or nil
if target and lib and lib.objects then
local normalizedTarget = CleveRoids.NormalizeGUID(target)
if normalizedTarget and lib.objects[normalizedTarget] then
-- Refresh all active Judgements on the target
for spellID, rec in pairs(lib.objects[normalizedTarget]) do
-- Check if this is a judgement by spell ID
if lib.judgementSpells and lib.judgementSpells[spellID] and rec.start and rec.duration then
-- Only refresh if the Judgement is still active and was cast by player
local remaining = rec.duration + rec.start - GetTime()
if remaining > 0 and rec.caster == "player" then
-- Refresh the Judgement by updating the start time
rec.start = GetTime()
local spellName = C_Spell.GetSpellName(spellID)
local baseName = CleveRoids.StripRank(spellName) or "Unknown"
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ffaa[Judgement Refresh]|r Refreshed %s (ID:%d) on %s hit - new duration: %ds",
baseName, spellID, action, rec.duration)
)
end
-- Also sync to pfUI if it's loaded (pre-7.6 only)
if pfUI and pfUI.api and pfUI.api.libdebuff then
local targetName = (lib.guidToName and lib.guidToName[normalizedTarget]) or UnitName("target")
local targetLevel = UnitLevel("target") or 0
if targetName then
pfUI.api.libdebuff:AddEffect(targetName, targetLevel, baseName, rec.duration, "player")
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ffaa[pfUI Judgement Refresh]|r Synced %s refresh to pfUI", baseName)
)
end
end
end
end
end
end
end
end
end
return -- Still return early after processing melee
end
-- Debug channel tracking
if CleveRoids.ChannelTimeDebug then
local spellName = spell_id and C_Spell.GetSpellName(spell_id) or "Unknown"
if string.find(spellName, "Arcane") then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[UNIT_CASTEVENT]|r %s: %s (ID:%s) caster=%s player=%s",
action, spellName, tostring(spell_id), tostring(caster), tostring(CleveRoids.playerGuid)))
end
end
-- handle cast spell tracking
local cast = CleveRoids.spell_tracking[caster]
if cast_time > 0 and action == "START" or action == "CHANNEL" then
CleveRoids.spell_tracking[caster] = { spell_id = spell_id, expires = GetTime() + cast_time/1000, type = action }
-- ALSO store under "player" literal for easier lookup
if caster == CleveRoids.playerGuid then
CleveRoids.spell_tracking["player"] = CleveRoids.spell_tracking[caster]
-- For CHANNEL events, capture duration for checkchanneled conditional
if action == "CHANNEL" then
CleveRoids.channelStartTime = GetTime()
-- 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()
-- 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
if CleveRoids.ChannelTimeDebug and caster == CleveRoids.playerGuid then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[Tracking]|r Set spell_tracking[%s] AND [player]: type=%s, expires=%.2f",
tostring(caster), action, GetTime() + cast_time/1000))
end
-- Always show for channels if debug is on
if CleveRoids.ChannelTimeDebug and action == "CHANNEL" then
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cff00ff00[Tracking]|r caster=%s, playerGuid=%s, match=%s",
tostring(caster), tostring(CleveRoids.playerGuid), tostring(caster == CleveRoids.playerGuid)))
end
elseif cast
and (
(cast.spell_id == spell_id and (action == "FAIL" or action == "CAST"))
or (GetTime() > cast.expires)
)
then
if CleveRoids.ChannelTimeDebug and caster == CleveRoids.playerGuid then
local reason = ""
if cast.spell_id == spell_id and (action == "FAIL" or action == "CAST") then
reason = string.format("spell_id match (%s) and action=%s", tostring(spell_id), action)
elseif GetTime() > cast.expires then
reason = string.format("expired (%.2f > %.2f)", GetTime(), cast.expires)
end
DEFAULT_CHAT_FRAME:AddMessage(string.format("|cffff0000[Tracking]|r CLEARING spell_tracking - %s", reason))
end
CleveRoids.spell_tracking[caster] = nil
-- Also clear "player" literal if this is the player
if caster == CleveRoids.playerGuid then
CleveRoids.spell_tracking["player"] = nil
end
end
-- handle cast sequence (SuperWoW)
if CleveRoids.currentSequence and caster == CleveRoids.playerGuid then
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
local name = C_Spell.GetSpellName(spell_id)
local rank = C_Spell.GetSpellSubtext(spell_id)
local nameRank = (rank and rank ~= "") and (name .. "(" .. rank .. ")") or nil
local isSeqSpell = active and active.action and (
active.action == name or
(nameRank and active.action == nameRank)
)
if isSeqSpell then
local status = CleveRoids.currentSequence.status
if status == 0 and (action == "START" or action == "CHANNEL") and cast_time > 0 then
-- cast_time is ms; GetTime() is seconds
CleveRoids.currentSequence.status = 1
CleveRoids.currentSequence.expires = GetTime() + (cast_time / 1000) - 2
elseif (status == 0 and action == "CAST" and cast_time == 0)
or (status == 1 and action == "CAST" and CleveRoids.currentSequence.expires) then
CleveRoids.currentSequence.status = 2
CleveRoids.currentSequence.lastUpdate = GetTime()
CleveRoids.AdvanceSequence(CleveRoids.currentSequence)
CleveRoids.currentSequence = nil
elseif action == "INTERRUPTED" or action == "FAILED" then
CleveRoids.currentSequence.status = 1
end
end
end
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
-- Nampower SPELL_CAST_EVENT handler for reliable channel tracking
-- Also handles spell_tracking clearing and cast sequence advancement (Nampower fallback for UNIT_CASTEVENT)
-- Nampower SPELL_CAST_EVENT. This one handler covers three separate concerns that
-- used to live in two same-named functions, the second of which silently replaced
-- the first (Lua assigns in file order), so the channel/sequence half never ran:
-- 1. [casting] conditional state + cast bookkeeping (lastCastSpell, pendingCasts,
-- combo-point capture for finishers).
-- 2. Channel start detection.
-- 3. The Nampower fallback for spell_tracking and cast-sequence advancement, used
-- when SuperWoW is absent and UNIT_CASTEVENT never fires.
-- Args arrive from the dispatcher as (self, arg1..arg10).
function CleveRoids.Frame:SPELL_CAST_EVENT(success, spellId, castType, targetGuid, itemId)
local CHANNEL = 4
-- BUGFIX: Update casting state on spell cast events (for [casting] conditional)
if CleveRoids.UpdateCastingState then
CleveRoids.UpdateCastingState()
end
if success == 1 then
CleveRoids.lastCastSpell = {
spellId = spellId,
castType = castType,
targetGuid = targetGuid,
timestamp = GetTime()
}
local name = C_Spell.GetSpellName(spellId)
if name then
CleveRoids.lastCastSpell.spellName = name
end
-- Track pending cast for SPELL_GO correlation (reactive ability detection)
-- Keyed by spellId so concurrent casts don't overwrite each other
CleveRoids.pendingCasts = CleveRoids.pendingCasts or {}
-- Clean up consumed entries older than 5 seconds (lightweight, runs per-cast)
local cleanupTime = GetTime() - 5
for id, entry in pairs(CleveRoids.pendingCasts) do
if entry.consumed and entry.consumedAt and entry.consumedAt < cleanupTime then
CleveRoids.pendingCasts[id] = nil
end
end
CleveRoids.pendingCasts[spellId] = {
castType = castType,
targetGuid = targetGuid,
timestamp = GetTime(),
comboPoints = nil,
}
-- Capture combo points NOW (before server consumes them)
-- SPELL_CAST_EVENT fires client-side, so CP are guaranteed available
if (CleveRoids.IsComboScalingSpellID and CleveRoids.IsComboScalingSpellID(spellId)) or
(CleveRoids.FerociousBiteSpellIDs and CleveRoids.FerociousBiteSpellIDs[spellId]) then
local cp = CleveRoids.GetComboPoints and CleveRoids.GetComboPoints() or 0
if cp > 0 then
CleveRoids.pendingCasts[spellId].comboPoints = cp
if CleveRoids.debug then
local castSpellName = C_Spell.GetSpellName(spellId) or "Unknown"
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff00ff88[SPELL_CAST_EVENT]|r Captured %d CP for %s (ID:%d)",
cp, castSpellName, spellId)
)
end
end
end
else
-- Cast failed - clear pending entry for this spell
if CleveRoids.pendingCasts then
CleveRoids.pendingCasts[spellId] = nil
end
end
if castType == CHANNEL and success == 1 then
-- Channel started successfully
CleveRoids.CurrentSpell.type = "channeled"
CleveRoids.CurrentSpell.castingSpellId = spellId
local spellName = C_Spell.GetSpellName(spellId)
if spellName then
CleveRoids.CurrentSpell.spellName = spellName
end
-- Force immediate action update
CleveRoids.TestForAllActiveActions()
end
-- Nampower fallback: spell_tracking and cast sequence (when SuperWoW not available)
if not CleveRoids.hasSuperwow and spellId then
local playerGuid = CleveRoids.playerGuid or CleveRoids.GetGUID("player")
-- Clear spell_tracking on success or failure
if success == 1 or success == 0 then
local cast = CleveRoids.spell_tracking[playerGuid]
if cast and cast.spell_id == spellId then
CleveRoids.spell_tracking[playerGuid] = nil
CleveRoids.spell_tracking["player"] = nil
end
end
-- Cast sequence advancement
if CleveRoids.currentSequence and success == 1 then
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
if active and active.action then
local name = C_Spell.GetSpellName(spellId)
local rank = C_Spell.GetSpellSubtext(spellId)
local nameRank = (rank and rank ~= "") and (name .. "(" .. rank .. ")") or nil
local isSeqSpell = (active.action == name or (nameRank and active.action == nameRank))
if isSeqSpell then
CleveRoids.currentSequence.status = 2
CleveRoids.currentSequence.lastUpdate = GetTime()
CleveRoids.AdvanceSequence(CleveRoids.currentSequence)
CleveRoids.currentSequence = nil
end
end
end
-- Cast sequence failure handling
if CleveRoids.currentSequence and success == 0 then
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
if active and active.action then
local name = C_Spell.GetSpellName(spellId)
local isSeqSpell = (active.action == name)
if isSeqSpell then
CleveRoids.currentSequence.status = 1 -- Reset to retry
end
end
end
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
end
-- Nampower SPELL_START_SELF handler (v2.25+)
-- Handles spell_tracking population and cast sequence START detection (Nampower fallback for UNIT_CASTEVENT)
function CleveRoids.Frame:SPELL_START_SELF(casterGuid, targetGuid, spellId, castTimeMs, durationMs, spellType, ...)
-- Skip if SuperWoW is handling this via UNIT_CASTEVENT
if CleveRoids.hasSuperwow then return end
if not spellId then return end
local playerGuid = CleveRoids.playerGuid or CleveRoids.GetGUID("player")
if casterGuid ~= playerGuid then return end
-- Populate spell_tracking (equivalent to UNIT_CASTEVENT START/CHANNEL)
local isChannel = (spellType == 1)
local action = isChannel and "CHANNEL" or "START"
if castTimeMs and castTimeMs > 0 then
CleveRoids.spell_tracking[casterGuid] = {
spell_id = spellId,
expires = GetTime() + castTimeMs / 1000,
type = action
}
CleveRoids.spell_tracking["player"] = CleveRoids.spell_tracking[casterGuid]
-- Channel duration capture (equivalent to UNIT_CASTEVENT CHANNEL)
if isChannel then
CleveRoids.channelStartTime = GetTime()
local tooltipDuration = CleveRoids.GetChannelDurationFromTooltipByID(spellId)
if tooltipDuration then
CleveRoids.channelDuration = tooltipDuration
else
CleveRoids.channelDuration = castTimeMs / 1000
end
end
-- Cast duration capture (equivalent to UNIT_CASTEVENT START)
if not isChannel then
CleveRoids.castStartTime = GetTime()
CleveRoids.castDuration = castTimeMs / 1000
end
end
-- Cast sequence: set status=1 (casting) for cast-time spells
if CleveRoids.currentSequence and castTimeMs and castTimeMs > 0 then
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
if active and active.action then
local name = C_Spell.GetSpellName(spellId)
local rank = C_Spell.GetSpellSubtext(spellId)
local nameRank = (rank and rank ~= "") and (name .. "(" .. rank .. ")") or nil
local isSeqSpell = (active.action == name or (nameRank and active.action == nameRank))
if isSeqSpell and CleveRoids.currentSequence.status == 0 then
CleveRoids.currentSequence.status = 1
CleveRoids.currentSequence.expires = GetTime() + (castTimeMs / 1000) - 2
end
end
end
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
-- Nampower SPELL_FAILED_SELF handler (v2.25+)
-- Clears spell_tracking on failure (Nampower fallback for UNIT_CASTEVENT FAIL)
function CleveRoids.Frame:SPELL_FAILED_SELF(casterGuid, targetGuid, spellId, ...)
-- Skip if SuperWoW is handling this via UNIT_CASTEVENT
if CleveRoids.hasSuperwow then return end
if not spellId then return end
local playerGuid = CleveRoids.playerGuid or CleveRoids.GetGUID("player")
if casterGuid ~= playerGuid then return end
-- Clear spell_tracking
local cast = CleveRoids.spell_tracking[casterGuid]
if cast and cast.spell_id == spellId then
CleveRoids.spell_tracking[casterGuid] = nil
CleveRoids.spell_tracking["player"] = nil
end
-- Cast sequence failure handling
if CleveRoids.currentSequence then
local active = CleveRoids.GetCurrentSequenceAction(CleveRoids.currentSequence)
if active and active.action then
local name = C_Spell.GetSpellName(spellId)
local isSeqSpell = name and (active.action == name)
if isSeqSpell then
CleveRoids.currentSequence.status = 1 -- Reset to retry
end
end
end
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
function CleveRoids.Frame:SPELLCAST_CHANNEL_START()
-- Set channel state immediately when event fires
-- Duration is captured by UNIT_CASTEVENT which fires earlier
CleveRoids.CurrentSpell.type = "channeled"
-- Try to get spell info - prefer GetCastInfo (Nampower 2.18+) for better data
local spellId = nil
if GetCastInfo then
local ok, info = pcall(GetCastInfo)
if ok and info and info.spellId and info.spellId > 0 then
spellId = info.spellId
-- Also capture timing data
CleveRoids.CurrentSpell.castRemainingMs = info.castRemainingMs
CleveRoids.CurrentSpell.castEndTime = info.castEndS
end
end
-- Fallback to GetCurrentCastingInfo for older Nampower
if not spellId and GetCurrentCastingInfo then
local _, visId = GetCurrentCastingInfo()
if visId and visId > 0 then
spellId = visId
end
end
-- Update spell info
if spellId then
CleveRoids.CurrentSpell.castingSpellId = spellId
local spellName = C_Spell.GetSpellName(spellId)
if spellName then
CleveRoids.CurrentSpell.spellName = spellName
end
end
-- v2.38+: Override channelDuration with the accurate server-provided value stored by
-- SPELL_START_SELF (which fires before this event). This beats the tooltip scan or
-- UNIT_CASTEVENT cast_time fallback, because the server already applied all modifiers.
if CleveRoids._v238ChannelDuration then
if not spellId or not CleveRoids._v238ChannelSpellId
or CleveRoids._v238ChannelSpellId == spellId then
CleveRoids.channelDuration = CleveRoids._v238ChannelDuration
end
CleveRoids._v238ChannelDuration = nil
CleveRoids._v238ChannelSpellId = nil
end
-- Force immediate action update
CleveRoids.TestForAllActiveActions()
end
function CleveRoids.Frame:SPELLCAST_CHANNEL_STOP()
-- Channel ended - clear state immediately
CleveRoids.CurrentSpell.type = ""
CleveRoids.CurrentSpell.spellName = ""
CleveRoids.CurrentSpell.castingSpellId = nil
-- WARLOCK DARK HARVEST: Mark channeling as ended
-- Credits: Avitasia / Cursive addon
if CleveRoids.darkHarvestData and CleveRoids.darkHarvestData.isActive then
CleveRoids.darkHarvestData.isActive = false
CleveRoids.darkHarvestData.endTime = GetTime()
-- Apply Dark Harvest end to all DoTs on target (finalizes reduction)
if CleveRoids.libdebuff and CleveRoids.libdebuff.ApplyDarkHarvestEnd then
CleveRoids.libdebuff.ApplyDarkHarvestEnd(CleveRoids.darkHarvestData.targetGUID)
end
if CleveRoids.debug then
local activeTime = CleveRoids.darkHarvestData.endTime - CleveRoids.darkHarvestData.startTime
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cff9482c9[Dark Harvest]|r Channel ended after %.1fs (DoT acceleration stopped)",
activeTime)
)
end
end
-- Force immediate action update
CleveRoids.TestForAllActiveActions()
end
function CleveRoids.Frame:SPELLCAST_START()
-- Cast-time spell started
-- Duration is captured by UNIT_CASTEVENT which fires earlier
CleveRoids.CurrentSpell.type = "cast"
-- Try to get spell info - prefer GetCastInfo (Nampower 2.18+) for better data
local spellId = nil
if GetCastInfo then
local ok, info = pcall(GetCastInfo)
if ok and info and info.spellId and info.spellId > 0 then
spellId = info.spellId
-- Also capture timing data
CleveRoids.CurrentSpell.castRemainingMs = info.castRemainingMs
CleveRoids.CurrentSpell.castEndTime = info.castEndS
CleveRoids.CurrentSpell.gcdRemainingMs = info.gcdRemainingMs
CleveRoids.CurrentSpell.gcdEndTime = info.gcdEndS
end
end
-- Fallback to GetCurrentCastingInfo for older Nampower
if not spellId and GetCurrentCastingInfo then
local castId = GetCurrentCastingInfo()
if castId and castId > 0 then
spellId = castId
end
end
-- Update spell info
if spellId then
CleveRoids.CurrentSpell.castingSpellId = spellId
local spellName = C_Spell.GetSpellName(spellId)
if spellName then
CleveRoids.CurrentSpell.spellName = spellName
end
end
-- Force immediate action update
CleveRoids.TestForAllActiveActions()
end
function CleveRoids.Frame:SPELLCAST_STOP()
-- Cast finished - clear state immediately
if CleveRoids.CurrentSpell.type == "cast" then
CleveRoids.CurrentSpell.type = ""
CleveRoids.CurrentSpell.spellName = ""
CleveRoids.CurrentSpell.castingSpellId = nil
-- Force immediate action update
CleveRoids.TestForAllActiveActions()
end
end
function CleveRoids.Frame:SPELLCAST_FAILED()
-- Cast failed - clear state immediately
if CleveRoids.CurrentSpell.type == "cast" then
CleveRoids.CurrentSpell.type = ""
CleveRoids.CurrentSpell.spellName = ""
CleveRoids.CurrentSpell.castingSpellId = nil
-- Force immediate action update
CleveRoids.TestForAllActiveActions()
end
end
function CleveRoids.Frame:SPELLCAST_INTERRUPTED()
-- Cast interrupted - clear state immediately
if CleveRoids.CurrentSpell.type == "cast" then
CleveRoids.CurrentSpell.type = ""
CleveRoids.CurrentSpell.spellName = ""
CleveRoids.CurrentSpell.castingSpellId = nil
-- Force immediate action update
CleveRoids.TestForAllActiveActions()
end
end
-- PLAYER_ENTER_COMBAT/PLAYER_LEAVE_COMBAT are for AUTO-ATTACK state (not actual combat)
function CleveRoids.Frame:PLAYER_ENTER_COMBAT()
CleveRoids.CurrentSpell.autoAttack = true
CleveRoids.CurrentSpell.autoAttackLock = false
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
function CleveRoids.Frame:PLAYER_LEAVE_COMBAT()
CleveRoids.CurrentSpell.autoAttack = false
CleveRoids.CurrentSpell.autoAttackLock = false
-- Full re-index after combat to refresh any items/bags that were skipped
-- during combat for performance. Use a slight delay to avoid spam.
local now = GetTime()
if (now - (CleveRoids.lastItemIndexTime or 0)) > 0.5 then
CleveRoids.lastItemIndexTime = now
CleveRoids.IndexItems()
CleveRoids.Actions = {}
CleveRoids.Macros = {}
CleveRoids.IndexActionBars()
end
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
-- PLAYER_REGEN_DISABLED/PLAYER_REGEN_ENABLED are for ACTUAL combat state (threat/aggro)
-- These events track what UnitAffectingCombat("player") reports
function CleveRoids.Frame:PLAYER_REGEN_DISABLED()
-- PERFORMANCE: Cache actual combat state for event-driven [combat]/[nocombat] conditionals
CleveRoids._cachedPlayerInCombat = true
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
function CleveRoids.Frame:PLAYER_REGEN_ENABLED()
-- PERFORMANCE: Cache actual combat state for event-driven [combat]/[nocombat] conditionals
CleveRoids._cachedPlayerInCombat = false
-- Reset any sequence with reset=combat that has progressed past the first step
-- Uses PLAYER_REGEN_ENABLED (actual combat ends) instead of PLAYER_LEAVE_COMBAT (auto-attack stops)
for _, sequence in pairs(CleveRoids.Sequences) do
if sequence.index > 1 and sequence.reset and sequence.reset.combat then
CleveRoids.ResetSequence(sequence)
end
end
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
-- Movement -> [moving]/[nomoving] icon refresh.
-- PLAYER_STARTED_MOVING is reliable; PLAYER_STOPPED_MOVING is not (key-release
-- based, misses geometry/click-to-move/root/knockback stops). So STARTED flips
-- the icon to "moving" and starts a bounded poll; the poll detects the real
-- stop from IsPlayerMoving() (the signal [moving] uses), refreshes once, and
-- shuts itself off. Only runs while actually moving, so no idle cost.
local MOVE_POLL_INTERVAL = 0.1
local moveTicker = nil
local function StopMovePoll()
if moveTicker then
moveTicker:Cancel()
moveTicker = nil
end
end
local function StartMovePoll()
if moveTicker then return end -- already polling this movement
moveTicker = C_Timer.NewTicker(MOVE_POLL_INTERVAL, function()
if CleveRoids.isShuttingDown then
StopMovePoll()
return
end
if not CleveRoids.IsPlayerMoving() then
-- Movement actually ended -- repaint [moving]/[nomoving] icons, stop polling.
StopMovePoll()
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
end)
end
function CleveRoids.Frame:PLAYER_STARTED_MOVING()
-- Started moving: flip to the [moving] icon now...
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
-- ...and watch for the (unreliable-event) stop ourselves.
StartMovePoll()
end
function CleveRoids.Frame:PLAYER_TARGET_CHANGED()
CleveRoids.CurrentSpell.autoAttack = false
CleveRoids.CurrentSpell.autoAttackLock = false
-- Clear resist state when target changes
CleveRoids.ClearResistState()
-- Handle sequences with reset=target - save/restore per-target state
-- Instead of resetting, we remember each target's progress in the sequence
local currentGuid = nil
if UnitExists("target") then
currentGuid = CleveRoids.GetGUID("target")
end
for _, sequence in pairs(CleveRoids.Sequences) do
if sequence.reset and sequence.reset.target then
local oldGuid = sequence.lastTargetGuid
-- Only process if target actually changed
if oldGuid ~= currentGuid then
-- Save current state for the old target (if we had one and progressed)
if oldGuid and sequence.index > 1 then
sequence.perTargetState = sequence.perTargetState or {}
sequence.perTargetState[oldGuid] = {
index = sequence.index,
lastUpdate = sequence.lastUpdate,
}
end
-- Restore state for the new target (or start fresh)
if currentGuid and sequence.perTargetState and sequence.perTargetState[currentGuid] then
-- Restore saved state for this target
local saved = sequence.perTargetState[currentGuid]
sequence.index = saved.index
sequence.lastUpdate = saved.lastUpdate
else
-- New target we haven't seen - start at step 1
sequence.index = 1
sequence.status = 0
end
-- Update the current target GUID
sequence.lastTargetGuid = currentGuid
end
end
end
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
-- Full rebuild of the macro/spell/talent/action-bar index. Invoked from the
-- update loop when macroRebuildTime elapses (armed by UPDATE_MACROS /
-- SPELLS_CHANGED / PLAYER_LOGIN), never inline from an event - so a burst of
-- those events collapses into one rebuild.
function CleveRoids.RebuildMacros()
CleveRoids.currentSequence = nil
CleveRoids.ParsedMsg = {}
CleveRoids.ExpandedGroups = {}
CleveRoids.Macros = {}
CleveRoids.Actions = {}
CleveRoids.Sequences = {}
CleveRoids.IndexSpells()
CleveRoids.IndexTalents()
CleveRoids.IndexPetSpells()
-- Action-bar indexing is wasted before `ready` (GetAction early-returns
-- while not ready), and the +1.5s init timer rebuilds the bars once anyway.
-- Skipping it here keeps the pre-ready login rebuilds from fanning 120 slot
-- updates out to Blizzard/pfUI/Bongos buttons.
if CleveRoids.ready then
CleveRoids.IndexActionBars()
-- Macros were re-parsed above, so every published value is stale. Covers
-- macros that aren't on a bar, which IndexActionBars never visits.
CleveRoids.PublishAllDisplays()
end
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
function CleveRoids.Frame:UPDATE_MACROS()
-- Debounce: collapse bursts (login, rapid macro edits) into one rebuild.
CleveRoids.macroRebuildTime = GetTime() + 0.3
end
function CleveRoids.Frame:SPELLS_CHANGED()
-- Clear spell caches immediately (cheap); defer the heavy rebuild.
CleveRoids.spellIdCache = {}
CleveRoids.spellNameCache = {}
CleveRoids.macroRebuildTime = GetTime() + 0.3
end
function CleveRoids.Frame:ACTIONBAR_SLOT_CHANGED()
CleveRoids.ClearAction(arg1)
CleveRoids.IndexActionSlot(arg1)
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
-- ClassicAPI fires GET_ITEM_INFO_RECEIVED when an async item-cache fill lands
-- (the hooked GetItemInfo auto-warms on a miss). It fires for EVERY fill in the
-- game though - quest DB scans, AH sweeps, chat-link hovers, inspects - so we
-- ignore anything not in pendingItemInfo (items we own that missed the last
-- index pass). That check is O(1) and keeps unrelated bursts free.
function CleveRoids.Frame:GET_ITEM_INFO_RECEIVED()
local pending = CleveRoids.pendingItemInfo
if not (pending and pending[arg1]) then return end
-- Skip during combat; PLAYER_LEAVE_COMBAT re-indexes once it's safe.
if UnitAffectingCombat("player") then return end
-- Arm once per burst; the update loop clears it after re-indexing. Later
-- arrivals re-arm it, guaranteeing the final resolved state is captured.
if not CleveRoids.itemInfoReindexTime then
CleveRoids.itemInfoReindexTime = GetTime() + 0.5
end
end
function CleveRoids.Frame:BAG_UPDATE_DELAYED()
-- In combat: Skip expensive indexing but still queue icon update
-- so conditionals like [inbag] re-evaluate (they use live bag APIs)
if UnitAffectingCombat("player") then
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
return
end
-- Out of combat: Full indexing with throttle
local now = GetTime()
if (now - (CleveRoids.lastItemIndexTime or 0)) > 1.0 then
CleveRoids.lastItemIndexTime = now
CleveRoids.IndexItems()
-- Rebuild action bars so item-dependent macro resolution refreshes.
-- Skipped before `ready` (GetAction early-returns; the +1.5s init timer
-- builds the bars), which keeps bag-fill events during login from firing
-- a full 120-slot rebuild each time.
if CleveRoids.ready then
CleveRoids.Actions = {}
CleveRoids.Macros = {}
CleveRoids.IndexActionBars()
end
end
-- Always queue icon update so conditionals like [inbag] re-evaluate
-- even when full indexing is throttled (they use live bag APIs)
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
function CleveRoids.Frame:PLAYER_EQUIPMENT_CHANGED()
-- arg1 = inventory slot that changed, arg2 = hasCurrent (slot now holds an item)
local slot, hasCurrent = arg1, arg2
-- In combat: skip the slot reindex to avoid lag during rapid gear swapping.
-- [equipped] reads live engine state (C_Item.IsEquippedItem) so it stays
-- correct regardless; the Items table self-heals on the next BAG_UPDATE.
if UnitAffectingCombat("player") then
return
end
-- Out of combat: reindex only the one slot that changed (the bag side of any
-- swap is covered by BAG_UPDATE_DELAYED). No action-bar rebuild is needed -
-- gear swaps don't change what's on the bars; QueueActionUpdate re-evaluates
-- [equipped:...] icons/conditionals.
CleveRoids.IndexEquipSlot(slot, hasCurrent)
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
function CleveRoids.Frame:START_AUTOREPEAT_SPELL()
local spells = CleveRoids.Spells[BOOKTYPE_SPELL]
if spells and spells[CleveRoids.Localized.AutoShot] then
CleveRoids.CurrentSpell.autoShot = true
else
CleveRoids.CurrentSpell.wand = true
end
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
function CleveRoids.Frame:STOP_AUTOREPEAT_SPELL()
local spells = CleveRoids.Spells[BOOKTYPE_SPELL]
if spells and spells[CleveRoids.Localized.AutoShot] then
CleveRoids.CurrentSpell.autoShot = false
else
CleveRoids.CurrentSpell.wand = false
end
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
-- Generic event handlers that just queue an update
function CleveRoids.Frame:PLAYER_FOCUS_CHANGED()
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
-- School-interrupt lockout applied/changed -> refresh [locked]/[nolocked] icons.
function CleveRoids.Frame:LOSS_OF_CONTROL_ADDED()
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
CleveRoids.Frame.LOSS_OF_CONTROL_UPDATE = CleveRoids.Frame.LOSS_OF_CONTROL_ADDED
function CleveRoids.Frame:UPDATE_SHAPESHIFT_FORM()
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
function CleveRoids.Frame:SPELL_UPDATE_COOLDOWN()
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
-- COOLDOWN FIX: Explicitly update cooldowns on all managed action buttons
-- This ensures cooldowns display correctly even when the active action hasn't changed
CleveRoids.UpdateAllManagedCooldowns()
end
-- Helper function to notify addon handlers about cooldown updates.
-- Blizzard's own buttons need nothing from us: ActionButton_OnEvent reads the stock
-- action-bar functions, which resolve through the value published with
-- C_Macro.SetMacroDisplay. This exists only for third-party bars that keep their own
-- cooldown frames -- Bongos does; pfUI's handler ignores the event outright.
--
-- Runs on every SPELL_UPDATE_COOLDOWN (so every GCD and cooldown tick) and touches
-- every managed slot, up to 120. Keep the per-slot work here and in the registered
-- handlers allocation-free.
CleveRoids.cooldownDebugLogged = {}
function CleveRoids.UpdateAllManagedCooldowns()
local Actions = CleveRoids.Actions
if not Actions then return end
local handlers = CleveRoids.actionEventHandlers
local handlerCount = handlers and table.getn(handlers) or 0
if handlerCount == 0 then return end
local slot, actions = next(Actions)
while slot do
if actions then
for i = 1, handlerCount do
handlers[i](slot, "ACTIONBAR_UPDATE_COOLDOWN")
end
end
slot, actions = next(Actions, slot)
end
end
-- PERFORMANCE OPTIMIZATION: Throttled event handlers to reduce CPU spam
-- UNIT_AURA can fire dozens of times per second during combat
-- 100ms throttle reduces calls by 90%+ while maintaining responsiveness
function CleveRoids.Frame:UNIT_AURA()
if CleveRoidMacros.realtime == 0 then
local now = GetTime()
if (now - CleveRoids.lastUnitAuraUpdate) >= CleveRoids.EVENT_THROTTLE then
CleveRoids.lastUnitAuraUpdate = now
CleveRoids.QueueActionUpdate()
end
end
end
function CleveRoids.Frame:UNIT_HEALTH()
if CleveRoidMacros.realtime == 0 then
local now = GetTime()
if (now - CleveRoids.lastUnitHealthUpdate) >= CleveRoids.EVENT_THROTTLE then
CleveRoids.lastUnitHealthUpdate = now
CleveRoids.QueueActionUpdate()
end
end
end
function CleveRoids.Frame:UNIT_POWER()
if CleveRoidMacros.realtime == 0 then
local now = GetTime()
if (now - CleveRoids.lastUnitPowerUpdate) >= CleveRoids.EVENT_THROTTLE then
CleveRoids.lastUnitPowerUpdate = now
CleveRoids.QueueActionUpdate()
end
end
end
-- GUID event handlers (v2.39+): fire once per unit state change instead of per-token
function CleveRoids.Frame:UNIT_AURA_GUID()
if CleveRoidMacros.realtime == 0 then
local now = GetTime()
if (now - CleveRoids.lastUnitAuraUpdate) >= CleveRoids.EVENT_THROTTLE then
CleveRoids.lastUnitAuraUpdate = now
CleveRoids.QueueActionUpdate()
end
end
end
function CleveRoids.Frame:UNIT_HEALTH_GUID()
if CleveRoidMacros.realtime == 0 then
local now = GetTime()
if (now - CleveRoids.lastUnitHealthUpdate) >= CleveRoids.EVENT_THROTTLE then
CleveRoids.lastUnitHealthUpdate = now
CleveRoids.QueueActionUpdate()
end
end
end
-- All power GUID events share the same handler
local function OnPowerGuidEvent()
if CleveRoidMacros.realtime == 0 then
local now = GetTime()
if (now - CleveRoids.lastUnitPowerUpdate) >= CleveRoids.EVENT_THROTTLE then
CleveRoids.lastUnitPowerUpdate = now
CleveRoids.QueueActionUpdate()
end
end
end
CleveRoids.Frame.UNIT_MANA_GUID = OnPowerGuidEvent
CleveRoids.Frame.UNIT_RAGE_GUID = OnPowerGuidEvent
CleveRoids.Frame.UNIT_ENERGY_GUID = OnPowerGuidEvent
function CleveRoids.Frame:SPELL_QUEUE_EVENT()
if event == "SPELL_QUEUE_EVENT" then
local eventCode = arg1
local spellId = arg2
local NORMAL_QUEUED = 2
local NON_GCD_QUEUED = 4
local ON_SWING_QUEUED = 0
local NORMAL_QUEUE_POPPED = 3
local NON_GCD_QUEUE_POPPED = 5
local ON_SWING_QUEUE_POPPED = 1
if eventCode == NORMAL_QUEUED or eventCode == NON_GCD_QUEUED or eventCode == ON_SWING_QUEUED then
CleveRoids.queuedSpell = {
spellId = spellId,
queueType = eventCode,
queueTime = GetTime()
}
local name = C_Spell.GetSpellName(spellId)
if name then
CleveRoids.queuedSpell.spellName = name
end
-- BUGFIX: Update casting state when spell is queued (for [casting] conditional)
if CleveRoids.UpdateCastingState then
CleveRoids.UpdateCastingState()
end
CleveRoids.QueueActionUpdate()
elseif eventCode == NORMAL_QUEUE_POPPED or eventCode == NON_GCD_QUEUE_POPPED or eventCode == ON_SWING_QUEUE_POPPED then
CleveRoids.queuedSpell = nil
-- BUGFIX: Update casting state when spell queue pops (for [casting] conditional)
if CleveRoids.UpdateCastingState then
CleveRoids.UpdateCastingState()
end
CleveRoids.QueueActionUpdate()
end
end
end
-- Nampower v2.41+: KEY_DOWN/KEY_UP events
-- arg1=keyCode (int), arg2=metaKeyState (Shift=1,Ctrl=2,Alt=4), arg3=repeat, arg4=time
-- Meta key codes (0=Shift, 1=Ctrl, 2=Alt) are skipped — they are already tracked via IsXKeyDown()
function CleveRoids.Frame:KEY_DOWN()
local keyCode = arg1
if keyCode == 0 or keyCode == 1 or keyCode == 2 then return end
CleveRoids._keyState[keyCode] = true
CleveRoids.isActionUpdateQueued = true
end
function CleveRoids.Frame:KEY_UP()
local keyCode = arg1
if keyCode == 0 or keyCode == 1 or keyCode == 2 then return end
CleveRoids._keyState[keyCode] = nil
CleveRoids.isActionUpdateQueued = true
end
-- Base SendChatMessage captured at first hook; used to break a hook cycle.
local baseSendChatMessage = SendChatMessage
local sendingChatMessage = false
-- Swallow #showtooltip lines Blizzard's macro executor sends to chat.
local function CleveRoids_SendChatMessage(msg, ...)
if msg and string.find(msg, "^#showtooltip") then
return
end
if sendingChatMessage then
-- Re-entered via a hook cycle; go straight to base to avoid recursion.
return baseSendChatMessage(msg, unpack(arg))
end
sendingChatMessage = true
local ok, err = pcall(CleveRoids.Hooks.SendChatMessage, msg, unpack(arg))
sendingChatMessage = false
if not ok and geterrorhandler then geterrorhandler()(err) end
end
-- Reassert our filter as the outermost SendChatMessage hook (no-op if already on
-- top). See the OnUpdate caller for why this is needed.
function CleveRoids.EnsureSendChatMessageHook()
if SendChatMessage ~= CleveRoids_SendChatMessage then
CleveRoids.Hooks.SendChatMessage = SendChatMessage
SendChatMessage = CleveRoids_SendChatMessage
end
end
CleveRoids.EnsureSendChatMessageHook()
CleveRoids.RegisterActionEventHandler = function(fn)
if type(fn) == "function" then
table.insert(CleveRoids.actionEventHandlers, fn)
end
end
CleveRoids.RegisterMouseOverResolver = function(fn)
if type(fn) == "function" then
table.insert(CleveRoids.mouseOverResolvers, fn)
end
end
CleverMacro = true
do
local f = CreateFrame("Frame")
f:SetScript("OnEvent", function(self, event, arg1)
if event == "PLAYER_LOGIN" then
-- This ensures we wait until the player is fully in the world and all addons are loaded.
self:UnregisterEvent("PLAYER_LOGIN")
-- Ensure both pfUI and its focus module are loaded before attempting to hook.
-- This also checks that the slash command we want to modify exists.
if pfUI and pfUI.uf and pfUI.uf.focus and SlashCmdList.PFFOCUS then
local original_PFFOCUS_Handler = SlashCmdList.PFFOCUS
SlashCmdList.PFFOCUS = function(msg)
-- First, execute the original /focus command from pfUI to set the unit name.
original_PFFOCUS_Handler(msg)
-- Now, if a focus name was set, find the corresponding UnitID.
if pfUI.uf.focus.unitname then
local focusName = pfUI.uf.focus.unitname
local found_label, found_id = nil, nil
-- This function iterates through all known friendly units to find a
-- name match and return its specific UnitID components.
local function findUnitID()
-- Check party members and their pets
for i = 1, GetNumPartyMembers() do
if strlower(UnitName("party"..i) or "") == focusName then
return "party", i
end
if UnitExists("partypet"..i) and strlower(UnitName("partypet"..i) or "") == focusName then
return "partypet", i
end
end
-- Check raid members and their pets
for i = 1, GetNumRaidMembers() do
if strlower(UnitName("raid"..i) or "") == focusName then
return "raid", i
end
if UnitExists("raidpet"..i) and strlower(UnitName("raidpet"..i) or "") == focusName then
return "raidpet", i
end
end
-- Check player and pet
if strlower(UnitName("player") or "") == focusName then return "player", nil end
if UnitExists("pet") and strlower(UnitName("pet") or "") == focusName then return "pet", nil end
return nil, nil
end
found_label, found_id = findUnitID()
-- Store the found label and ID. CleveRoids' Core.lua will use this
-- for a direct, reliable cast without needing to change your target.
pfUI.uf.focus.label = found_label
pfUI.uf.focus.id = found_id
else
-- Focus was cleared (e.g., via /clearfocus), so ensure our cached data is cleared too.
pfUI.uf.focus.label = nil
pfUI.uf.focus.id = nil
end
end
end
end
end)
f:RegisterEvent("PLAYER_LOGIN")
end
SLASH_CLEVEROID1 = "/cleveroid"
SLASH_CLEVEROID2 = "/cleveroidmacros"
SlashCmdList["CLEVEROID"] = function(msg)
if type(msg) ~= "string" then
msg = ""
end
local cmd, val, val2
local s, e, a, b, c = string.find(msg, "^(%S*)%s*(%S*)%s*(.*)$")
if a then cmd = a else cmd = "" end
if b then val = b else val = "" end
if c then val2 = c else val2 = "" end
-- No command: show current value and available commands
if cmd == "" then
CleveRoids.Print("Current Settings:")
DEFAULT_CHAT_FRAME:AddMessage("realtime (force fast updates, CPU intensive) = " .. CleveRoidMacros.realtime .. " (Default: 0)")
DEFAULT_CHAT_FRAME:AddMessage("refresh (updates per second) = " .. CleveRoidMacros.refresh .. " (Default: 5)")
DEFAULT_CHAT_FRAME:AddMessage("macrocheck (syntax checker) = " .. CleveRoidMacros.macrocheck .. " (Default: 1)")
DEFAULT_CHAT_FRAME:AddMessage("debug (show learning messages) = " .. (CleveRoids.debug and "1" or "0") .. " (Default: 0)")
DEFAULT_CHAT_FRAME:AddMessage(" ")
CleveRoids.Print("Available Commands:")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid realtime 0 or 1 - Force realtime updates")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid refresh X - Set refresh rate (1-10 updates/sec)")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid macrocheck 0 or 1 - Enable/disable macro syntax checker")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid learn <spellID> <duration> - Manually set spell duration")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid forget <spellID|all> - Forget learned duration(s)")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid debug [0|1] - Toggle learning debug messages")
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Spell Schools:|r")
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listschools - List all learned spell schools')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearschools - Clear learned spell school data')
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Immunity Tracking:|r")
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listimmune [school] - List immunity data')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid addimmune "<NPC>" <school> [buff] - Add immunity')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid removeimmune "<NPC>" <school> - Remove immunity')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearimmune [school] - Clear immunity data')
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00CC Immunity Tracking:|r")
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listccimmune [type] - List CC immunity data')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid addccimmune "<NPC>" <type> [buff] - Add CC immunity')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid removeccimmune "<NPC>" <type> - Remove CC immunity')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearccimmune [type] - Clear CC immunity data')
DEFAULT_CHAT_FRAME:AddMessage(" CC types: stun, fear, root, silence, sleep, charm, polymorph, banish, horror, disorient, snare")
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Combo Point Tracking:|r")
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid combotrack - Show combo point tracking info')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid comboclear - Clear combo tracking data')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid combolearn - Show learned combo durations (per CP)')
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Talent Modifiers:|r")
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid talenttabs - Show talent tab IDs for your class')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listtab <tab> - List all talents in a tab with their IDs')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid talents - Show current talent ranks')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid testtalent <spellID> - Test talent modifier for a spell')
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Debuff Tracking Debug:|r")
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid debuffdebug [spell] - Debug debuff tracking on target')
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00pfUI Tank Integration:|r")
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid tankdebug - Debug tank targeting conditionals')
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00All-Caster Aura Tracking:|r")
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid auradebug - Debug buff/debuff time tracking from all casters')
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Overflow Buff Frame:|r")
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid overflowframe [on|off] - Enable/disable player overflow buff frame (default: off)')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid overflowtest - Toggle overflow buff frame test mode')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid overflowreset - Reset overflow frame positions to defaults')
return
end
-- realtime
if cmd == "realtime" then
local num = tonumber(val)
if num == 0 or num == 1 then
CleveRoidMacros.realtime = num
-- The unit streams are only worth receiving in event-driven mode.
CleveRoids.ApplyUnitStreamEvents()
CleveRoids.Print("realtime set to " .. num)
else
CleveRoids.Print("Usage: /cleveroid realtime 0 or 1 - Force realtime updates rather than event based updates (Default: 0. 1 = on, increases CPU load.)")
CleveRoids.Print("Current realtime = " .. tostring(CleveRoidMacros.realtime))
end
return
end
-- refresh
if cmd == "refresh" then
local num = tonumber(val)
if num and num >= 1 and num <= 10 then
CleveRoidMacros.refresh = num
CleveRoids.cachedRefreshRate = nil -- Invalidate cached rate
CleveRoids.Print("refresh set to " .. num .. " times per second")
else
CleveRoids.Print("Usage: /cleveroid refresh X - Set refresh rate. (1 to 10 updates per second. Default: 5)")
CleveRoids.Print("Current refresh = " .. tostring(CleveRoidMacros.refresh) .. " times per second")
end
return
end
-- macrocheck
if cmd == "macrocheck" then
local num = tonumber(val)
if num == 0 or num == 1 then
CleveRoidMacros.macrocheck = num
if num == 1 then
CleveRoids.Print("Macro syntax checker |cff00ff00enabled|r")
else
CleveRoids.Print("Macro syntax checker |cffff0000disabled|r (reload UI to take effect)")
end
else
CleveRoids.Print("Usage: /cleveroid macrocheck 0 or 1 - Enable/disable macro syntax checker (Default: 1)")
CleveRoids.Print("Current macrocheck = " .. tostring(CleveRoidMacros.macrocheck))
end
return
end
-- learn (manual set duration)
if cmd == "learn" then
local spellID = tonumber(val)
local duration = tonumber(val2)
if spellID and duration then
local playerGUID = CleveRoids.GetGUID("player")
CleveRoids_LearnedDurations = CleveRoids_LearnedDurations or {}
CleveRoids_LearnedDurations[spellID] = CleveRoids_LearnedDurations[spellID] or {}
CleveRoids_LearnedDurations[spellID][playerGUID] = duration
local spellName = C_Spell.GetSpellName(spellID) or "Unknown"
CleveRoids.Print("Set " .. spellName .. " (ID:" .. spellID .. ") duration to " .. duration .. "s")
else
CleveRoids.Print("Usage: /cleveroid learn <spellID> <duration> - Manually set spell duration")
CleveRoids.Print("Example: /cleveroid learn 11597 30")
end
return
end
-- forget (delete learned duration)
if cmd == "forget" or cmd == "unlearn" then
if val == "all" then
CleveRoids_LearnedDurations = {}
CleveRoids.Print("Forgot all learned spell durations")
else
local spellID = tonumber(val)
if spellID and CleveRoids_LearnedDurations and CleveRoids_LearnedDurations[spellID] then
local spellName = C_Spell.GetSpellName(spellID) or "Unknown"
CleveRoids_LearnedDurations[spellID] = nil
CleveRoids.Print("Forgot " .. spellName .. " (ID:" .. spellID .. ") duration")
elseif spellID then
CleveRoids.Print("No learned duration for spell ID " .. spellID)
else
CleveRoids.Print("Usage: /cleveroid forget <spellID> - Forget learned duration")
CleveRoids.Print(" /cleveroid forget all - Forget all learned durations")
end
end
return
end
-- debug (toggle learning messages)
if cmd == "debug" then
local num = tonumber(val)
if num == 0 or num == 1 then
CleveRoids.debug = (num == 1)
CleveRoids.Print("debug set to " .. num)
else
CleveRoids.debug = not CleveRoids.debug
CleveRoids.Print("debug " .. (CleveRoids.debug and "enabled" or "disabled"))
end
-- Clear keyed debug state so messages re-fire when debug is re-enabled
CleveRoids._lastDebugState = {}
return
end
-- listimmune (list immunity data)
if cmd == "listimmune" or cmd == "immunelist" then
CleveRoids.ListImmunities(val ~= "" and val or nil)
return
end
-- clearimmune (clear immunity data)
if cmd == "clearimmune" then
CleveRoids.ClearImmunities(val ~= "" and val or nil)
return
end
-- addimmune (manually add immunity)
if cmd == "addimmune" then
-- Parse: /cleveroid addimmune <NPC Name> <school> [buff]
-- Example: /cleveroid addimmune "Golemagg the Incinerator" fire
-- Example: /cleveroid addimmune Vaelastrasz fire "Burning Adrenaline"
local npcName, school, buffName = nil, nil, nil
-- Try to extract quoted NPC name
local _, _, quotedNpc, rest = string.find(msg, '^addimmune%s+"([^"]+)"%s*(.*)$')
if quotedNpc then
npcName = quotedNpc
-- Parse school and optional buff from rest
local _, _, sch, buff = string.find(rest, "^(%S+)%s*(.*)$")
school = sch
if buff and buff ~= "" then
-- Check if buff is quoted
local _, _, quotedBuff = string.find(buff, '^"([^"]+)"$')
buffName = quotedBuff or buff
end
else
-- No quoted NPC, use simple parsing
npcName = val
school = val2
end
CleveRoids.AddImmunity(npcName, school, buffName)
return
end
-- removeimmune (manually remove immunity)
if cmd == "removeimmune" then
-- Parse: /cleveroid removeimmune <NPC Name> <school>
local npcName, school = nil, nil
-- Try to extract quoted NPC name
local _, _, quotedNpc, sch = string.find(msg, '^removeimmune%s+"([^"]+)"%s*(%S*)$')
if quotedNpc then
npcName = quotedNpc
school = sch
else
npcName = val
school = val2
end
CleveRoids.RemoveImmunity(npcName, school)
return
end
-- ========== CC IMMUNITY COMMANDS ==========
-- listccimmune (list CC immunity data)
if cmd == "listccimmune" or cmd == "ccimmunelist" then
CleveRoids.ListCCImmunities(val ~= "" and val or nil)
return
end
-- clearccimmune (clear CC immunity data)
if cmd == "clearccimmune" then
CleveRoids.ClearCCImmunities(val ~= "" and val or nil)
return
end
-- addccimmune (manually add CC immunity)
if cmd == "addccimmune" then
-- Parse: /cleveroid addccimmune <NPC Name> <cctype> [buff]
-- Example: /cleveroid addccimmune "Stone Guardian" stun
-- Example: /cleveroid addccimmune "Boss Name" fear "Enrage"
local npcName, ccType, buffName = nil, nil, nil
-- Try to extract quoted NPC name
local _, _, quotedNpc, rest = string.find(msg, '^addccimmune%s+"([^"]+)"%s*(.*)$')
if quotedNpc then
npcName = quotedNpc
-- Parse ccType and optional buff from rest
local _, _, cct, buff = string.find(rest, "^(%S+)%s*(.*)$")
ccType = cct
if buff and buff ~= "" then
-- Check if buff is quoted
local _, _, quotedBuff = string.find(buff, '^"([^"]+)"$')
buffName = quotedBuff or buff
end
else
-- No quoted NPC, use simple parsing
npcName = val
ccType = val2
end
CleveRoids.AddCCImmunity(npcName, ccType, buffName)
return
end
-- removeccimmune (manually remove CC immunity)
if cmd == "removeccimmune" then
-- Parse: /cleveroid removeccimmune <NPC Name> <cctype>
local npcName, ccType = nil, nil
-- Try to extract quoted NPC name
local _, _, quotedNpc, cct = string.find(msg, '^removeccimmune%s+"([^"]+)"%s*(%S*)$')
if quotedNpc then
npcName = quotedNpc
ccType = cct
else
npcName = val
ccType = val2
end
CleveRoids.RemoveCCImmunityCommand(npcName, ccType)
return
end
-- listschools (show learned spell schools)
if cmd == "listschools" or cmd == "schools" then
CleveRoids.Print("|cff88ff88=== Learned Spell Schools ===|r")
if not CleveRoids_SpellSchools or not next(CleveRoids_SpellSchools) then
CleveRoids.Print("No spell schools learned yet. Deal damage to enemies!")
CleveRoids.Print("Nampower damage events will automatically track spell schools.")
else
local count = 0
local bySchool = {}
-- Group by school
for spellID, school in pairs(CleveRoids_SpellSchools) do
if not bySchool[school] then
bySchool[school] = {}
end
table.insert(bySchool[school], spellID)
count = count + 1
end
CleveRoids.Print("Total spells tracked: " .. count)
CleveRoids.Print(" ")
-- Display by school
for school, spellIDs in pairs(bySchool) do
local schoolColor = "|cff88ff88"
if school == "fire" then schoolColor = "|cffff4400"
elseif school == "frost" then schoolColor = "|cff00ffff"
elseif school == "nature" then schoolColor = "|cff00ff00"
elseif school == "shadow" then schoolColor = "|cff8800ff"
elseif school == "arcane" then schoolColor = "|cffff00ff"
elseif school == "holy" then schoolColor = "|cffffff00"
elseif school == "bleed" then schoolColor = "|cffff0000"
end
CleveRoids.Print(schoolColor .. string.upper(school) .. "|r (" .. table.getn(spellIDs) .. " spells):")
for _, spellID in ipairs(spellIDs) do
local spellName = C_Spell.GetSpellName(spellID) or "Unknown"
CleveRoids.Print(" " .. spellName .. " (ID:" .. spellID .. ")")
end
end
end
return
end
-- clearschools (clear learned spell schools)
if cmd == "clearschools" then
local count = 0
if CleveRoids_SpellSchools then
for _ in pairs(CleveRoids_SpellSchools) do
count = count + 1
end
end
CleveRoids_SpellSchools = {}
CleveRoids.spellSchoolMapping = CleveRoids_SpellSchools
CleveRoids.Print("Cleared " .. count .. " learned spell schools")
return
end
-- combotrack (show combo point tracking info)
if cmd == "combotrack" or cmd == "combo" then
CleveRoids.ShowComboTracking()
return
end
-- comboclear (clear combo tracking data)
if cmd == "comboclear" then
CleveRoids.ComboPointTracking = {}
CleveRoids.ComboPointTracking.byID = {}
CleveRoids.Print("Combo point tracking data cleared")
return
end
-- combolearn (show learned combo durations)
if cmd == "combolearn" or cmd == "combodurations" then
CleveRoids.Print("=== Learned Combo Durations ===")
if not CleveRoids_ComboDurations or not next(CleveRoids_ComboDurations) then
CleveRoids.Print("No learned combo durations yet. Cast finishers and let them expire!")
else
for spellID, cpData in pairs(CleveRoids_ComboDurations) do
local spellName = C_Spell.GetSpellName(spellID) or ("Spell " .. spellID)
CleveRoids.Print(spellName .. " (ID:" .. spellID .. "):")
for cp = 1, 5 do
if cpData[cp] then
CleveRoids.Print(" " .. cp .. " CP = " .. cpData[cp] .. "s")
end
end
end
end
return
end
-- talenttabs (show talent tab IDs)
if cmd == "talenttabs" or cmd == "tabs" then
CleveRoids.Print("=== Talent Tabs ===")
for i = 1, GetNumTalentTabs() do
local name, _, pointsSpent = GetTalentTabInfo(i)
CleveRoids.Print("Tab " .. i .. ": " .. name .. " (" .. pointsSpent .. " points)")
end
return
end
-- listtab (show all talents in a specific tab with their IDs)
if cmd == "listtab" or cmd == "tab" then
local tabNum = tonumber(val)
if not tabNum or tabNum < 1 or tabNum > GetNumTalentTabs() then
CleveRoids.Print("Usage: /cleveroid listtab <tab number>")
CleveRoids.Print("Example: /cleveroid listtab 2")
CleveRoids.Print("Use /cleveroid talenttabs to see your tab numbers")
return
end
local tabName = GetTalentTabInfo(tabNum)
CleveRoids.Print("=== " .. tabName .. " (Tab " .. tabNum .. ") ===")
local numTalents = GetNumTalents(tabNum)
for i = 1, numTalents do
local name, _, _, _, rank, maxRank = GetTalentInfo(tabNum, i)
if name then
local rankText = rank .. "/" .. maxRank
CleveRoids.Print("ID " .. i .. ": " .. name .. " [" .. rankText .. "]")
end
end
return
end
-- talents (show current talent ranks)
if cmd == "talents" or cmd == "talent" then
CleveRoids.Print("=== Current Talents ===")
-- Ensure talents are indexed
if not CleveRoids.Talents or table.getn(CleveRoids.Talents) == 0 then
if CleveRoids.IndexTalents then
CleveRoids.IndexTalents()
end
end
local count = 0
for name, rank in pairs(CleveRoids.Talents) do
if type(name) == "string" and tonumber(rank) and tonumber(rank) > 0 then
CleveRoids.Print(name .. ": Rank " .. rank)
count = count + 1
end
end
if count == 0 then
CleveRoids.Print("No talents learned yet!")
else
CleveRoids.Print("Total: " .. count .. " talents")
end
return
end
-- testtalent (test talent modifier for a spell)
if cmd == "testtalent" or cmd == "talenttest" then
local spellID = tonumber(val)
if not spellID then
CleveRoids.Print("Usage: /cleveroid testtalent <spellID>")
CleveRoids.Print("Example: /cleveroid testtalent 1943 (Rupture Rank 1)")
return
end
local spellName = C_Spell.GetSpellName(spellID) or ("Spell " .. spellID)
local modifier = CleveRoids.talentModifiers and CleveRoids.talentModifiers[spellID]
if not modifier then
CleveRoids.Print(spellName .. " (ID:" .. spellID .. ") has no talent modifier configured")
return
end
-- Get talent rank using position-based or name-based lookup
local talentRank = 0
local maxRank = 3
local talentName = modifier.talent or ("Tab " .. tostring(modifier.tab) .. " ID " .. tostring(modifier.id))
-- Position-based lookup (preferred)
if modifier.tab and modifier.id then
local _, name, _, _, rank, max = GetTalentInfo(modifier.tab, modifier.id)
talentRank = tonumber(rank) or 0
maxRank = tonumber(max) or 3
if name then
talentName = name
end
end
-- Name-based fallback
if talentRank == 0 and modifier.talent and CleveRoids.GetTalentRank then
talentRank = CleveRoids.GetTalentRank(modifier.talent)
end
CleveRoids.Print("=== Talent Modifier Test ===")
CleveRoids.Print("Spell: " .. spellName .. " (ID:" .. spellID .. ")")
CleveRoids.Print("Talent: " .. talentName)
CleveRoids.Print("Your Rank: " .. talentRank .. "/" .. maxRank)
if talentRank == 0 then
CleveRoids.Print("|cffff0000You don't have this talent!|r")
else
-- Test with a base duration (use 10s as example)
local baseDur = 10
local modDur = modifier.modifier(baseDur, talentRank)
CleveRoids.Print("Example: 10s base -> " .. modDur .. "s modified (+" .. (modDur - baseDur) .. "s)")
end
return
end
-- debuffdebug (debug debuff tracking on target)
if cmd == "debuffdebug" or cmd == "debuff" or cmd == "trackdebug" then
local searchName = val
if val2 and val2 ~= "" then
searchName = val .. " " .. val2
end
-- Strip underscores and quotes
if searchName and searchName ~= "" then
searchName = string.gsub(searchName, "_", " ")
searchName = string.gsub(searchName, '"', "")
end
CleveRoids.Print("|cff88ff88=== Debuff Tracking Debug ===|r")
local guid = CleveRoids.GetGUID("target")
if not guid then
CleveRoids.Print("|cffff0000No target selected!|r")
return
end
local targetName = UnitName("target") or "Unknown"
CleveRoids.Print("Target: " .. targetName .. " (GUID: " .. tostring(guid) .. ")")
-- Show tracking table for this target
CleveRoids.Print(" ")
CleveRoids.Print("|cff00ff00=== Tracked Debuffs (libdebuff.objects) ===|r")
local lib = CleveRoids.libdebuff
if lib and lib.objects and lib.objects[guid] then
local count = 0
for spellID, rec in pairs(lib.objects[guid]) do
if rec and rec.start and rec.duration then
local timeRemaining = rec.duration + rec.start - GetTime()
local spellName = C_Spell.GetSpellName(spellID) or "Unknown"
local caster = rec.caster or "unknown"
local stacks = rec.stacks or 0
if timeRemaining > 0 then
CleveRoids.Print(string.format(" |cff00ff00[%d]|r %s: %.1fs left (caster: %s, stacks: %d)",
spellID, spellName, timeRemaining, caster, stacks))
count = count + 1
else
CleveRoids.Print(string.format(" |cffff0000[%d]|r %s: EXPIRED %.1fs ago (caster: %s)",
spellID, spellName, -timeRemaining, caster))
end
end
end
if count == 0 then
CleveRoids.Print(" (no active tracked debuffs)")
end
else
CleveRoids.Print(" (no tracking data for this target)")
end
-- Show actual debuff slots (1-16 via UnitDebuff, 17-48 via overflow)
CleveRoids.Print(" ")
CleveRoids.Print("|cff00ff00=== Debuff Slots (UnitDebuff 1-16) ===|r")
local debuffCount = 0
for i = 1, 16 do
local texture, stacks, debuffType, spellID = UnitDebuff("target", i)
if texture then
local spellName = C_Spell.GetSpellName(spellID) or "slot" .. i
CleveRoids.Print(string.format(" Slot %d: [%d] %s (stacks: %d)",
i, spellID or 0, spellName, stacks or 0))
debuffCount = debuffCount + 1
end
end
if debuffCount == 0 then
CleveRoids.Print(" (no debuffs in slots 1-16)")
end
-- Show overflow debuffs in buff slots (17-48)
CleveRoids.Print(" ")
CleveRoids.Print("|cff00ff00=== Overflow Debuffs (UnitBuff 1-32 as debuffs 17-48) ===|r")
local overflowCount = 0
for i = 1, 32 do
local texture, stacks, spellID = UnitBuff("target", i)
if texture and spellID then
-- Check if this might be an overflow debuff by checking libdebuff durations
local isDebuff = lib and lib.durations and lib.durations[spellID]
if isDebuff then
local spellName = C_Spell.GetSpellName(spellID) or "slot" .. i
CleveRoids.Print(string.format(" Buff Slot %d (=Debuff %d): [%d] %s (stacks: %d) |cffff8800OVERFLOW|r",
i, i + 16, spellID, spellName, stacks or 0))
overflowCount = overflowCount + 1
end
end
end
if overflowCount == 0 then
CleveRoids.Print(" (no overflow debuffs detected)")
end
-- If a specific debuff name was provided, test the conditional
if searchName and searchName ~= "" then
CleveRoids.Print(" ")
CleveRoids.Print("|cff00ff00=== Testing [debuff:\"" .. searchName .. "\"] ===|r")
-- Test ValidateUnitDebuff
local result = CleveRoids.ValidateUnitDebuff("target", { name = searchName })
CleveRoids.Print("ValidateUnitDebuff(target, {name='" .. searchName .. "'}): " ..
(result and "|cff00ff00true|r" or "|cffff0000false|r"))
-- Test with time conditional
local resultTime = CleveRoids.ValidateUnitDebuff("target", { name = searchName, operator = "<", amount = 99999 })
CleveRoids.Print("ValidateUnitDebuff(target, {name='" .. searchName .. "', operator='<', amount=99999}): " ..
(resultTime and "|cff00ff00true|r" or "|cffff0000false|r"))
-- Look for spell IDs matching this name
CleveRoids.Print(" ")
CleveRoids.Print("|cff00ff00=== Spell ID Lookup for \"" .. searchName .. "\" ===|r")
local foundIDs = {}
-- Check Spells table
if CleveRoids.Spells then
for id, name in pairs(CleveRoids.Spells) do
if type(name) == "string" and string.lower(name) == string.lower(searchName) then
table.insert(foundIDs, id)
end
end
end
-- Also check GetSpellRecField
if GetSpellRecField then
for id = 1, 30000 do
local name = C_Spell.GetSpellName(id)
if name and string.lower(name) == string.lower(searchName) then
local found = false
for _, existingID in ipairs(foundIDs) do
if existingID == id then found = true break end
end
if not found then
table.insert(foundIDs, id)
end
end
-- Stop early if we found some
if table.getn(foundIDs) > 10 then break end
end
end
if table.getn(foundIDs) > 0 then
for _, id in ipairs(foundIDs) do
local tracked = lib and lib.objects and lib.objects[guid] and lib.objects[guid][id]
local trackedStr = tracked and "|cff00ff00TRACKED|r" or "|cff888888not tracked|r"
CleveRoids.Print(" SpellID " .. id .. ": " .. trackedStr)
if tracked then
local remaining = tracked.duration + tracked.start - GetTime()
CleveRoids.Print(" -> " .. string.format("%.1fs remaining (caster: %s)", remaining, tracked.caster or "?"))
end
end
else
CleveRoids.Print(" (no spell IDs found for this name)")
end
end
return
end
-- tankdebug (debug pfUI tank integration)
if cmd == "tankdebug" then
CleveRoids.Print("=== Tank Debug ===")
-- Check pfUI availability
if not CleveRoids.HasPfUITanks() then
CleveRoids.Print("|cffff0000pfUI tank system not available!|r")
CleveRoids.Print(" pfUI loaded: " .. tostring(type(pfUI) == "table"))
CleveRoids.Print(" pfUI_config: " .. tostring(type(pfUI_config) == "table"))
return
end
CleveRoids.Print("|cff00ff00pfUI tank system available|r")
-- List raid frame tanks
CleveRoids.Print(" ")
CleveRoids.Print("|cffffaa00Raid Frame Tanks (right-click toggle):|r")
local raidTankCount = 0
if type(pfUI) == "table" and type(pfUI.uf) == "table" and
type(pfUI.uf.raid) == "table" and type(pfUI.uf.raid.tankrole) == "table" then
for name, isTank in pairs(pfUI.uf.raid.tankrole) do
if isTank then
CleveRoids.Print(" - " .. name)
raidTankCount = raidTankCount + 1
end
end
end
if raidTankCount == 0 then
CleveRoids.Print(" (none)")
end
-- List nameplate off-tank names
CleveRoids.Print(" ")
CleveRoids.Print("|cffffaa00Nameplate Off-Tank Names (config setting):|r")
local npTankCount = 0
if type(pfUI_config) == "table" and type(pfUI_config.nameplates) == "table" then
local list = pfUI_config.nameplates.combatofftanks or ""
if list ~= "" then
CleveRoids.Print(" Raw setting: \"" .. list .. "\"")
-- pfUI uses # as separator
for name in string.gfind(list, "[^#]+") do
name = string.gsub(name, "^%s*(.-)%s*$", "%1") -- trim
if name ~= "" then
CleveRoids.Print(" - " .. name .. " (lowercase: " .. string.lower(name) .. ")")
npTankCount = npTankCount + 1
end
end
end
end
if npTankCount == 0 then
CleveRoids.Print(" (none)")
end
-- Check current target
CleveRoids.Print(" ")
CleveRoids.Print("|cffffaa00Current Target Check:|r")
if not UnitExists("target") then
CleveRoids.Print(" No target selected")
else
local targetName = UnitName("target") or "Unknown"
CleveRoids.Print(" Your target: " .. targetName)
if UnitExists("targettarget") then
local ttName = UnitName("targettarget") or "Unknown"
CleveRoids.Print(" Target's target: " .. ttName)
local isTank = CleveRoids.IsPlayerTank(ttName)
if isTank then
CleveRoids.Print(" |cff00ff00" .. ttName .. " IS marked as tank|r")
else
CleveRoids.Print(" |cffff8800" .. ttName .. " is NOT marked as tank|r")
CleveRoids.Print(" (lowercase check: " .. string.lower(ttName) .. ")")
end
else
CleveRoids.Print(" Target's target: (none)")
end
-- Test the conditional functions
CleveRoids.Print(" ")
CleveRoids.Print("|cffffaa00Conditional Results:|r")
local isTargetingTank = CleveRoids.IsTargetingAnyTank("target")
CleveRoids.Print(" IsTargetingAnyTank('target'): " .. tostring(isTargetingTank))
CleveRoids.Print(" [targeting:tank] would be: " .. tostring(isTargetingTank))
CleveRoids.Print(" [notargeting:tank] would be: " .. tostring(not isTargetingTank))
end
return
end
-- auradebug (debug all-caster aura tracking)
if cmd == "auradebug" or cmd == "bufftracking" then
CleveRoids.Print("=== All-Caster Aura Tracking Debug ===")
-- Check CVar status
local auraCastEnabled = GetCVar("NP_EnableAuraCastEvents")
if auraCastEnabled == "1" then
CleveRoids.Print("|cff00ff00NP_EnableAuraCastEvents = 1 (ENABLED)|r")
else
CleveRoids.Print("|cffff0000NP_EnableAuraCastEvents = " .. tostring(auraCastEnabled) .. " (DISABLED)|r")
CleveRoids.Print(" Enable with: |cff00ffff/run SetCVar(\"NP_EnableAuraCastEvents\", \"1\")|r")
CleveRoids.Print(" Then /reload")
end
-- Check Nampower version
local API = CleveRoids.NampowerAPI
if API and API.features then
CleveRoids.Print("Nampower hasAuraCastEvents: " .. tostring(API.features.hasAuraCastEvents))
else
CleveRoids.Print("|cffff0000Nampower API not available|r")
end
-- Show tracking data for current target/mouseover
CleveRoids.Print(" ")
CleveRoids.Print("|cffffaa00Tracked Auras:|r")
local trackingCount = 0
local now = GetTime()
local backingTable = CleveRoids.AllCasterAuraTracking or {}
for targetGuid, spellNames in pairs(backingTable) do
local unitName = nil
-- Try to find unit name for this GUID (use pcall to handle invalid units like "focus")
for _, testUnit in ipairs({"target", "mouseover", "party1", "party2", "party3", "party4"}) do
local ok, exists, testGuid = pcall(function() return UnitExists(testUnit) end)
if ok and exists then
_, testGuid = UnitExists(testUnit)
if testGuid == targetGuid then
unitName = UnitName(testUnit)
break
end
end
end
for spellName, casters in pairs(spellNames) do
for casterGuid, auraData in pairs(casters) do
local startTime = isPfUI and auraData.startTime or auraData.start
if startTime and auraData.duration then
local remaining = auraData.duration + startTime - now
if remaining > 0 then
local display = unitName or (string.sub(targetGuid, 1, 16) .. "...")
CleveRoids.Print(string.format(" %s on %s: %.1fs left (caster: %s)",
spellName, display, remaining, string.sub(tostring(casterGuid), 1, 16)))
trackingCount = trackingCount + 1
end
end
end
end
end
if trackingCount == 0 then
CleveRoids.Print(" (no auras currently tracked)")
end
-- Check specific target
if UnitExists("target") then
CleveRoids.Print(" ")
CleveRoids.Print("|cffffaa00Target Buff Check:|r")
local targetGuid = CleveRoids.GetGUID("target")
CleveRoids.Print(" Target GUID: " .. tostring(targetGuid))
local targetData = CleveRoids.GetAuraTrackingData(targetGuid)
if targetData then
local count = 0
for _, casters in pairs(targetData) do
for _ in pairs(casters) do count = count + 1 end
end
CleveRoids.Print(" Tracked aura entries on target: " .. count)
else
CleveRoids.Print(" |cffff9900No tracking data for this target|r")
end
end
return
end
-- overflowframe (enable/disable player overflow buff frame)
if cmd == "overflowframe" then
CleveRoidMacros = CleveRoidMacros or {}
if val == "on" or val == "1" then
CleveRoidMacros.overflowPlayerEnabled = true
CleveRoids.Print("Player overflow buff frame |cff00ff00enabled|r")
elseif val == "off" or val == "0" then
CleveRoidMacros.overflowPlayerEnabled = false
CleveRoids.Print("Player overflow buff frame |cffff0000disabled|r")
else
CleveRoidMacros.overflowPlayerEnabled = not CleveRoidMacros.overflowPlayerEnabled
if CleveRoidMacros.overflowPlayerEnabled then
CleveRoids.Print("Player overflow buff frame |cff00ff00enabled|r")
else
CleveRoids.Print("Player overflow buff frame |cffff0000disabled|r")
end
end
if CleveRoids.RebuildOverflowFrame then
CleveRoids.RebuildOverflowFrame()
end
return
end
-- overflowtest (toggle overflow buff frame test mode)
if cmd == "overflowtest" then
if CleveRoids.ToggleOverflowTest then
CleveRoids.ToggleOverflowTest()
else
CleveRoids.Print("|cffff0000OverflowBuffFrame extension not loaded|r")
end
return
end
-- overflowreset (reset frame positions to defaults)
if cmd == "overflowreset" then
if CleveRoids.ResetOverflowFramePositions then
CleveRoids.ResetOverflowFramePositions()
else
CleveRoids.Print("|cffff0000OverflowBuffFrame extension not loaded|r")
end
return
end
-- Delegate to extension console handlers (chain pattern)
if CleveRoids.HandleConsoleCommand then
CleveRoids.HandleConsoleCommand(msg)
return
end
-- Unknown command fallback
CleveRoids.Print("Usage:")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid - Show current settings")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid realtime 0 or 1 - Force realtime updates (Default: 0. 1 = on, increases CPU load)")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid refresh X - Set refresh rate (1 to 10 updates per second. Default: 5)")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid learn <spellID> <duration> - Manually set spell duration")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid forget <spellID|all> - Forget learned duration(s)")
DEFAULT_CHAT_FRAME:AddMessage("/cleveroid debug [0|1] - Toggle learning debug messages")
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Immunity Tracking:|r")
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listimmune [school] - List immunity data')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid addimmune "<NPC>" <school> [buff] - Add immunity')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid removeimmune "<NPC>" <school> - Remove immunity')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearimmune [school] - Clear immunity data')
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00CC Immunity Tracking:|r")
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid listccimmune [type] - List CC immunity data')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid addccimmune "<NPC>" <type> [buff] - Add CC immunity')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid removeccimmune "<NPC>" <type> - Remove CC immunity')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid clearccimmune [type] - Clear CC immunity data')
DEFAULT_CHAT_FRAME:AddMessage("|cffffaa00Combo Point Tracking:|r")
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid combotrack - Show combo point tracking info')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid comboclear - Clear combo tracking data')
DEFAULT_CHAT_FRAME:AddMessage('/cleveroid combolearn - Show learned combo durations (per CP)')
end
SLASH_CLEAREQUIPQUEUE1 = "/clearequipqueue"
SlashCmdList.CLEAREQUIPQUEUE = function()
-- Release all entries back to pool
for i = 1, CleveRoids.equipmentQueueLen do
local entry = CleveRoids.equipmentQueue[i]
if entry then
CleveRoids.equipmentQueue[i] = nil
-- Return to pool if space available
if table.getn(CleveRoids.queueEntryPool) < 10 then
entry.item = nil
entry.slotName = nil
entry.inventoryId = nil
table.insert(CleveRoids.queueEntryPool, entry)
end
end
end
CleveRoids.equipmentQueueLen = 0
if CleveRoids.equipQueueFrame then
CleveRoids.equipQueueFrame:Hide()
end
CleveRoids.Print("Equipment queue cleared")
end
SLASH_EQUIPQUEUESTATUS1 = "/equipqueuestatus"
SlashCmdList.EQUIPQUEUESTATUS = function()
local count = CleveRoids.equipmentQueueLen
CleveRoids.Print("Equipment queue has " .. count .. " pending items")
for i = 1, count do
local entry = CleveRoids.equipmentQueue[i]
if entry then
local itemName = (entry.item and entry.item.name) or "Unknown"
local slotName = entry.slotName or "Unknown"
local retries = entry.retries or 0
CleveRoids.Print(i .. ". " .. itemName .. " -> " .. slotName .. " (retries: " .. retries .. ")")
end
end
end
-- Apply temporary weapon enchants (poisons, oils, sharpening stones, etc.)
-- Usage: /applymain [conditionals] ItemName
-- Usage: /applyoff [conditionals] ItemName
-- Example: /applymain [nomhimbue] Instant Poison
-- Example: /applyoff [combat] Crippling Poison
local function FindItemInBags(itemName)
-- Require a non-empty item name to prevent matching everything
if not itemName or itemName == "" then
return nil
end
local searchName = string.lower(itemName)
for bag = 0, 4 do
for slot = 1, GetContainerNumSlots(bag) do
local id = C_Container.GetContainerItemID(bag, slot)
if id then
local foundName = C_Item.GetItemNameByID(id)
if foundName and string.find(string.lower(foundName), searchName, 1, true) then
return bag, slot, foundName
end
end
end
end
return nil
end
function CleveRoids.DoFeedPet(msg)
-- Check macro stop flags
if CleveRoids.stopMacroFlag or CleveRoids.skipMacroFlag then
return false
end
local action = function(itemName)
-- Defensive: make sure we are not in "split stack" mode and nothing is on the cursor
if type(CloseStackSplitFrame) == "function" then CloseStackSplitFrame() end
if CursorHasItem and CursorHasItem() then ClearCursor() end
local _, isHunterPet = HasPetUI()
if not isHunterPet or not UnitExists("pet") or UnitHealth("pet") <= 0 then
return false
end
local bag, bagSlot = FindItemInBags(itemName)
if not bag then
CleveRoids.Print("Item not found: " .. itemName)
return false
end
local feedPet = C_Spell.GetSpellName(6991) or "Feed Pet"
CastSpellByName(feedPet)
-- Only apply the food if Feed Pet actually entered item-targeting mode.
if SpellIsTargeting and not SpellIsTargeting() then
return false
end
PickupContainerItem(bag, bagSlot)
return true
end
-- PERFORMANCE: Use numeric iteration to avoid pairs() iterator allocation
local parts = CleveRoids.splitStringIgnoringQuotes(msg)
for i = 1, table.getn(parts) do
if CleveRoids.DoWithConditionals(parts[i], action, CleveRoids.FixEmptyTarget, false, action) then
-- If /firstaction was used, stop macro evaluation after first successful feed
if CleveRoids.stopOnCastFlag then
CleveRoids.stopMacroFlag = true
end
return true
end
end
return false
end
function CleveRoids.DoApply(hand, msg)
local weaponSlots = {
["main"] = 16,
["off"] = 17,
}
local slot = weaponSlots[hand]
if not slot then
CleveRoids.Print("Invalid hand: " .. tostring(hand))
return false
end
local action = function(itemName)
local bag, bagSlot, foundName = FindItemInBags(itemName)
if not bag then
CleveRoids.Print("Item not found: " .. itemName)
return false
end
-- Apply the item to the weapon
UseContainerItem(bag, bagSlot)
PickupInventoryItem(slot)
ReplaceEnchant()
ClearCursor()
return true
end
local handled = false
for _, v in pairs(CleveRoids.splitStringIgnoringQuotes(msg)) do
if CleveRoids.DoWithConditionals(v, action, CleveRoids.FixEmptyTarget, false, action) then
-- If /firstaction was used, stop macro evaluation after first successful apply
if CleveRoids.stopOnCastFlag then
CleveRoids.stopMacroFlag = true
end
handled = true
break
end
end
return handled
end
SLASH_FEEDPET1 = "/feedpet"
SlashCmdList.FEEDPET = function(msg)
-- Require an item name argument
if not msg or msg == "" or string.gsub(msg, "%s+", "") == "" then
CleveRoids.Print("Usage: /feedpet [conditionals] ItemName")
return
end
CleveRoids.DoFeedPet(msg)
end
SLASH_APPLYMAIN1 = "/applymain"
SlashCmdList.APPLYMAIN = function(msg)
-- Require an item name argument
if not msg or msg == "" or string.gsub(msg, "%s+", "") == "" then
CleveRoids.Print("Usage: /applymain [conditionals] ItemName")
return
end
CleveRoids.DoApply("main", msg)
end
SLASH_APPLYOFF1 = "/applyoff"
SlashCmdList.APPLYOFF = function(msg)
-- Require an item name argument
if not msg or msg == "" or string.gsub(msg, "%s+", "") == "" then
CleveRoids.Print("Usage: /applyoff [conditionals] ItemName")
return
end
CleveRoids.DoApply("off", msg)
end