Files
SuperCleveRoidMacros/Console.lua
Brues 828acc0c4b Remove two silently-shadowed code paths
Both were invisible: no error, no warning, just logic that never ran.

Console.lua no longer defines the global RunMacro. Core.lua installs a hook of
the same name and loads first, so this file's name-only wrapper replaced it. What
that cost, whenever SuperMacro was absent: macro *index* arguments stopped
working (Blizzard's RunMacro accepts an index or a name, and ExecuteMacroByName
takes only a name), the delegation to SuperMacro went away, so did the fallback
to the saved Blizzard original when a macro would not resolve, and so did
clearing the stop/skip flags at the start of a top-level run -- which is what
lets /stopmacro, /skipmacro and /firstaction work across parent/child macro
boundaries. With SuperMacro loaded, Compatibility/SuperMacro.lua reassigns the
global afterwards anyway, so this definition only ever did harm.
SlashCmdList.RUNMACRO keeps using ExecuteMacroByName, which is correct there.

Utility.lua drops ClearReactiveProcOnCast and the wrappers it existed for, which
tried to clear a reactive proc when its spell was cast. Three independent bugs
meant it never worked: the wrappers guarded on CleveRoids.Frame, which Core.lua
creates after this file loads, so the guard was always false and neither wrapper
installed; the UNIT_CASTEVENT wrapper tested arg2 == "START" when that signature
is (caster, target, action, spell_id, cast_time), so it compared the target; and
both passed a spell ID to a lookup keyed by spell name. They also allocated an
arg table per call and unpack()'d it. Deleting is behaviour-preserving.

Swept for both patterns: no other duplicate global definitions, and no remaining
file-scope use of CleveRoids.Frame in a file that loads before Core.lua.
2026-09-10 14:40:16 -05:00

494 lines
18 KiB
Lua

--[[
Author: Dennis Werner Garske (DWG) / brian / Mewtiny
License: MIT License
]]
local _G = _G or getfenv(0)
local CleveRoids = _G.CleveRoids or {}
SLASH_PETATTACK1 = "/petattack"
SlashCmdList.PETATTACK = function(msg) CleveRoids.DoPetAction(PetAttack, msg); end
SLASH_PETFOLLOW1 = "/petfollow"
SlashCmdList.PETFOLLOW = function(msg) CleveRoids.DoPetAction(PetFollow, msg); end
SLASH_PETWAIT1 = "/petwait"
SlashCmdList.PETWAIT = function(msg) CleveRoids.DoPetAction(PetWait, msg); end
SLASH_PETPASSIVE1 = "/petpassive"
SlashCmdList.PETPASSIVE = function(msg) CleveRoids.DoPetAction(PetPassiveMode, msg); end
SLASH_PETAGGRESSIVE1 = "/petaggressive"
SlashCmdList.PETAGGRESSIVE = function(msg) CleveRoids.DoPetAction(PetAggressiveMode, msg); end
SLASH_PETDEFENSIVE1 = "/petdefensive"
SlashCmdList.PETDEFENSIVE = function(msg) CleveRoids.DoPetAction(PetDefensiveMode, msg); end
SLASH_RELOAD1 = "/rl"
SlashCmdList.RELOAD = function() ReloadUI(); end
SLASH_USE1 = "/use"
SlashCmdList.USE = CleveRoids.DoUse
SLASH_EQUIP1 = "/equip"
SlashCmdList.EQUIP = CleveRoids.DoUse
-- take back supermacro and pfUI /equip and /use
SlashCmdList.SMEQUIP = CleveRoids.DoUse
SlashCmdList.PFEQUIP = CleveRoids.DoUse
SlashCmdList.PFUSE = CleveRoids.DoUse
SLASH_EQUIPMH1 = "/equipmh"
SlashCmdList.EQUIPMH = CleveRoids.DoEquipMainhand
SLASH_EQUIPOH1 = "/equipoh"
SlashCmdList.EQUIPOH = CleveRoids.DoEquipOffhand
SLASH_EQSLOT111 = "/equip11"
SlashCmdList.EQSLOT11 = CleveRoids.DoEquipRing1
SLASH_EQSLOT121 = "/equip12"
SlashCmdList.EQSLOT12 = CleveRoids.DoEquipRing2
SLASH_EQSLOT131 = "/equip13"
SlashCmdList.EQSLOT13 = CleveRoids.DoEquipTrinket1
SLASH_EQSLOT141 = "/equip14"
SlashCmdList.EQSLOT14 = CleveRoids.DoEquipTrinket2
-- Reclaim ClassicAPI's /equipset (command name EQUIP_SET) so it supports
-- conditionals. ClassicAPI registers the localized aliases against EQUIP_SET;
-- swapping the handler keeps every alias and adds the conditional engine.
if SlashCmdList.EQUIP_SET then
SlashCmdList.EQUIP_SET = CleveRoids.DoEquipSet
else
SLASH_EQUIPSET1 = "/equipset"
SlashCmdList.EQUIPSET = CleveRoids.DoEquipSet
end
SLASH_UNSHIFT1 = "/unshift"
SlashCmdList.UNSHIFT = CleveRoids.DoUnshift
SLASH_CANCELFORM1 = "/cancelform"
SlashCmdList.CANCELFORM = CleveRoids.DoCancelForm
SLASH_UNQUEUE1 = "/unqueue"
SlashCmdList.UNQUEUE = SpellStopCasting
-- TODO make this conditional too
SLASH_CANCELAURA1 = "/cancelaura"
SLASH_CANCELAURA2 = "/unbuff"
SlashCmdList.CANCELAURA = CleveRoids.DoConditionalCancelAura
SLASH_CASTPET1 = "/castpet"
SlashCmdList.CASTPET = function(msg)
CleveRoids.DoCastPet(msg)
end
-- Define original implementations before hooking them.
-- This ensures we have a fallback for non-conditional use.
local StartAttack = function(msg)
if not UnitExists("target") or CleveRoids.IsUnitDead("target") then TargetNearestEnemy() end
-- Check both event-based flag AND action bar state for reliable detection
local isAttacking = CleveRoids.CurrentSpell.autoAttack
if not isAttacking then
-- Fallback: check action bar state via IsCurrentAction
local slot = CleveRoids.GetProxyActionSlot(CleveRoids.Localized.Attack)
if slot and IsCurrentAction(slot) then
CleveRoids.CurrentSpell.autoAttack = true
isAttacking = true
end
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
local StopAttack = function(msg)
-- Deferred to next frame because CastSpellByName starts autoattack as a C++
-- side-effect that hasn't settled yet — AttackTarget() toggle misses it.
CleveRoids.DeferStopAttack()
end
local StopChanneling = function(msg)
-- No Blizzard equivalent: 1.12 has SpellStopCasting (immediate) and nothing
-- that waits for the next channel tick, so this is nampower-only.
CleveRoids.StopChanneling()
end
-- Register slash commands and assign original handlers.
-- These will be hooked immediately after.
SLASH_STARTATTACK1 = "/startattack"
SlashCmdList.STARTATTACK = StartAttack
SLASH_STOPATTACK1 = "/stopattack"
SlashCmdList.STOPATTACK = StopAttack
SLASH_STOPCASTING1 = "/stopcasting"
SlashCmdList.STOPCASTING = SpellStopCasting
SLASH_STOPCHANNELING1 = "/stopchanneling"
SlashCmdList.STOPCHANNELING = StopChanneling
SLASH_CLEARTARGET1 = "/cleartarget"
SlashCmdList.CLEARTARGET = ClearTarget
----------------------------------
-- HOOK DEFINITIONS START
----------------------------------
-- /cleartarget hook
CleveRoids.Hooks.CLEARTARGET_SlashCmd = SlashCmdList.CLEARTARGET
SlashCmdList.CLEARTARGET = function(msg)
if CleveRoids.stopMacroFlag then return end
msg = msg or ""
if string.find(msg, "%[") then
CleveRoids.DoConditionalClearTarget(msg)
else
CleveRoids.Hooks.CLEARTARGET_SlashCmd()
end
end
-- /startattack hook
CleveRoids.Hooks.STARTATTACK_SlashCmd = SlashCmdList.STARTATTACK
SlashCmdList.STARTATTACK = function(msg)
if CleveRoids.stopMacroFlag then return end
msg = msg or ""
if string.find(msg, "%[") then
CleveRoids.DoConditionalStartAttack(msg)
else
CleveRoids.Hooks.STARTATTACK_SlashCmd(msg)
end
end
-- /stopattack hook
CleveRoids.Hooks.STOPATTACK_SlashCmd = SlashCmdList.STOPATTACK
SlashCmdList.STOPATTACK = function(msg)
if CleveRoids.stopMacroFlag then return end
msg = msg or ""
if string.find(msg, "%[") then
-- If conditionals are present, let the function handle it.
-- It will only stop the attack if the conditions are met.
CleveRoids.DoConditionalStopAttack(msg)
else
-- If no conditionals, run the original command.
CleveRoids.Hooks.STOPATTACK_SlashCmd(msg)
end
end
-- /stopcasting hook
CleveRoids.Hooks.STOPCASTING_SlashCmd = SlashCmdList.STOPCASTING
SlashCmdList.STOPCASTING = function(msg)
if CleveRoids.stopMacroFlag then return end
msg = msg or ""
if string.find(msg, "%[") then
-- If conditionals are present, let the function handle it.
-- It will only stop the cast if the conditions are met.
CleveRoids.DoConditionalStopCasting(msg)
else
-- If no conditionals, run the original command.
CleveRoids.Hooks.STOPCASTING_SlashCmd()
end
end
-- /stopchanneling hook
CleveRoids.Hooks.STOPCHANNELING_SlashCmd = SlashCmdList.STOPCHANNELING
SlashCmdList.STOPCHANNELING = function(msg)
if CleveRoids.stopMacroFlag then return end
msg = msg or ""
if string.find(msg, "%[") then
-- If conditionals are present, let the function handle it.
-- It will only stop the channel if the conditions are met.
CleveRoids.DoConditionalStopChanneling(msg)
else
-- If no conditionals, run the original command.
CleveRoids.Hooks.STOPCHANNELING_SlashCmd(msg)
end
end
-- /unqueue hook
CleveRoids.Hooks.UNQUEUE_SlashCmd = SlashCmdList.UNQUEUE
SlashCmdList.UNQUEUE = function(msg)
if CleveRoids.stopMacroFlag then return end
msg = msg or ""
if string.find(msg, "%[") then
-- If conditionals are present, let the function handle it.
CleveRoids.DoConditionalStopCasting(msg)
else
-- If no conditionals, run the original command.
CleveRoids.Hooks.UNQUEUE_SlashCmd()
end
end
-- /cast hook
CleveRoids.Hooks.CAST_SlashCmd = SlashCmdList.CAST
SlashCmdList.CAST = function(msg)
if CleveRoids.stopMacroFlag or CleveRoids.skipMacroFlag then return end
if msg and string.find(msg, "[%[%?!~{]") then
CleveRoids.DoCast(msg)
else
-- Use lastComboPoints which is updated on every OnUpdate tick
-- This is critical for instant-cast finishers where GetComboPoints() returns 0 immediately
local currentCP = CleveRoids.lastComboPoints or 0
-- Also try GetComboPoints as a fallback
if currentCP == 0 and GetComboPoints then
currentCP = GetComboPoints()
end
if currentCP > 0 then
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffaaff00[/cast Hook]|r Using %d CP for %s",
currentCP, msg)
)
end
-- Pre-inject combo duration into pfUI for instant-cast combo finishers
-- Get the spell data to find the proper spell name (handles case-insensitive input)
local spellData = CleveRoids.GetSpell and CleveRoids.GetSpell(msg)
local spellName = spellData and spellData.name or msg
-- If GetSpell didn't find it, capitalize first letter as fallback
if not spellData and spellName then
spellName = string.upper(string.sub(spellName, 1, 1)) .. string.sub(spellName, 2)
end
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffcccccc[/cast Debug]|r input='%s', spellName='%s', GetSpell=%s, IsComboScalingSpell=%s",
msg, spellName or "nil", tostring(spellData ~= nil), tostring(CleveRoids.IsComboScalingSpell ~= nil))
)
end
if CleveRoids.IsComboScalingSpell and CleveRoids.IsComboScalingSpell(spellName) then
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00[/cast Debug]|r IS combo scaling spell")
end
local duration = CleveRoids.CalculateComboScaledDuration and
CleveRoids.CalculateComboScaledDuration(spellName, currentCP)
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffcccccc[/cast Debug]|r duration=%s, pfUI=%s, pfUI.api=%s, pfUI.api.libdebuff=%s, debuffs=%s",
tostring(duration), tostring(pfUI ~= nil),
tostring(pfUI and pfUI.api ~= nil),
tostring(pfUI and pfUI.api and pfUI.api.libdebuff ~= nil),
tostring(pfUI and pfUI.api and pfUI.api.libdebuff and pfUI.api.libdebuff.debuffs ~= nil))
)
end
if duration and CleveRoids.ComboPointTracking then
-- Remove rank from spell name for pfUI compatibility
local baseName = CleveRoids.StripRank(spellName)
-- Populate name-based tracking BEFORE the spell is cast
-- This allows pfUI's AddEffect hook to find it
CleveRoids.ComboPointTracking[baseName] = {
combo_points = currentCP,
duration = duration,
cast_time = GetTime(),
target = UnitName("target") or "Unknown",
confirmed = true
}
if CleveRoids.debug then
DEFAULT_CHAT_FRAME:AddMessage(
string.format("|cffff00ff[/cast Pre-Tracking]|r Set tracking['%s'] = %ds (%d CP)",
baseName, duration, currentCP)
)
end
end
elseif CleveRoids.debug and currentCP > 0 then
DEFAULT_CHAT_FRAME:AddMessage("|cffff9900[/cast Debug]|r NOT a combo scaling spell")
end
end
CleveRoids.Hooks.CAST_SlashCmd(msg)
end
end
CleveRoids.Hooks.TARGET_SlashCmd = SlashCmdList.TARGET
CleveRoids.TARGET_SlashCmd = function(msg)
tmsg = CleveRoids.Trim(msg)
if tmsg ~= "" and not string.find(tmsg, "%[") and not string.find(tmsg, "@") then
CleveRoids.Hooks.TARGET_SlashCmd(tmsg)
return
end
if CleveRoids.DoTarget(tmsg) then
if UnitExists("target") then
return
end
end
CleveRoids.Hooks.TARGET_SlashCmd(msg)
end
SlashCmdList.TARGET = CleveRoids.TARGET_SlashCmd
SLASH_CASTSEQUENCE1 = "/castsequence"
SlashCmdList.CASTSEQUENCE = function(msg)
msg = CleveRoids.Trim(msg)
local sequence = CleveRoids.GetSequence(msg)
if not sequence then return end
-- if not sequence.active then return end
CleveRoids.DoCastSequence(sequence)
end
SLASH_RUNMACRO1 = "/runmacro"
SlashCmdList.RUNMACRO = function(msg)
return CleveRoids.ExecuteMacroByName(CleveRoids.Trim(msg))
end
-- The global RunMacro is installed by Core.lua, which loads before this file. Do not
-- redefine it here: Core's hook accepts a macro index as well as a name (Blizzard's
-- RunMacro takes either), delegates to SuperMacro when that is driving execution,
-- falls back to the saved Blizzard original when a macro will not resolve, and clears
-- the stop/skip flags at the start of a top-level run, which is what lets /stopmacro,
-- /skipmacro and /firstaction work across parent/child macro boundaries. A plain
-- name-only wrapper here silently replaced all of that.
-- When SuperMacro is loaded, Compatibility/SuperMacro.lua redirects the global to
-- SuperMacro_RunMacro so macros go through RunLine (where CRM commands are intercepted).
SLASH_RETARGET1 = "/retarget"
SlashCmdList.RETARGET = function(msg)
CleveRoids.DoRetarget()
end
SLASH_STOPMACRO1 = "/stopmacro"
SlashCmdList.STOPMACRO = function(msg)
CleveRoids.DoStopMacro(msg)
end
SLASH_SKIPMACRO1 = "/skipmacro"
SlashCmdList.SKIPMACRO = function(msg)
CleveRoids.DoSkipMacro(msg)
end
-- Enable "first action only" mode - stop evaluation after first successful /cast or /use
-- Example:
-- /firstaction
-- /cast [myrawpower:>48] Shred
-- /cast [myrawpower:>40] Claw
-- Result: Only Shred casts if energy >= 48, Claw won't be queued
SLASH_FIRSTACTION1 = "/firstaction"
SlashCmdList.FIRSTACTION = function(msg)
CleveRoids.DoFirstAction(msg)
end
-- Re-enable multi-queue behavior after /firstaction
-- Use this to restore normal evaluation where multiple casts can queue
-- Example:
-- /firstaction
-- /cast [myrawpower:>48] Shred -- Priority section
-- /cast [myrawpower:>40] Claw
-- /nofirstaction
-- /cast Tiger's Fury -- Can queue alongside above
SLASH_NOFIRSTACTION1 = "/nofirstaction"
SlashCmdList.NOFIRSTACTION = function(msg)
CleveRoids.DoNoFirstAction(msg)
end
-- QuickHeal with conditionals support (requires QuickHeal addon)
-- Usage: /quickheal [conditionals] [target] [type]
-- Examples:
-- /quickheal -- Smart heal (auto-select target)
-- /quickheal target -- Heal current target
-- /quickheal [combat] party -- Heal party member if in combat
-- /quickheal [mypower:>50] mt -- Heal tank if mana > 50%
-- /quickheal [threat:<80] hot -- Apply HoT if threat is low
SLASH_QUICKHEAL1 = "/quickheal"
SLASH_QUICKHEAL2 = "/qh"
SlashCmdList.QUICKHEAL = function(msg)
CleveRoids.DoQuickHeal(msg)
end
--- Execute QuickHeal with optional conditionals
--- @param msg string The command arguments (conditionals + QuickHeal params)
function CleveRoids.DoQuickHeal(msg)
-- Check if QuickHeal addon is loaded
if type(QuickHeal) ~= "function" then
if not CleveRoids._quickHealErrorShown then
DEFAULT_CHAT_FRAME:AddMessage("|cffff0000[SuperCleveRoidMacros]|r The /quickheal command requires the QuickHeal addon.", 1, 0.5, 0.5)
CleveRoids._quickHealErrorShown = true
end
return
end
msg = CleveRoids.Trim(msg or "")
-- Check if there are conditionals
if string.find(msg, "^%[") then
-- Parse the conditionals and remaining args
local actions = CleveRoids.ParseMsg(msg)
if not actions or table.getn(actions) == 0 then
-- No valid actions parsed, just run QuickHeal
QuickHeal()
return
end
-- Find the first action whose conditionals pass
for i = 1, table.getn(actions) do
local action = actions[i]
if CleveRoids.TestAction(action) then
-- Conditionals passed - extract the target/type from action args
local healTarget = nil
local healType = nil
if action.args then
-- Parse args - could be "target", "party", "mt", "hot", etc.
local args = CleveRoids.Trim(action.args)
if args ~= "" then
-- Check if it's a target or type keyword
local lowerArgs = string.lower(args)
if lowerArgs == "hot" or lowerArgs == "heal" or lowerArgs == "hs" or lowerArgs == "chainheal" then
healType = args
else
-- Assume it's a target specifier
healTarget = args
end
end
end
-- Execute QuickHeal with parsed parameters
QuickHeal(healTarget, nil, nil, nil)
return
end
end
-- No conditions matched - don't heal
return
else
-- No conditionals, pass through to QuickHeal directly
-- Parse basic args: target and/or type
local args = msg
if args == "" then
QuickHeal()
else
-- QuickHeal accepts: Target, SpellID, extParam, forceMaxHPS
-- Common targets: player, target, targettarget, party, mt, nonmt, subgroup
-- Common types: heal, hot, hs (paladin), chainheal (shaman)
QuickHeal(args)
end
end
end