From 1de455f96e377ec4cabeca281e8ec86223a20222 Mon Sep 17 00:00:00 2001 From: Brues <5278969+brues-code@users.noreply.github.com> Date: Thu, 10 Sep 2026 17:17:08 -0500 Subject: [PATCH] Support Blizzard-style bracket groups: [a][b] Spell and [] fall-through A clause may now carry a leading run of [group] blocks sharing one action; groups are OR'd, first pass wins, and [] always passes. The existing ;-separated single-block form is unchanged and mixes freely. The OR loop lives in the two single-clause evaluators, DoWithConditionals and TestAction, which expand a multi-group clause into one-group variants and recurse. That covers every ;-split site, /castsequence (where ; is not a separator and the sequence is keyed by the full args), /cancelaura, /stopmacro and the editor highlight without rewriting strings, so action.args, the ParsedMsg/Sequences caches and highlight offsets keep their identity. ParseMsg scans the leading group run (quote-aware) instead of the greedy %[(.+)%], which previously turned [a][b] into the bogus key a][b and made such clauses fail silently on both the cast and icon paths. The flag pattern still runs on the whole clause when there are no groups, so !Spell is byte-identical. TestAction reports which variant passed and TestForActiveAction stores its conditionals, so range/usable colouring follows the passing group's @unit rather than group 1's. /target gains ; and group support; /pfcast injects the resolved unit into every group, not just the first. The macro checker validates every leading block, drops the EMPTY_CONDITIONAL error, and catches a missing action after the last group. --- Core.lua | 108 ++++++++++++++++++++++++++++++------ Extensions/MacroErrorUI.lua | 64 +++++++++++++-------- Init.lua | 1 + MacroErrorChecker.lua | 43 ++++++++------ README.md | 1 + Utility.lua | 70 +++++++++++++++++++++++ 6 files changed, 231 insertions(+), 56 deletions(-) diff --git a/Core.lua b/Core.lua index 2d54ac4..b28b170 100644 --- a/Core.lua +++ b/Core.lua @@ -779,7 +779,7 @@ function CleveRoids.TestForActiveAction(actions) else -- First pass: find first action with conditionals that passes for _, action in ipairs(actions.list) do - local result = CleveRoids.TestAction(action.cmd, action.args) + local result, passedClause = CleveRoids.TestAction(action.cmd, action.args) -- Check if action has conditionals local _, conditionals = CleveRoids.GetParsedMsg(action.args) @@ -806,6 +806,13 @@ function CleveRoids.TestForActiveAction(actions) 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) @@ -880,7 +887,8 @@ function CleveRoids.TestForActiveAction(actions) -- Enhanced nampower range check with spell ID support if IsSpellInRange then - local unit = actions.active.conditionals and actions.active.conditionals.target or "target" + 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 @@ -1930,12 +1938,20 @@ function CleveRoids.ParseMsg(msg) conditionals.ignoretooltip = ignorecount CleveRoids._ignoretooltip = ignorecount - -- capture a single [...] conditional block if present - local _, cbEnd, conditionBlock = string.find(msg, "%[(.+)%]") + -- 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 condition block (or from start if none) + -- split off flags/action after the groups (or from the start if there are none) local _, _, noSpam, cancelAura, action = string.find( - string.sub(msg, (cbEnd or 0) + 1), + groups.n > 0 and string.sub(msg, restStart) or msg, "^%s*(!?)(~?)([^!~]+.*)" ) action = CleveRoids.Trim(action or "") @@ -2294,6 +2310,17 @@ function CleveRoids.AdvanceSequence(sequence) 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 @@ -2377,7 +2404,7 @@ function CleveRoids.TestAction(cmd, args) CleveRoids._isTestingAction = false conditionals.target = origTarget - return CleveRoids.GetMacroNameFromAction(msg) or msg + return CleveRoids.GetMacroNameFromAction(msg) or msg, args end function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBeforeAction, action) @@ -2390,6 +2417,17 @@ function CleveRoids.DoWithConditionals(msg, hook, fixEmptyTargetFunc, targetBefo 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 @@ -2710,22 +2748,38 @@ local function ResolvePfCastUnit() 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 - local v = parts[i] - -- If conditionals are present but no explicit @unit, inject the pfUI-resolved unit so - -- all conditionals ([help], [nodebuff:X], etc.) evaluate against the same unit pfUI - -- would cast on, and the final CastSpellByName gets the correct unit token. - if string.find(v, "%[") and not string.find(v, "@") then - local unit = ResolvePfCastUnit() - if unit then - v = string.gsub(v, "%[", "[@" .. unit .. ",", 1) + -- 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 CleveRoids.DoWithConditionals(v, CleveRoids.Hooks.PFCAST_SlashCmd, CleveRoids.FixEmptyTarget, false, CastSpellByName) then + if handled then if CleveRoids.stopOnCastFlag then CleveRoids.stopMacroFlag = true end @@ -2763,6 +2817,27 @@ function CleveRoids.DoTarget(msg) 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 @@ -5288,6 +5363,7 @@ end function CleveRoids.RebuildMacros() CleveRoids.currentSequence = nil CleveRoids.ParsedMsg = {} + CleveRoids.ExpandedGroups = {} CleveRoids.Macros = {} CleveRoids.Actions = {} CleveRoids.Sequences = {} diff --git a/Extensions/MacroErrorUI.lua b/Extensions/MacroErrorUI.lua index 0951802..dc46741 100644 --- a/Extensions/MacroErrorUI.lua +++ b/Extensions/MacroErrorUI.lua @@ -697,30 +697,9 @@ local function EnsureCondHighlightPool() end end --- Test whether conditionals pass for a given command + alternative text. --- Returns: true (passes), false (fails), nil (unconditional / no conditionals) -local function TestConditionalPasses(cmd, alternative) - -- Strip ? tooltip hints (irrelevant for conditional evaluation) - if string.find(alternative, "?", 1, true) then - alternative = string.gsub(alternative, "%?", "") - end - - local hasConditional = string.find(alternative, "%[") ~= nil - - -- Dynamic commands: delegate to TestAction - if CleveRoids.dynamicCmds[cmd] then - local result = CleveRoids.TestAction(cmd, alternative) - if not hasConditional then - return nil -- unconditional - end - return result ~= nil and result ~= false - end - - -- Non-dynamic commands: parse and evaluate Keywords manually - if not hasConditional then - return nil -- unconditional - end - +-- Evaluate one single-group clause of a non-dynamic command against Keywords. +-- Returns: true (passes), false (fails), nil (could not parse) +local function EvaluateNonDynamic(alternative) local ok, action, conditionals = pcall(CleveRoids.GetParsedMsg, alternative) if not ok or not conditionals then return nil @@ -745,6 +724,43 @@ local function TestConditionalPasses(cmd, alternative) return passes end +-- Test whether conditionals pass for a given command + alternative text. +-- Returns: true (passes), false (fails), nil (unconditional / no conditionals) +local function TestConditionalPasses(cmd, alternative) + -- Strip ? tooltip hints (irrelevant for conditional evaluation) + if string.find(alternative, "?", 1, true) then + alternative = string.gsub(alternative, "%?", "") + end + + local hasConditional = string.find(alternative, "%[") ~= nil + + -- Dynamic commands: delegate to TestAction + if CleveRoids.dynamicCmds[cmd] then + local result = CleveRoids.TestAction(cmd, alternative) + if not hasConditional then + return nil -- unconditional + end + return result ~= nil and result ~= false + end + + -- Non-dynamic commands: evaluate Keywords manually. `[a][b] X` passes when + -- any of its groups does. + if not hasConditional then + return nil -- unconditional + end + + local variants = CleveRoids.ExpandBracketGroups(alternative) + if not variants then + return EvaluateNonDynamic(alternative) + end + for i = 1, variants.n do + if EvaluateNonDynamic(variants[i]) then + return true + end + end + return false +end + -- Find character offset ranges for each semicolon-separated alternative. -- Returns array of { start, finish } pairs (1-based indices within argsText). local function FindAlternativeOffsets(argsText) diff --git a/Init.lua b/Init.lua index 6b11519..e716c27 100644 --- a/Init.lua +++ b/Init.lua @@ -39,6 +39,7 @@ CleveRoids.useClassicAPIDisplay = CleveRoids.ClassicAPIMacroDisplay = CleveRoids.useClassicAPIDisplay CleveRoids.ParsedMsg = {} +CleveRoids.ExpandedGroups = {} CleveRoids.Items = {} CleveRoids.Spells = {} CleveRoids.PetSpells = {} diff --git a/MacroErrorChecker.lua b/MacroErrorChecker.lua index c91c953..1dbc07c 100644 --- a/MacroErrorChecker.lua +++ b/MacroErrorChecker.lua @@ -561,7 +561,6 @@ end local ERROR_TYPES = { INVALID_CONDITIONAL = "Invalid conditional", MISMATCHED_BRACKETS = "Mismatched brackets", - EMPTY_CONDITIONAL = "Empty conditional block", INVALID_OPERATOR = "Invalid operator", MISSING_ARGUMENT = "Missing argument", INVALID_COMMAND = "Unknown command", @@ -849,16 +848,19 @@ local function validateLine(line, lineNum) actionPart = "/" .. actionPart -- Add leading slash if missing after split end - -- Parse conditionals if present - use non-greedy match - local condStart = safeStringFind(actionPart, "%[") + -- Parse the leading run of [conditional] blocks, if any. `[a][b] X` is + -- Blizzard-style OR chaining: every block is validated, and the action + -- is what follows the last one. + local blocks = {} local condEnd = nil - local conditionBlock = nil + local condStart = safeStringFind(actionPart, "%[") + local len = safeStringLen(actionPart) - if condStart then + while condStart do -- Find matching closing bracket local depth = 0 local inQuotes = false - local len = safeStringLen(actionPart) + local closePos = nil for i = condStart, len do local char = safeStringSub(actionPart, i, i) @@ -872,23 +874,30 @@ local function validateLine(line, lineNum) elseif char == "]" then depth = depth - 1 if depth == 0 then - condEnd = i - conditionBlock = safeStringSub(actionPart, condStart + 1, i - 1) + closePos = i break end end end end + if not closePos then break end + + table.insert(blocks, safeStringSub(actionPart, condStart + 1, closePos - 1)) + condEnd = closePos + + -- Another block directly after this one (whitespace allowed)? + local _, wsEnd = safeStringFind(actionPart, "^%s*", closePos + 1) + local nextPos = (wsEnd or closePos) + 1 + if safeStringSub(actionPart, nextPos, nextPos) == "[" then + condStart = nextPos + else + condStart = nil + end end - if conditionBlock then - if safeTrim(conditionBlock) == "" then - table.insert(localErrors, { - type = ERROR_TYPES.EMPTY_CONDITIONAL, - line = lineNum, - message = "Empty conditional block []" - }) - else + for _, conditionBlock in ipairs(blocks) do + -- `[]` is the always-true group; nothing to validate + if safeTrim(conditionBlock) ~= "" then -- Check for invalid @ target syntax local _, _, target = safeStringFind(conditionBlock, "(@[^%s,]+)") if target and not safeStringFind(target, "^@[a-z]+%d*") then @@ -936,7 +945,9 @@ local function validateLine(line, lineNum) end end end + end + if condEnd then -- Check for action after conditionals -- Extract the command from this action part local _, _, cmdFromAction = safeStringFind(actionPart, "^(/[a-z]+%d*)") diff --git a/README.md b/README.md index 5705b9b..dbd54b6 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Enhanced macro addon for World of Warcraft 1.12.1 (Vanilla/Turtle WoW) with dyna - **Arguments** use colon: `[mod:alt]`, `[hp:>50]` - **Negation** with `no` prefix: `[nobuff]`, `[nomod:alt]` - **Target** with `@`: `[@mouseover,help]`, `[@party1,hp:<50]` +- **Fallback groups**, Blizzard-style: `[@mouseover,help][@focus,help][] Rejuvenation` tries each `[...]` in order and the first that passes casts the shared spell; `[]` always passes. Mixes freely with `;` - **Spell names** with spaces: `"Mark of the Wild"` or `Mark_of_the_Wild` **Multi-value logic:** diff --git a/Utility.lua b/Utility.lua index 369b37c..b9a8bff 100644 --- a/Utility.lua +++ b/Utility.lua @@ -614,6 +614,76 @@ function CleveRoids.splitStringIgnoringQuotes(str, separator) return result end +-- Splits a macro clause into its leading run of [group] blocks and the action +-- that follows, Blizzard-style: `[a][b] Spell` is two groups sharing one action. +-- Returns prefix (the leading whitespace and ? ! ~ flag characters, kept so a +-- variant re-parses exactly like the original), groups (array with .n, each +-- entry including its brackets) and restStart (index of the action text; 1 when +-- there are no groups). Quote-aware for " so a ] inside a quoted argument does +-- not close a group; not nesting-aware. An unclosed [ ends the scan at that [, +-- which is what the old greedy "%[(.+)%]" produced for such input. +function CleveRoids.ScanBracketGroups(msg) + local groups = { n = 0 } + if not msg then return "", groups, 1 end + + local len = string.len(msg) + local _, prefixEnd = string.find(msg, "^[%s%?!~]*") + local prefix = string.sub(msg, 1, prefixEnd) + local i = prefixEnd + 1 + + while i <= len and string.sub(msg, i, i) == "[" do + local close = nil + local inQuotes = false + for j = i + 1, len do + local c = string.sub(msg, j, j) + if c == "\"" then + inQuotes = not inQuotes + elseif c == "]" and not inQuotes then + close = j + break + end + end + if not close then break end + + groups.n = groups.n + 1 + groups[groups.n] = string.sub(msg, i, close) + local _, wsEnd = string.find(msg, "^%s*", close + 1) + i = wsEnd + 1 + end + + if groups.n == 0 then + return prefix, groups, 1 + end + return prefix, groups, i +end + +-- Single-group variants of a multi-group clause: `[a][b] Spell` becomes +-- { "[a] Spell", "[b] Spell", n = 2 }. nil for anything with fewer than two +-- groups so callers take their normal path without allocating. Memoised per +-- clause string in CleveRoids.ExpandedGroups (false marks "nothing to expand"). +function CleveRoids.ExpandBracketGroups(msg) + if not msg or not string.find(msg, "%[") then return nil end + + local cached = CleveRoids.ExpandedGroups[msg] + if cached ~= nil then + return cached or nil + end + + local variants = nil + local prefix, groups, restStart = CleveRoids.ScanBracketGroups(msg) + if groups.n > 1 then + local rest = string.sub(msg, restStart) + if rest ~= "" then rest = " " .. rest end + variants = { n = groups.n } + for i = 1, groups.n do + variants[i] = prefix .. groups[i] .. rest + end + end + + CleveRoids.ExpandedGroups[msg] = variants or false + return variants +end + function CleveRoids.Print(...) local c = "|cFF4477FFCleveR|r|cFFFFFFFFoid :: |r" local out = ""