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