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 = ""