From 612873260d2e3e4bf90b272d688fa43bf8061894 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Thu, 15 Jan 2026 16:16:49 -0500 Subject: [PATCH] add gcd conditional and firstaction slashcommand and fix stopmacro --- Compatibility/SuperMacro.lua | 1 + Conditionals.lua | 87 ++++++++++++++++ Console.lua | 24 +++++ Core.lua | 196 +++++++++++++++++++++++++++++++++-- README.md | 39 +++++++ 5 files changed, 341 insertions(+), 6 deletions(-) diff --git a/Compatibility/SuperMacro.lua b/Compatibility/SuperMacro.lua index 6e285cd..2698d7c 100644 --- a/Compatibility/SuperMacro.lua +++ b/Compatibility/SuperMacro.lua @@ -20,6 +20,7 @@ do local INTERCEPT = { cast=true, castsequence=true, use=true, startattack=true, stopattack=true, stopcasting=true, stopmacro=true, + skipmacro=true, firstaction=true, nofirstaction=true, target=true, retarget=true, cancelaura=true, unbuff=true, unqueue=true, unshift=true, equip=true, equipmh=true, equipoh=true, -- runmacro intentionally NOT included - see note above diff --git a/Conditionals.lua b/Conditionals.lua index 9fb58d9..40470bc 100644 --- a/Conditionals.lua +++ b/Conditionals.lua @@ -4449,6 +4449,93 @@ CleveRoids.Keywords = { end, conditionals, "nocdgcd") end, + -- GCD conditional - check if GCD is active/remaining + -- Usage: [gcd] - true if GCD is active + -- [gcd:<1] - true if GCD has less than 1 second remaining + -- [nogcd] - true if GCD is not active + gcd = function(conditionals) + return Multi(conditionals.gcd, function(args) + -- Get GCD remaining in seconds + local gcdRemaining = 0 + + -- Try Nampower API first (most accurate) + if CleveRoids.NampowerAPI and CleveRoids.NampowerAPI.GetGCDRemainingMs then + local gcdMs = CleveRoids.NampowerAPI.GetGCDRemainingMs() + if gcdMs and gcdMs > 0 then + gcdRemaining = gcdMs / 1000 + end + else + -- Fallback: check the first spell in spellbook for GCD + -- GCD shows as cooldown <= 1.5s on any GCD-triggering spell + for i = 1, 200 do + local spellName = GetSpellName(i, BOOKTYPE_SPELL) + if not spellName then break end + local start, duration = GetSpellCooldown(i, BOOKTYPE_SPELL) + if start and duration and duration > 0 and duration <= 1.5 then + gcdRemaining = (start + duration) - GetTime() + if gcdRemaining < 0 then gcdRemaining = 0 end + break + end + end + end + + -- No args or just [gcd] - check if GCD is active + if not args or args == "" or (type(args) == "table" and not args.operator) then + return gcdRemaining > 0 + end + + -- With operator: [gcd:<1] etc + if type(args) == "table" and args.operator and args.amount then + if CleveRoids.operators[args.operator] then + return CleveRoids.comparators[args.operator](gcdRemaining, args.amount) + end + end + + return gcdRemaining > 0 + end, conditionals, "gcd") + end, + + nogcd = function(conditionals) + return NegatedMulti(conditionals.nogcd, function(args) + -- Get GCD remaining in seconds + local gcdRemaining = 0 + + -- Try Nampower API first (most accurate) + if CleveRoids.NampowerAPI and CleveRoids.NampowerAPI.GetGCDRemainingMs then + local gcdMs = CleveRoids.NampowerAPI.GetGCDRemainingMs() + if gcdMs and gcdMs > 0 then + gcdRemaining = gcdMs / 1000 + end + else + -- Fallback: check the first spell in spellbook for GCD + for i = 1, 200 do + local spellName = GetSpellName(i, BOOKTYPE_SPELL) + if not spellName then break end + local start, duration = GetSpellCooldown(i, BOOKTYPE_SPELL) + if start and duration and duration > 0 and duration <= 1.5 then + gcdRemaining = (start + duration) - GetTime() + if gcdRemaining < 0 then gcdRemaining = 0 end + break + end + end + end + + -- [nogcd] - true if GCD is NOT active + if not args or args == "" or (type(args) == "table" and not args.operator) then + return gcdRemaining <= 0 + end + + -- With operator: [nogcd:<1] means NOT (gcd < 1), i.e., gcd >= 1 + if type(args) == "table" and args.operator and args.amount then + if CleveRoids.operators[args.operator] then + return not CleveRoids.comparators[args.operator](gcdRemaining, args.amount) + end + end + + return gcdRemaining <= 0 + end, conditionals, "nogcd") + end, + channeled = function(conditionals) -- Use time-based prediction for accuracy if CleveRoids.CurrentSpell.type == "channeled" and CleveRoids.channelStartTime and CleveRoids.channelDuration then diff --git a/Console.lua b/Console.lua index d146b77..229f383 100644 --- a/Console.lua +++ b/Console.lua @@ -329,6 +329,30 @@ 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: diff --git a/Core.lua b/Core.lua index 107597d..6f38b6c 100644 --- a/Core.lua +++ b/Core.lua @@ -1291,8 +1291,51 @@ function CleveRoids.ExecuteMacroBody(body,inline) if CleveRoids.macroRefDebug then CleveRoids.Print("|cff88ff88[MacroRef]|r Executing line " .. k .. ": " .. string.sub(v, 1, 60)) end - ChatFrameEditBox:SetText(v) - ChatEdit_SendText(ChatFrameEditBox) + + -- IMPORTANT: Handle macro control commands directly to bypass Blizzard's built-in /stopmacro + -- Blizzard intercepts bare /stopmacro before it reaches our SlashCmdList handler + local trimmed = CleveRoids.Trim(v) + local cmdHandled = false + + -- 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 + + -- Check for /nofirstaction (with or without conditionals) + if not cmdHandled then + local _, _, nofirstactionArgs = string.find(trimmed, "^/nofirstaction%s*(.*)") + if nofirstactionArgs then + CleveRoids.DoNoFirstAction(nofirstactionArgs) + cmdHandled = true + 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) @@ -2093,9 +2136,9 @@ function CleveRoids.TestAction(cmd, args) end function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBeforeAction, action) - -- Check macro stop flags (skip non-STOPMACRO/SKIPMACRO commands when flag is set) - -- This enables /stopmacro and /skipmacro to work without SuperMacro for vanilla macros - if (CleveRoids.stopMacroFlag or CleveRoids.skipMacroFlag) and action ~= "STOPMACRO" and action ~= "SKIPMACRO" then + -- 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 @@ -2119,13 +2162,19 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo end end - -- Handle STOPMACRO/SKIPMACRO without conditionals (bare command) + -- 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 + CleveRoids.stopOnCastFlag = false + return true end if hook then @@ -2251,6 +2300,14 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo -- 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 flag to re-enable multi-queue behavior + CleveRoids.stopOnCastFlag = false + return true end local result = true @@ -2323,6 +2380,10 @@ function CleveRoids.DoCast(msg) for i = 1, table.getn(parts) do local v = parts[i] if CleveRoids.DoWithConditionals(v, CleveRoids.Hooks.CAST_SlashCmd, CleveRoids.FixEmptyTarget, not CleveRoids.hasSuperwow, 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 @@ -2844,6 +2905,10 @@ function CleveRoids.DoUse(msg) 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 @@ -3256,6 +3321,59 @@ function CleveRoids.DoSkipMacro(msg) 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(msg, nil, nil, not CleveRoids.hasSuperwow, "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(msg, nil, nil, not CleveRoids.hasSuperwow, "NOFIRSTACTION") then + return true + end + end + return false + else + -- No conditionals - just clear the flag + CleveRoids.stopOnCastFlag = false + return true + end +end + function CleveRoids.DoCastSequence(sequence) if not CleveRoids.hasSuperwow then CleveRoids.Print("|cFFFF0000/castsequence|r requires |cFF00FFFFSuperWoW|r.") @@ -3372,6 +3490,9 @@ function CleveRoids.OnUpdate(self) if CleveRoids.skipMacroFlag then CleveRoids.skipMacroFlag = false end + if CleveRoids.stopOnCastFlag then + CleveRoids.stopOnCastFlag = false + end -- PERFORMANCE: Single GetTime() call per frame local time = GetTime() @@ -4040,6 +4161,69 @@ function IsConsumableAction(slot) return CleveRoids.Hooks.IsConsumableAction(slot) end +-- ============================================================================ +-- RunMacro Hook - Use our own macro execution system +-- ============================================================================ +-- This allows /stopmacro, /skipmacro, /firstaction to work across parent/child +-- macro boundaries without requiring SuperMacro addon. +-- +-- When SuperMacro is installed, its RunLine hook takes precedence for extended +-- macro features. This hook handles vanilla Blizzard macros. +-- ============================================================================ +if not CleveRoids.RunMacroHooked then + CleveRoids.Hooks.RunMacro = RunMacro + + function RunMacro(indexOrName) + -- Skip if SuperMacro is handling macro execution (it has its own hooks) + if CleveRoids.SM_RunLineHooked then + return CleveRoids.Hooks.RunMacro(indexOrName) + end + + -- Resolve macro index + local macroIndex + if type(indexOrName) == "number" then + macroIndex = indexOrName + elseif type(indexOrName) == "string" then + -- Could be a name or a numeric string + local num = tonumber(indexOrName) + if num then + macroIndex = num + else + macroIndex = GetMacroIndexByName(indexOrName) + end + end + + if not macroIndex or macroIndex == 0 then + -- Fallback to original if we can't resolve + return CleveRoids.Hooks.RunMacro(indexOrName) + end + + -- Get macro body + local name, icon, body = GetMacroInfo(macroIndex) + if not body or body == "" then + return CleveRoids.Hooks.RunMacro(indexOrName) + end + + -- Clear macro flags at the start of top-level macro execution + CleveRoids.stopMacroFlag = false + CleveRoids.skipMacroFlag = false + -- Note: stopOnCastFlag is intentionally NOT cleared here - it persists + -- within a frame and is cleared by OnUpdate + + if CleveRoids.macroRefDebug then + CleveRoids.Print("|cff00ff00[RunMacro Hook]|r Executing macro '" .. (name or macroIndex) .. "' via CleveRoids") + end + + -- Execute through our system + CleveRoids.ExecuteMacroBody(body) + + -- Don't call original - we've handled it + return + end + + CleveRoids.RunMacroHooked = true +end + -- Create a hidden tooltip frame to read buff names if not AuraScanTooltip and not CleveRoids.hasSuperwow then CreateFrame("GameTooltip", "AuraScanTooltip") diff --git a/README.md b/README.md index e6c1bbc..6e1929c 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ All conditionals support negation with `no` prefix (e.g., `[nocombat]`, `[nobuff |-------------|---------|-------------| | `cooldown` | `[cooldown:"Spell"<5]` | CD remaining (ignores GCD) | | `cdgcd` | `[cdgcd:"Spell">0]` | CD remaining (includes GCD) | +| `gcd` | `[gcd]` `[gcd:<1]` | GCD is active / remaining time | | `usable` | `[usable:"Spell"]` | Spell/item is usable | | `reactive` | `[reactive:Overpower]` | Reactive ability available | | `known` | `[known:"Spell">#2]` | Spell/talent known (with rank) | @@ -322,6 +323,8 @@ These commands accept `[conditionals]` and use UnitXP 3D enemy scanning when app | `/quickheal [cond]` | ✅ | — | Smart heal (requires QuickHeal) | | `/stopmacro [cond]` | ✅ | — | Stop ALL macro execution (including parent macros) | | `/skipmacro [cond]` | ✅ | — | Stop current submacro only, parent continues | +| `/firstaction [cond]` | ✅ | — | Stop on first successful `/cast` or `/use` (priority mode) | +| `/nofirstaction [cond]` | ✅ | — | Re-enable multi-queue after `/firstaction` | | `/petattack [cond]` | ✅ | — | Pet attack with conditionals | | `/petfollow [cond]` | ✅ | — | Pet follow with conditionals | | `/petwait [cond]` | ✅ | — | Pet stay with conditionals | @@ -336,6 +339,42 @@ These commands accept `[conditionals]` and use UnitXP 3D enemy scanning when app | `/applymain [cond] Item` | ✅ | — | Apply poison/oil to main hand | | `/applyoff [cond] Item` | ✅ | — | Apply poison/oil to off hand | +#### Priority-Based Macro Evaluation (`/firstaction` & `/nofirstaction`) + +By default, all `/cast` lines in a macro are evaluated and may queue spells with Nampower. Use `/firstaction` to enable "first successful cast wins" behavior - once a `/cast` or `/use` succeeds, the macro stops evaluating subsequent lines. Use `/nofirstaction` to re-enable normal multi-queue behavior. + +```lua +-- WITHOUT /firstaction: Both Shred AND Claw may queue if conditions are met +/cast [myrawpower:>48] Shred +/cast [myrawpower:>40] Claw + +-- WITH /firstaction: Only Shred casts if energy >= 48, Claw is skipped +/firstaction +/cast [myrawpower:>48] Shred +/cast [myrawpower:>40] Claw + +-- Mixed mode: priority section + always-queue section +/firstaction +/cast [myrawpower:>48] Shred -- Priority: only one of these +/cast [myrawpower:>40] Claw +/nofirstaction +/cast Tiger's Fury -- Always evaluates (can queue alongside) +/startattack + +-- With conditionals +/firstaction [group] -- Priority mode only in groups +/cast [myrawpower:>48] Shred +/cast [myrawpower:>40] Claw +``` + +**Child Macro Behavior:** `/firstaction` and `/nofirstaction` in child macros (called via `{MacroName}`) affect subsequent lines in the parent macro. Think of the child's lines as being "inlined" at the call site. + +**Comparison:** +- `/stopmacro [cond]` - Stop if condition is true (regardless of cast success) +- `/skipmacro [cond]` - Stop current submacro only +- `/firstaction [cond]` - Stop on first **successful** cast/use (priority mode) +- `/nofirstaction [cond]` - Re-enable multi-queue behavior after `/firstaction` + ### Commands without Conditional Support | Command | Description |