Files
SuperCleveRoidMacros/Core.lua
T
2026-07-30 20:41:23 -05:00

7138 lines
284 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,
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()
if not hasNampower30 or not hasUnitXP or not hasClassicAPI 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.")
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) for current player class
local function IsRelicSlot(slot)
if slot ~= 18 then return false end
local playerClass = CleveRoids.playerClass
return playerClass == "PALADIN" or playerClass == "DRUID" or playerClass == "SHAMAN"
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 (ranged) is only a weapon for Hunter/Warrior/Rogue/Mage/Warlock/Priest
-- For Druid/Paladin/Shaman, slot 18 is idol/libram/totem - can swap freely
local isWeapon = (inventoryId == 16 or inventoryId == 17)
if inventoryId == 18 then
-- Use cached playerClass for performance
local playerClass = CleveRoids.playerClass
-- Only treat slot 18 as weapon for classes that use ranged weapons
isWeapon = (playerClass == "HUNTER" or playerClass == "WARRIOR" or
playerClass == "ROGUE" or playerClass == "MAGE" or
playerClass == "WARLOCK" or playerClass == "PRIEST")
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
-- 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.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
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 = 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
-- 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 unit = actions.active.conditionals and actions.active.conditionals.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 = GetSpellCooldown(actions.active.spell.spellSlot, actions.active.spell.bookType)
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
-- 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
-- Send event to ALL slots that use this macro
local count = slots._count
for j = 1, count do
CleveRoids.SendEventForAction(slots[j], "ACTIONBAR_SLOT_CHANGED", slots[j])
end
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.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
function CleveRoids.SendEventForAction(slot, event, ...)
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" .. i]
end
if type(arg) == "table" then
local n_varargs_from_arg_table = arg.n or 0
for i = 1, 10 do
if i <= n_varargs_from_arg_table then
_G["arg" .. i] = arg[i]
else
_G["arg" .. i] = nil
end
end
else
for i = 1, 10 do
_G["arg" .. i] = nil
end
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" .. 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
if type(arg) == "table" and arg.n then
if arg.n == 0 then
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event) end
elseif arg.n == 1 then
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1]) end
elseif arg.n == 2 then
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2]) end
elseif arg.n == 3 then
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2], arg[3]) end
elseif arg.n == 4 then
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2], arg[3], arg[4]) end
elseif arg.n == 5 then
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2], arg[3], arg[4], arg[5]) end
elseif arg.n == 6 then
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2], arg[3], arg[4], arg[5], arg[6]) end
else
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do fn_h(slot, event, arg[1], arg[2], arg[3], arg[4], arg[5], arg[6], arg[7]) end
end
else
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do
fn_h(slot, event)
end
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 name of the focus target or nil
function CleveRoids.GetFocusName()
return UnitName('focus')
end
-- Attempts to target the focus target.
-- returns: Whether or not it succeeded
function CleveRoids.TryTargetFocus()
-- ClassicAPI native focus token: exact target switch, no name matching.
if UnitExists("focus") then
TargetUnit("focus")
return UnitExists("target")
end
-- Fallback: pfUI / name-based focus.
local name = CleveRoids.GetFocusName()
if not name then
return false
end
TargetByName(name, true)
if not UnitExists("target") or (string.lower(UnitName("target")) ~= name) then
-- The target switch failed (out of range, LoS, etc.)
return false
end
return true
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 = {},
}
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
-- capture a single [...] conditional block if present
local _, cbEnd, conditionBlock = string.find(msg, "%[(.+)%]")
-- split off flags/action after the condition block (or from start if none)
local _, _, noSpam, cancelAura, action = string.find(
string.sub(msg, (cbEnd or 0) + 1),
"^%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]
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)
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
if conditionals.target == "focus" then
local focusUnitId = CleveRoids.GetFocusUnitId()
if focusUnitId then
conditionals.target = focusUnitId
else
if not CleveRoids.GetFocusName() then
return
end
conditionals.target = "target"
end
elseif conditionals.target == "focustarget" then
local focusUnitId = CleveRoids.GetFocusUnitId()
if focusUnitId then
conditionals.target = focusUnitId .. "target"
else
if not CleveRoids.GetFocusName() then
return
end
conditionals.target = "targettarget"
end
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
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
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)
if conditionals.target == "focus" or conditionals.target == "focustarget" then
local isFocusTarget = conditionals.target == "focustarget"
local focusUnitId = CleveRoids.GetFocusUnitId()
if focusUnitId then
-- Use the resolved pfUI unit token directly (avoids changing the player's target)
conditionals.target = focusUnitId .. (isFocusTarget and "target" or "")
needRetarget = false
else
-- return false if pfUI is installed and no focus is set instead of "invalid target"
if pfUI and pfUI.uf and (not pfUI.uf.focus or pfUI.uf.focus.label == nil or pfUI.uf.focus.label == "") then return false end
-- Fall back to targeting focus by name
if not CleveRoids.TryTargetFocus() then
UIErrorsFrame:AddMessage(SPELL_FAILED_BAD_TARGETS, 1.0, 0.0, 0.0, 1.0)
conditionals.target = origTarget
return false
end
conditionals.target = isFocusTarget and "targettarget" or "target"
needRetarget = true
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
-- 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
-- [mouseuse] modifier: auto-click the AOE targeting circle at cursor position
if conditionals.mouseuse and SpellIsTargeting() then
local wasAttacking = CleveRoids.CurrentSpell.autoAttack
CameraOrSelectOrMoveStart()
CameraOrSelectOrMoveStop()
-- CameraOrSelectOrMoveStart can start auto-attack as a side effect.
-- Stop it immediately if it wasn't active before.
if not wasAttacking then
local slot = CleveRoids.GetProxyActionSlot(CleveRoids.Localized.Attack)
if slot and CleveRoids.Hooks.IsCurrentAction(slot) then
AttackTarget()
CleveRoids.CurrentSpell.autoAttack = false
end
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
-- /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
local v = parts[i]
-- 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.
if string.find(v, "%[") and not string.find(v, "@") then
local unit = ResolvePfCastUnit()
if unit then
v = string.gsub(v, "%[", "[@" .. unit .. ",", 1)
end
end
if CleveRoids.DoWithConditionals(v, CleveRoids.Hooks.PFCAST_SlashCmd, CleveRoids.FixEmptyTarget, false, CastSpellByName) 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
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
-- PERFORMANCE: Module-level actions to avoid closure allocation per call
local function _stopAttackAction()
CleveRoids.DeferStopAttack()
end
local function _stopCastingAction()
SpellStopCasting()
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
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()
-- 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()
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
-- PERFORMANCE: Delayed WDB warmup after login (ensures GetItemInfo works after WDB clear)
if CR.wdbWarmupTime and time >= CR.wdbWarmupTime then
CR.wdbWarmupTime = nil
CR.DoWDBWarmup()
end
-- PERFORMANCE: Cache refresh rate calculation (avoid per-frame division)
local refreshRate = 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 (standalone mode only, pfUI 7.6 manages its own)
if not CR.hasPfUI76 then
local ct = CR.castTracking
local ctGuid, ctEntry = next(ct)
while ctGuid do
local nextCtGuid = next(ct, ctGuid)
if ctEntry.endTime and time > ctEntry.endTime + 0.5 then
ct[ctGuid] = nil
end
ctGuid = nextCtGuid
ctEntry = nextCtGuid and ct[nextCtGuid]
end
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
-- Initialize the nested table for the GameTooltip hooks if it doesn't exist
if not CleveRoids.Hooks.GameTooltip then CleveRoids.Hooks.GameTooltip = {} end
-- Save the original GameTooltip.SetAction function before we override it
CleveRoids.Hooks.GameTooltip.SetAction = GameTooltip.SetAction
-- Now, define our custom version of the function
function GameTooltip.SetAction(self, slot)
if not slot then return end
local actions = CleveRoids.GetAction(slot)
-- If this is our macro but has no active action, show just the macro name
if actions and not actions.active then
local macroName = GetActionText(slot)
if macroName then
GameTooltip:SetText(macroName)
GameTooltip:Show()
return
end
end
local action_to_display_info = nil
if actions then
-- Only show spell/item tooltip when there's an active action
if actions.active then
action_to_display_info = actions.active
end
end
if action_to_display_info and action_to_display_info.action then
local action_name = action_to_display_info.action
-- NEW: Check if action is a slot ID for tooltip
local slotId = tonumber(action_name)
if slotId and slotId >= 1 and slotId <= 19 then
-- Use the more specific SetInventoryItem function to prevent conflicts with other addons.
GameTooltip:SetInventoryItem("player", slotId)
GameTooltip:Show()
return
end
-- End new logic
local current_spell_data = CleveRoids.GetSpell(action_name)
if current_spell_data and current_spell_data.id then
if current_spell_data.spellSlot and current_spell_data.bookType then
GameTooltip:SetSpell(current_spell_data.spellSlot, current_spell_data.bookType)
else
GameTooltip:SetSpellByID(current_spell_data.id)
end
GameTooltip:Show()
return
end
local current_item_data = CleveRoids.GetItem(action_name)
if current_item_data then
-- Use specific functions based on where the item is located.
if current_item_data.inventoryID then
GameTooltip:SetInventoryItem("player", current_item_data.inventoryID)
elseif current_item_data.bagID and current_item_data.slot then
GameTooltip:SetBagItem(current_item_data.bagID, current_item_data.slot)
else
-- Fallback to the original method if location is unknown.
GameTooltip:SetHyperlink(current_item_data.link)
end
GameTooltip:Show()
return
end
if action_to_display_info.macro and type(action_to_display_info.macro) == "table" then
local nested_action_info = action_to_display_info.macro
local nested_action_name = nested_action_info.action
current_spell_data = CleveRoids.GetSpell(nested_action_name)
if current_spell_data and current_spell_data.id then
if current_spell_data.spellSlot and current_spell_data.bookType then
GameTooltip:SetSpell(current_spell_data.spellSlot, current_spell_data.bookType)
else
GameTooltip:SetSpellByID(current_spell_data.id)
end
GameTooltip:Show()
return
end
current_item_data = CleveRoids.GetItem(nested_action_name)
if current_item_data then
if current_item_data.inventoryID then
GameTooltip:SetInventoryItem("player", current_item_data.inventoryID)
elseif current_item_data.bagID and current_item_data.slot then
GameTooltip:SetBagItem(current_item_data.bagID, current_item_data.slot)
else
GameTooltip:SetHyperlink(current_item_data.link)
end
GameTooltip:Show()
return
end
end
end
-- If none of our custom logic handled it, call the original function we saved earlier.
CleveRoids.Hooks.GameTooltip.SetAction(self, slot)
end
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
CleveRoids.Hooks.ActionHasRange = ActionHasRange
function ActionHasRange(slot)
if not slot then return nil end
local actions = CleveRoids.GetAction(slot)
-- Only override for our macros with #showtooltip
if actions and actions.tooltip then
-- If we have an active action with valid range data, use it
if actions.active then
if actions.active.inRange ~= -1 then
return 1 -- Has range check with valid data
else
-- For channeled spells (inRange == -1), try proxy slot lookup
local spellName = actions.active.spell and actions.active.spell.name
local proxySlot = spellName and CleveRoids.GetProxyActionSlot(spellName)
if proxySlot then
return CleveRoids.Hooks.ActionHasRange(proxySlot)
end
end
end
-- FIX: Even without actions.active, if the macro has potential spell actions,
-- return 1 to ensure pfUI calls IsActionInRange (which will do proper checks)
-- This fixes Super macros where actions.active might not be populated yet
if actions.tooltip.spell or actions.tooltip.type == "spell" then
return 1 -- Tooltip is a spell - has range
end
-- Check if any action in the list is a spell
if actions.list then
for i = 1, table.getn(actions.list) do
local action = actions.list[i]
if action and (action.spell or action.type == "spell") then
return 1 -- Has at least one spell action
end
end
end
end
-- Not a macro we're tracking - pass through to original
return CleveRoids.Hooks.ActionHasRange(slot)
end
CleveRoids.Hooks.IsActionInRange = IsActionInRange
function IsActionInRange(slot, unit)
if not slot then return nil end
local actions = CleveRoids.GetAction(slot)
-- Only override for our macros with #showtooltip
if actions and actions.tooltip then
-- If we have an active spell action, use its range data
if actions.active and actions.active.type == "spell" then
if actions.active.inRange ~= -1 then
return actions.active.inRange
else
-- For channeled spells (inRange == -1), try proxy slot lookup
local spellName = actions.active.spell and actions.active.spell.name
local proxySlot = spellName and CleveRoids.GetProxyActionSlot(spellName)
if proxySlot then
return CleveRoids.Hooks.IsActionInRange(proxySlot, unit)
end
end
end
-- FIX: If no active action but macro has tooltip spell, check its range directly
-- This ensures range coloring works even when actions.active hasn't been populated
if not actions.active and actions.tooltip.spell then
local spell = actions.tooltip.spell
local spellName = spell.name
if spellName and IsSpellInRange then
local targetUnit = unit or "target"
if UnitExists(targetUnit) then
-- FIX: Wrap in pcall - spell might not be in spellbook (items, other class spells, etc.)
local ok, result = pcall(IsSpellInRange, spellName, targetUnit)
if ok then
if result == 0 then
return 0 -- Out of range
elseif result == 1 then
return 1 -- In range
end
end
end
end
-- Try proxy slot as fallback
local proxySlot = spellName and CleveRoids.GetProxyActionSlot(spellName)
if proxySlot then
return CleveRoids.Hooks.IsActionInRange(proxySlot, unit)
end
end
end
-- Not a macro we're tracking - pass through to original
return CleveRoids.Hooks.IsActionInRange(slot, unit)
end
CleveRoids.Hooks.OriginalIsUsableAction = IsUsableAction
CleveRoids.Hooks.IsUsableAction = IsUsableAction
function IsUsableAction(slot, unit)
if not slot then return nil, nil end
local actions = CleveRoids.GetAction(slot)
-- If this is one of our macros AND it uses #showtooltip
if actions and actions.tooltip then
-- IMPORTANT: Only override usability when #showtooltip is present
-- Macros without #showtooltip should use default game behavior
if actions.active then
-- We have an active action - return its usable state
-- FIX: Convert oom boolean to number (1/nil) for Blizzard's button code
local oomValue = actions.active.oom and 1 or nil
return actions.active.usable, oomValue
else
-- FIX: If no active action but tooltip has a spell, check its usability directly
-- This ensures usability coloring works even when actions.active hasn't been populated
if actions.tooltip.spell then
local spell = actions.tooltip.spell
local spellName = spell.name
if spellName and IsSpellUsable then
-- FIX: Wrap in pcall - spell might not be in spellbook (items, other class spells, etc.)
local ok, usable, notEnoughMana = pcall(IsSpellUsable, spellName)
if ok then
if usable == 1 then
return 1, nil -- Usable
elseif notEnoughMana == 1 then
return nil, 1 -- Out of mana
else
return nil, nil -- Not usable (wrong stance, etc.)
end
end
end
-- Fallback: check if spell has a cost and compare to current mana
if spell.cost and spell.cost > 0 then
local currentMana = UnitMana("player")
if currentMana < spell.cost then
return nil, 1 -- Out of mana
end
end
-- Default to usable if we can't determine otherwise
return 1, nil
elseif actions.tooltip.item then
-- FIX: Handle item tooltips - items are always usable unless on cooldown
-- The cooldown is handled separately by GetActionCooldown hook
return 1, nil
end
-- No tooltip spell or item - this macro's conditionals all failed
-- Return nil to make the icon dark
return nil, nil
end
else
-- Not our macro OR no #showtooltip - use game's default behavior
return CleveRoids.Hooks.IsUsableAction(slot, unit)
end
end
CleveRoids.Hooks.IsCurrentAction = IsCurrentAction
function IsCurrentAction(slot)
if not slot then return nil end
local actions = CleveRoids.GetAction(slot)
-- When no action is active (all conditionals failed), don't check tooltip's "current" status
if actions and not actions.active and not actions.explicitTooltip and actions.list and table.getn(actions.list) > 0 then
return CleveRoids.Hooks.IsCurrentAction(slot)
end
-- Use the same priority as GetActionTexture: active first, then tooltip
local actionToCheck = (actions and actions.active) or (actions and actions.tooltip)
if not actionToCheck then
return CleveRoids.Hooks.IsCurrentAction(slot)
else
local name
if actionToCheck.spell then
if CleveRoids.IsAutoAttackSpell(actionToCheck.spell) then
return CleveRoids.CurrentSpell.autoAttack and 1 or nil
end
local rank = actionToCheck.spell.rank or actionToCheck.spell.highest.rank
name = actionToCheck.spell.name..(rank and ("("..rank..")"))
-- Check if this spell is currently queued or being cast via Nampower
-- Get spell ID for comparison
local spellId = actionToCheck.spell.id
if not spellId and GetSpellIdForName then
spellId = GetSpellIdForName(name)
end
if spellId then
-- Prefer GetCastInfo (Nampower 2.18+) for cleaner API
if GetCastInfo then
local ok, info = pcall(GetCastInfo)
if ok and info and info.spellId == spellId then
-- Spell is actively being cast/channeled
return true
end
end
-- Also check GetCurrentCastingInfo for queued spell detection
if GetCurrentCastingInfo then
local castId, visId, autoId, casting, channeling = GetCurrentCastingInfo()
-- Show glow if actively casting/channeling this spell
if (casting == 1 and castId == spellId) or (channeling == 1 and visId == spellId) then
return true
end
-- Show glow if this spell is queued (castId set but not yet casting)
if casting == 0 and channeling == 0 and castId == spellId then
return true
end
end
end
elseif actionToCheck.item then
name = actionToCheck.item.name
end
return CleveRoids.Hooks.IsCurrentAction(CleveRoids.GetProxyActionSlot(name) or slot)
end
end
local function GetSlotMacroTexture(slot)
local kind, macroId = CleveRoids.ClassicAPI.GetActionInfo(slot)
if kind == "macro" and macroId then
local _, texture = GetMacroInfo(macroId)
return texture
end
return nil
end
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
CleveRoids.Hooks.GetActionTexture = GetActionTexture
function GetActionTexture(slot)
if not slot then return nil end
local actions = CleveRoids.GetAction(slot)
-- Check if this is one of our macros
if actions and (actions.active or actions.tooltip) then
-- This block handles the case where all conditionals fail and no explicit
-- #showtooltip was set. It defaults to the macro's chosen icon.
if not actions.active and not actions.explicitTooltip and actions.list and table.getn(actions.list) > 0 then
-- Get the macro's own icon as fallback
local macroTexture = GetSlotMacroTexture(slot)
-- When no conditionals pass, use macro icon (not first action's icon)
-- The actions.tooltip is just the first action which didn't pass conditionals
if macroTexture then
return macroTexture
end
-- Should never reach here, but return unknown as last resort
return CleveRoids.unknownTexture
end
-- Prioritize active action, fall back to tooltip
local a = actions.active or actions.tooltip
-- Handle numeric slot actions (e.g., /use 13)
local slotId = tonumber(a.action)
if slotId and slotId >= 1 and slotId <= 19 then
local currentTexture = GetInventoryItemTexture("player", slotId)
if currentTexture then
return currentTexture
end
-- Slot is empty, fall back to macro icon
local macroTexture = GetSlotMacroTexture(slot)
if macroTexture then
return macroTexture
end
return CleveRoids.unknownTexture
end
-- *** THIS IS THE FIX ***
-- If an action is active, return its texture directly.
-- If no action is active, return the tooltip's texture.
-- If neither has a texture, fall back to the macro's icon.
local texture = (actions.active and actions.active.texture) or (actions.tooltip and actions.tooltip.texture)
-- Check if this is a shapeshift form spell and use active texture if toggled on
if a and a.spell and a.action then
-- Strip rank info and underscores from spell name for comparison
local spellName = string.gsub(a.action, "%s*%(.-%)%s*$", "")
spellName = string.gsub(spellName, "_", " ")
-- Check all shapeshift forms to see if this spell matches and is active
for i = 1, GetNumShapeshiftForms() do
local icon, name, isActive, isCastable = GetShapeshiftFormInfo(i)
if name and string.lower(name) == string.lower(spellName) and isActive and icon then
texture = icon
break
end
end
end
-- Check if this is a toggled buff ability (Prowl, Shadowmeld) and swap icon based on buff state
-- Note: Stealth is handled above by shapeshift form logic
if a and a.spell and a.action then
local spellName = string.gsub(a.action, "%s*%(.-%)%s*$", "")
spellName = string.gsub(spellName, "_", " ")
-- Check if this is one of our toggled buff abilities (not shapeshift forms)
local toggledAbilities = {
[CleveRoids.Localized.Spells["Prowl"]] = true,
[CleveRoids.Localized.Spells["Shadowmeld"]] = true,
}
if toggledAbilities[spellName] then
-- Check if the buff is active
if CleveRoids.ValidatePlayerBuff(spellName) then
-- Buff is active, use the active texture from auraTextures
local activeTexture = CleveRoids.auraTextures[spellName]
if activeTexture then
texture = activeTexture
end
end
end
end
if a and a.spell and CleveRoids.IsAutoAttackSpell(a.spell) then
local mainHandTexture = GetInventoryItemTexture("player", 16)
if mainHandTexture then
texture = mainHandTexture
end
end
if texture then
return texture
end
-- Final fallback: get the macro's icon
local macroTexture = GetSlotMacroTexture(slot)
if macroTexture then
return macroTexture
end
-- Should never reach here
return CleveRoids.unknownTexture
end
-- Not one of our macros, use the original function
return CleveRoids.Hooks.GetActionTexture(slot)
end
-- TODO: Look into https://github.com/Stanzilla/WoWUIBugs/issues/47 if needed
CleveRoids.Hooks.GetActionCooldown = GetActionCooldown
function GetActionCooldown(slot)
-- Guard against nil/invalid slot
if not slot then return 0, 0, 0 end
local actions = CleveRoids.GetAction(slot)
-- Check for actions.active OR actions.tooltip
if actions and (actions.active or actions.tooltip) then
-- When no action is active (all conditionals failed) but tooltip exists from #showtooltip,
-- don't show the tooltip's cooldown - fall back to the original function (no cooldown)
-- This matches the icon behavior in GetActionTexture (lines 3958-3977)
if not actions.active and not actions.explicitTooltip and actions.list and table.getn(actions.list) > 0 then
return CleveRoids.Hooks.GetActionCooldown(slot)
end
-- Prioritize the active action, but fall back to the tooltip action
local a = actions.active or actions.tooltip
local slotId = tonumber(a.action)
if slotId and slotId >= 1 and slotId <= 19 then
return GetInventoryItemCooldown("player", slotId)
end
if a.spell then
return GetSpellCooldown(a.spell.spellSlot, a.spell.bookType)
elseif a.item then
if a.item.bagID and a.item.slot then
return GetContainerItemCooldown(a.item.bagID, a.item.slot)
elseif a.item.inventoryID then
return GetInventoryItemCooldown("player", a.item.inventoryID)
end
end
-- DEBUG: Log when we fall through without finding spell/item data (once per slot)
if CleveRoids.cooldownDebug and not CleveRoids.cooldownDebugLogged[slot] then
CleveRoids.cooldownDebugLogged[slot] = true
DEFAULT_CHAT_FRAME:AddMessage(string.format(
"|cffff0000[CD Issue]|r Slot %d: No cooldown data! action='%s' type=%s spell=%s item=%s",
slot,
tostring(a.action),
tostring(a.type),
tostring(a.spell ~= nil),
tostring(a.item ~= nil)
))
DEFAULT_CHAT_FRAME:AddMessage("|cffff8800[CD Issue]|r This slot will show incorrect cooldown. Report macro text to developer.")
end
-- Fallback to original function (preserves SuperWoW's #showtooltip handling)
return CleveRoids.Hooks.GetActionCooldown(slot)
else
return CleveRoids.Hooks.GetActionCooldown(slot)
end
end
CleveRoids.Hooks.GetActionCount = GetActionCount
function GetActionCount(slot)
-- Guard against nil/invalid slot
if not slot then return 0 end
local action = CleveRoids.GetAction(slot)
local count
-- When no action is active (all conditionals failed), don't show tooltip's count
if action and not action.active and not action.explicitTooltip and action.list and table.getn(action.list) > 0 then
return CleveRoids.Hooks.GetActionCount(slot)
end
-- Use the same priority as GetActionTexture: active first, then tooltip
local actionToCheck = (action and action.active) or (action and action.tooltip)
if actionToCheck then
local slotId = tonumber(actionToCheck.action)
if slotId and slotId >= 1 and slotId <= 19 then
return GetInventoryItemCount("player", slotId)
end
if actionToCheck.item then
count = CleveRoids.GetLiveItemCount(actionToCheck.item.name or actionToCheck.action)
elseif actionToCheck.spell then
local reagentId = actionToCheck.spell.reagentId
if not reagentId then
local ss, bt = actionToCheck.spell.spellSlot, actionToCheck.spell.bookType
if ss and bt then
local _, _, rid = CleveRoids.GetSpellCost(ss, bt, actionToCheck.spell.id)
actionToCheck.spell.reagentId = rid -- cache itemID so we don't re-derive every frame
reagentId = rid
end
end
if reagentId then
count = CleveRoids.GetReagentCount(reagentId) -- id-based bag scan
end
end
end
return count or CleveRoids.Hooks.GetActionCount(slot)
end
CleveRoids.Hooks.IsConsumableAction = IsConsumableAction
function IsConsumableAction(slot)
-- Guard against nil/invalid slot
if not slot then return nil end
local action = CleveRoids.GetAction(slot)
-- When no action is active (all conditionals failed), don't show tooltip's consumable status
if action and not action.active and not action.explicitTooltip and action.list and table.getn(action.list) > 0 then
return CleveRoids.Hooks.IsConsumableAction(slot)
end
-- Use the same priority as GetActionTexture: active first, then tooltip
local actionToCheck = (action and action.active) or (action and action.tooltip)
if actionToCheck then
local slotId = tonumber(actionToCheck.action)
if slotId and slotId >= 1 and slotId <= 19 then
local _, count = GetInventoryItemCount("player", slotId)
if count and count > 0 then return 1 end
end
if actionToCheck.item and
(CleveRoids.countedItemTypes[actionToCheck.item.type]
or CleveRoids.countedItemTypes[actionToCheck.item.name])
then
return 1
end
if actionToCheck.spell and actionToCheck.spell.reagentId then
return 1
end
end
return CleveRoids.Hooks.IsConsumableAction(slot)
end
-- ============================================================================
-- 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)
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("PLAYER_EQUIPMENT_CHANGED")
CleveRoids.Frame:RegisterEvent("UNIT_PET")
-- == 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
-- Use GUID events when available (v2.39+), fall back to standard per-token events
if CleveRoids.NampowerAPI.features.hasUnitGuidEvents then
CleveRoids.Frame:RegisterEvent("UNIT_AURA_GUID")
CleveRoids.Frame:RegisterEvent("UNIT_HEALTH_GUID")
CleveRoids.Frame:RegisterEvent("UNIT_MANA_GUID")
CleveRoids.Frame:RegisterEvent("UNIT_RAGE_GUID")
CleveRoids.Frame:RegisterEvent("UNIT_ENERGY_GUID")
else
CleveRoids.Frame:RegisterEvent("UNIT_AURA")
CleveRoids.Frame:RegisterEvent("UNIT_HEALTH")
CleveRoids.Frame:RegisterEvent("UNIT_POWER")
end
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
-- 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.IndexSpells()
CleveRoids.IndexPetSpells()
CleveRoids.initializationTimer = GetTime() + 1.5
-- PERFORMANCE: Initialize event-driven cache states
CleveRoids._cachedPlayerInCombat = UnitAffectingCombat("player") and true or false
-- Schedule delayed WDB warmup (loads items into client cache via tooltip scan)
-- This ensures GetItemInfo() works for all inventory items after a WDB clear
CleveRoids.wdbWarmupTime = GetTime() + 3.0 -- 3 second delay after login
end
-- PERFORMANCE: WDB warmup - tooltip scan all bag items to ensure they're cached
-- This prevents GetItemInfo() returning nil for items after a WDB clear
function CleveRoids.DoWDBWarmup()
if CleveRoids.wdbWarmupDone then return end
CleveRoids.wdbWarmupDone = true
-- Create a hidden tooltip for scanning if it doesn't exist
local tip = CleveRoidsWDBTip
if not tip then
tip = CreateFrame("GameTooltip", "CleveRoidsWDBTip", UIParent, "GameTooltipTemplate")
tip:SetOwner(WorldFrame, "ANCHOR_NONE")
end
local scanned = 0
-- Scan all bag slots
for bag = 0, 4 do
local slots = GetContainerNumSlots(bag) or 0
for slot = 1, slots do
if C_Container.GetContainerItemID(bag, slot) then
-- Tooltip scan loads the item into WDB
tip:ClearLines()
tip:SetBagItem(bag, slot)
scanned = scanned + 1
end
end
end
-- Scan equipped items
for slot = 1, 19 do
if GetInventoryItemID("player", slot) then
tip:ClearLines()
tip:SetInventoryItem("player", slot)
scanned = scanned + 1
end
end
-- Now trigger a full item index to populate the cache with valid data
if CleveRoids.IndexItems then
CleveRoids.IndexItems()
end
if CleveRoids.debug then
CleveRoids.Print("|cff88ff88[WDB Warmup]|r Scanned " .. scanned .. " items into cache")
end
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 not CleveRoids.hasPfUI76 and 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)
function CleveRoids.Frame:SPELL_CAST_EVENT(success, spellId, castType, targetGuid, itemId)
local CHANNEL = 4
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
function CleveRoids.Frame:UPDATE_MACROS()
CleveRoids.currentSequence = nil
-- Explicitly nil tables before re-assignment
CleveRoids.ParsedMsg = nil;
CleveRoids.ParsedMsg = {}
CleveRoids.Macros = nil;
CleveRoids.Macros = {}
CleveRoids.Actions = nil;
CleveRoids.Actions = {}
CleveRoids.Sequences = nil;
CleveRoids.Sequences = {}
CleveRoids.IndexSpells()
CleveRoids.IndexTalents()
CleveRoids.IndexPetSpells()
CleveRoids.IndexActionBars()
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
end
end
function CleveRoids.Frame:SPELLS_CHANGED()
-- PERFORMANCE: Clear spell caches when spells change (learn new ranks, etc.)
CleveRoids.spellIdCache = {}
CleveRoids.spellNameCache = {}
CleveRoids.Frame:UPDATE_MACROS()
end
function CleveRoids.Frame:ACTIONBAR_SLOT_CHANGED()
CleveRoids.ClearAction(arg1)
CleveRoids.IndexActionSlot(arg1)
if CleveRoidMacros.realtime == 0 then
CleveRoids.QueueActionUpdate()
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()
-- Directly clear all relevant caches and force a UI refresh for all buttons.
CleveRoids.Actions = {}
CleveRoids.Macros = {}
CleveRoids.IndexActionBars()
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
-- NOTE: Blizzard action buttons are handled natively via ActionButton_OnEvent which calls
-- our GetActionCooldown hook. We do NOT call CooldownFrame_SetTimer directly to avoid
-- conflicts with SuperWoW's own GetActionCooldown handling for #showtooltip macros.
-- Track which slots have been logged to avoid spam
CleveRoids.cooldownDebugLogged = {}
function CleveRoids.UpdateAllManagedCooldowns()
local Actions = CleveRoids.Actions
if not Actions then return end
-- Only notify third-party handlers (pfUI/Bongos)
-- Blizzard buttons update themselves via GetActionCooldown hook
local handlerCount = CleveRoids.actionEventHandlers and table.getn(CleveRoids.actionEventHandlers) or 0
if handlerCount == 0 then return end
for slot, actions in pairs(Actions) do
if actions then
-- Notify action event handlers (for pfUI/Bongos) about the cooldown update
for _, fn_h in ipairs(CleveRoids.actionEventHandlers) do
fn_h(slot, "ACTIONBAR_UPDATE_COOLDOWN")
end
end
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
function CleveRoids.Frame:SPELL_CAST_EVENT()
if event == "SPELL_CAST_EVENT" then
local success = arg1
local spellId = arg2
local castType = arg3
local targetGuid = arg4
-- 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
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
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()
-- Determine which backing table to iterate
local isPfUI = CleveRoids.hasPfUI76 and pfUI and pfUI.libdebuff_all_auras
local backingTable = isPfUI and pfUI.libdebuff_all_auras or CleveRoids.AllCasterAuraTracking or {}
if isPfUI then
CleveRoids.Print(" (reading from pfUI.libdebuff_all_auras)")
end
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.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_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