Files
Brues 5213bd5474 Migrate to ClassicAPI for macro display, auras and timing
Squash of the classicapi_next branch (10 commits). ClassicAPI is now a hard
requirement, and the Lua-side reimplementations it supersedes are gone:
net -1087 lines across 12 files.

Macro display: resolved macro actions are published through
C_Macro.SetMacroDisplay, so the client draws macro buttons and the
action-bar function overrides this addon used to install are removed.
Ownership is per macro -- ClassicAPI keeps the macros we never claim.

Auras and timing: ClassicAPI is the source of truth for non-player aura
timing via the positional C_UnitAuras.UnitAura, replacing the write-only
buff tracking tables and the overflow-slot fallback.

Cleanup: the hasPfUI76 flag and the pfUI 7.6 branches it gated, two
silently-shadowed code paths, and per-call allocations in the event and
publish paths (SPELL_CAST_EVENT merged). Packaging moves to
brues-code/packager@vCAPI.

Macro syntax: a clause may carry a leading run of [group] blocks sharing
one action, Blizzard-style -- groups are OR'd, first pass wins, and []
always passes. The ;-separated form is unchanged and mixes freely. @focus
clauses with no focus set now fail quietly instead of printing
"Invalid target" on the way past.
2026-09-11 00:30:36 -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