From 506b4965922933d27791e383bcdaac36c19e749f Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Tue, 28 Oct 2025 20:36:10 -0400 Subject: [PATCH 1/9] initial macro error checker --- MacroErrorChecker.lua | 443 +++++++++++++++++++++++++++++++++++++++ SuperCleveRoidMacros.toc | 1 + 2 files changed, 444 insertions(+) create mode 100644 MacroErrorChecker.lua diff --git a/MacroErrorChecker.lua b/MacroErrorChecker.lua new file mode 100644 index 0000000..d977f1f --- /dev/null +++ b/MacroErrorChecker.lua @@ -0,0 +1,443 @@ +--[[ + Macro Syntax Error Checker + Author: Mewtiny + License: MIT License + + Validates macro syntax and reports errors to help users debug their macros +]] +local _G = _G or getfenv(0) +local CleveRoids = _G.CleveRoids or {} + +-- Known valid conditionals +local VALID_CONDITIONALS = { + -- General + actionbar = true, noactionbar = true, + button = true, + channeling = true, nochanneling = true, + class = true, noclass = true, + cdgcd = true, nocdgcd = true, + combo = true, nocombo = true, + cooldown = true, nocooldown = true, + equipped = true, noequipped = true, + flyable = true, noflyable = true, + flying = true, noflying = true, + form = true, noform = true, + group = true, nogroup = true, + indoors = true, noindoors = true, + known = true, noknown = true, + mod = true, nomod = true, + mounted = true, nomounted = true, + mybuff = true, nomybuff = true, + mydebuff = true, nomydebuff = true, + myhp = true, nomyhp = true, + myhplost = true, nomyhplost = true, + mypower = true, nomypower = true, + mypowerlost = true, nomypowerlost = true, + myrawhp = true, nomyrawhp = true, + myrawpower = true, nomyrawpower = true, + outdoors = true, nooutdoors = true, + pet = true, nopet = true, + petbuff = true, nopetbuff = true, + petdebuff = true, nopetdebuff = true, + reactive = true, noreactive = true, + resting = true, noresting = true, + stance = true, nostance = true, + stat = true, nostat = true, + stealth = true, nostealth = true, + swimming = true, noswimming = true, + talent = true, notalent = true, + zone = true, nozone = true, + + -- Unit based + alive = true, noalive = true, + buff = true, nobuff = true, + casting = true, nocasting = true, + combat = true, nocombat = true, + dead = true, nodead = true, + debuff = true, nodebuff = true, + harm = true, noharm = true, + help = true, nohelp = true, + hp = true, nohp = true, + hplost = true, nohplost = true, + inrange = true, noinrange = true, + isnpc = true, noisnpc = true, + isplayer = true, noisplayer = true, + member = true, nomember = true, + party = true, noparty = true, + power = true, nopower = true, + powerlost = true, nopowerlost = true, + raid = true, noraid = true, + rawhp = true, norawhp = true, + rawpower = true, norawpower = true, + type = true, notype = true, + targeting = true, notargeting = true, + exists = true, noexists = true, +} + +-- Known valid commands +local VALID_COMMANDS = { + ["/cast"] = true, + ["/castpet"] = true, + ["/cancelaura"] = true, + ["/castsequence"] = true, + ["/equip"] = true, + ["/equipmh"] = true, + ["/equipoh"] = true, + ["/focus"] = true, + ["/petattack"] = true, + ["/petfollow"] = true, + ["/petwait"] = true, + ["/petpassive"] = true, + ["/petaggressive"] = true, + ["/petdefensive"] = true, + ["/print"] = true, + ["/run"] = true, + ["/runmacro"] = true, + ["/script"] = true, + ["/startattack"] = true, + ["/stopattack"] = true, + ["/stopcasting"] = true, + ["/stopmacro"] = true, + ["/target"] = true, + ["/unqueue"] = true, + ["/use"] = true, + ["/unbuff"] = true, + ["/unshift"] = true, + ["/retarget"] = true, +} + +-- Error types +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", + INVALID_TARGET = "Invalid target format", + MALFORMED_QUOTES = "Malformed quotes", + EMPTY_ACTION = "Empty action", + INVALID_SYNTAX = "Invalid syntax", +} + +CleveRoids.MacroErrors = {} + +-- Check if a string has balanced brackets +local function checkBrackets(text) + local openCount = 0 + local inQuotes = false + + for i = 1, string.len(text) do + local char = string.sub(text, i, i) + + if char == '"' then + inQuotes = not inQuotes + elseif not inQuotes then + if char == "[" then + openCount = openCount + 1 + elseif char == "]" then + openCount = openCount - 1 + if openCount < 0 then + return false, "Extra closing bracket" + end + end + end + end + + if openCount > 0 then + return false, "Missing closing bracket" + elseif openCount < 0 then + return false, "Extra closing bracket" + end + + return true +end + +-- Check if quotes are balanced +local function checkQuotes(text) + local quoteCount = 0 + local escaped = false + + for i = 1, string.len(text) do + local char = string.sub(text, i, i) + + if escaped then + escaped = false + elseif char == "\\" then + escaped = true + elseif char == '"' then + quoteCount = quoteCount + 1 + end + end + + local quotient = math.floor(quoteCount / 2) + if (quoteCount - (quotient * 2)) ~= 0 then + return false, "Unmatched quotes" + end + + return true +end + +-- Validate conditional syntax +local function validateConditional(conditional, args, action) + local errors = {} + + -- Check if conditional is valid + local baseCond = string.lower(conditional) + if not VALID_CONDITIONALS[baseCond] then + table.insert(errors, { + type = ERROR_TYPES.INVALID_CONDITIONAL, + conditional = conditional, + message = "Unknown conditional: " .. conditional + }) + end + + -- Check for required arguments + local needsArgs = { + combo = true, + hp = true, myhp = true, rawhp = true, myrawhp = true, + power = true, mypower = true, rawpower = true, myrawpower = true, + hplost = true, myhplost = true, + powerlost = true, mypowerlost = true, + stat = true, + talent = true, + actionbar = true, + button = true, + form = true, stance = true, + } + + if needsArgs[baseCond] and (not args or args == "") and (not action or action == "") then + table.insert(errors, { + type = ERROR_TYPES.MISSING_ARGUMENT, + conditional = conditional, + message = conditional .. " requires an argument" + }) + end + + -- Check operator syntax for numeric comparisons + if args and string.find(baseCond, "hp") or string.find(baseCond, "power") or + string.find(baseCond, "combo") or baseCond == "stat" then + local hasOperator = string.find(args, "[<>=~]+") + if args ~= "" and not hasOperator and not string.find(args, "^%d+$") then + -- Might be missing operator + if not string.find(args, "[a-zA-Z]") then + table.insert(errors, { + type = ERROR_TYPES.INVALID_OPERATOR, + conditional = conditional, + message = conditional .. " may need an operator (>, <, =, >=, <=)" + }) + end + end + end + + return errors +end + +-- Parse and validate a single line +local function validateLine(line, lineNum) + local errors = {} + + -- Skip comments and empty lines + line = CleveRoids.Trim(line) + if line == "" or string.sub(line, 1, 2) == "--" then + return errors + end + + -- Check for valid command + local _, _, cmd = string.find(line, "^(/[a-z]+)") + if cmd then + local lowerCmd = string.lower(cmd) + if not VALID_COMMANDS[lowerCmd] then + table.insert(errors, { + type = ERROR_TYPES.INVALID_COMMAND, + line = lineNum, + command = cmd, + message = "Unknown command: " .. cmd + }) + end + end + + -- Check brackets + local bracketsOk, bracketError = checkBrackets(line) + if not bracketsOk then + table.insert(errors, { + type = ERROR_TYPES.MISMATCHED_BRACKETS, + line = lineNum, + message = bracketError + }) + end + + -- Check quotes + local quotesOk, quoteError = checkQuotes(line) + if not quotesOk then + table.insert(errors, { + type = ERROR_TYPES.MALFORMED_QUOTES, + line = lineNum, + message = quoteError + }) + end + + -- Parse conditionals if present + local _, cbEnd, conditionBlock = string.find(line, "%[(.+)%]") + if conditionBlock then + if CleveRoids.Trim(conditionBlock) == "" then + table.insert(errors, { + type = ERROR_TYPES.EMPTY_CONDITIONAL, + line = lineNum, + message = "Empty conditional block []" + }) + else + -- Check for invalid @ target syntax + local _, _, target = string.find(conditionBlock, "(@[^%s,]+)") + if target and not string.find(target, "^@[a-z]+") then + table.insert(errors, { + type = ERROR_TYPES.INVALID_TARGET, + line = lineNum, + message = "Invalid target: " .. target + }) + end + + -- Parse individual conditionals + local seenConditionals = {} + for _, condGroup in CleveRoids.splitStringIgnoringQuotes(conditionBlock, {",", " "}) do + if condGroup ~= "" and condGroup ~= target then + local parts = CleveRoids.splitStringIgnoringQuotes(condGroup, ":") + local cond = string.lower(CleveRoids.Trim(parts[1] or "")) + local args = CleveRoids.Trim(parts[2] or "") + + if cond ~= "" then + seenConditionals[cond] = true + + -- Validate the conditional + local condErrors = validateConditional(cond, args, nil) + for _, err in condErrors do + err.line = lineNum + table.insert(errors, err) + end + end + end + end + end + + -- Check for action after conditionals + local actionPart = string.sub(line, (cbEnd or 0) + 1) + local _, _, action = string.find(actionPart, "^%s*[!~]?(.+)") + if not action or CleveRoids.Trim(action) == "" then + table.insert(errors, { + type = ERROR_TYPES.EMPTY_ACTION, + line = lineNum, + message = "Conditional has no action" + }) + end + end + + return errors +end + +-- Validate an entire macro +function CleveRoids.ValidateMacro(macroName) + local errors = {} + local macroID = GetMacroIndexByName(macroName) + + if not macroID or macroID == 0 then + return {{ + type = "ERROR", + message = "Macro not found: " .. tostring(macroName) + }} + end + + local name, texture, body = GetMacroInfo(macroID) + if not body or body == "" then + return {{ + type = "ERROR", + message = "Macro is empty" + }} + end + + -- Split into lines + local lines = CleveRoids.splitString(body, "\n") + + for lineNum, line in ipairs(lines) do + local lineErrors = validateLine(line, lineNum) + for _, err in lineErrors do + table.insert(errors, err) + end + end + + return errors +end + +-- Validate all macros +function CleveRoids.ValidateAllMacros() + local results = {} + local totalErrors = 0 + + local numMacros = GetNumMacros() + for i = 1, numMacros do + local name = GetMacroInfo(i) + if name then + local errors = CleveRoids.ValidateMacro(name) + if errors and table.getn(errors) > 0 then + results[name] = errors + totalErrors = totalErrors + table.getn(errors) + end + end + end + + return results, totalErrors +end + +-- Print errors for a macro +function CleveRoids.PrintMacroErrors(macroName) + local errors = CleveRoids.ValidateMacro(macroName) + + if not errors or table.getn(errors) == 0 then + CleveRoids.Print("|cff00ff00✓|r Macro '" .. macroName .. "' has no syntax errors") + return + end + + CleveRoids.Print("|cffff0000✗|r Macro '" .. macroName .. "' has " .. table.getn(errors) .. " error(s):") + + for _, err in errors do + local line = err.line and ("Line " .. err.line .. ": ") or "" + local msg = "|cffffaa00" .. line .. "|r" .. err.message + DEFAULT_CHAT_FRAME:AddMessage(" " .. msg) + end +end + +-- Print all macro errors +function CleveRoids.PrintAllMacroErrors() + local results, totalErrors = CleveRoids.ValidateAllMacros() + + if totalErrors == 0 then + CleveRoids.Print("|cff00ff00✓|r All macros are error-free!") + return + end + + CleveRoids.Print("|cffff0000Found " .. totalErrors .. " error(s) in " .. table.getn(results) .. " macro(s):|r") + + for macroName, errors in pairs(results) do + DEFAULT_CHAT_FRAME:AddMessage(" ") + DEFAULT_CHAT_FRAME:AddMessage("|cffff8800" .. macroName .. "|r (" .. table.getn(errors) .. " error(s)):") + + for _, err in errors do + local line = err.line and ("Line " .. err.line .. ": ") or "" + local msg = " |cffffaa00" .. line .. "|r" .. err.message + DEFAULT_CHAT_FRAME:AddMessage(msg) + end + end +end + +-- Slash command +SLASH_MACROCHECK1 = "/macrocheck" +SlashCmdList.MACROCHECK = function(msg) + msg = CleveRoids.Trim(msg or "") + + if msg == "" or msg == "all" then + CleveRoids.PrintAllMacroErrors() + else + CleveRoids.PrintMacroErrors(msg) + end +end + +CleveRoids.Print("Macro syntax checker loaded. Use /macrocheck [macroname] or /macrocheck all") diff --git a/SuperCleveRoidMacros.toc b/SuperCleveRoidMacros.toc index 87ea771..f29c07f 100644 --- a/SuperCleveRoidMacros.toc +++ b/SuperCleveRoidMacros.toc @@ -12,6 +12,7 @@ Core.lua Conditionals.lua Console.lua ExtensionsManager.lua +MacroErrorChecker.lua Compatibility\SuperMacro.lua Compatibility\pfUI.lua From b075b2cf1761c457eacbed59c0473c59ab97c10f Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Wed, 29 Oct 2025 15:47:59 -0400 Subject: [PATCH 2/9] improve checker accuracy --- MacroErrorChecker.lua | 220 ++++++++++++++++++++++++++---------------- 1 file changed, 136 insertions(+), 84 deletions(-) diff --git a/MacroErrorChecker.lua b/MacroErrorChecker.lua index d977f1f..c9923aa 100644 --- a/MacroErrorChecker.lua +++ b/MacroErrorChecker.lua @@ -2,7 +2,7 @@ Macro Syntax Error Checker Author: Mewtiny License: MIT License - + Validates macro syntax and reports errors to help users debug their macros ]] local _G = _G or getfenv(0) @@ -47,7 +47,7 @@ local VALID_CONDITIONALS = { swimming = true, noswimming = true, talent = true, notalent = true, zone = true, nozone = true, - + -- Unit based alive = true, noalive = true, buff = true, nobuff = true, @@ -79,13 +79,22 @@ local VALID_COMMANDS = { ["/cast"] = true, ["/castpet"] = true, ["/cancelaura"] = true, + ["/cancelform"] = true, ["/castsequence"] = true, + ["/castrandom"] = true, + ["/changeactionbar"] = true, + ["/clearfocus"] = true, + ["/cleartarget"] = true, + ["/click"] = true, + ["/dismount"] = true, ["/equip"] = true, ["/equipmh"] = true, ["/equipoh"] = true, + ["/equipslot"] = true, ["/focus"] = true, ["/petattack"] = true, ["/petfollow"] = true, + ["/petstay"] = true, ["/petwait"] = true, ["/petpassive"] = true, ["/petaggressive"] = true, @@ -98,9 +107,19 @@ local VALID_COMMANDS = { ["/stopattack"] = true, ["/stopcasting"] = true, ["/stopmacro"] = true, + ["/stopspelltarget"] = true, + ["/swapactionbar"] = true, ["/target"] = true, + ["/targetenemy"] = true, + ["/targetfriend"] = true, + ["/targetlastenemy"] = true, + ["/targetlastfriend"] = true, + ["/targetlasttarget"] = true, + ["/targetparty"] = true, + ["/targetraid"] = true, ["/unqueue"] = true, ["/use"] = true, + ["/userandom"] = true, ["/unbuff"] = true, ["/unshift"] = true, ["/retarget"] = true, @@ -126,10 +145,10 @@ CleveRoids.MacroErrors = {} local function checkBrackets(text) local openCount = 0 local inQuotes = false - + for i = 1, string.len(text) do local char = string.sub(text, i, i) - + if char == '"' then inQuotes = not inQuotes elseif not inQuotes then @@ -143,13 +162,13 @@ local function checkBrackets(text) end end end - + if openCount > 0 then return false, "Missing closing bracket" elseif openCount < 0 then return false, "Extra closing bracket" end - + return true end @@ -157,10 +176,10 @@ end local function checkQuotes(text) local quoteCount = 0 local escaped = false - + for i = 1, string.len(text) do local char = string.sub(text, i, i) - + if escaped then escaped = false elseif char == "\\" then @@ -169,19 +188,19 @@ local function checkQuotes(text) quoteCount = quoteCount + 1 end end - + local quotient = math.floor(quoteCount / 2) if (quoteCount - (quotient * 2)) ~= 0 then return false, "Unmatched quotes" end - + return true end -- Validate conditional syntax local function validateConditional(conditional, args, action) local errors = {} - + -- Check if conditional is valid local baseCond = string.lower(conditional) if not VALID_CONDITIONALS[baseCond] then @@ -191,7 +210,7 @@ local function validateConditional(conditional, args, action) message = "Unknown conditional: " .. conditional }) end - + -- Check for required arguments local needsArgs = { combo = true, @@ -205,7 +224,7 @@ local function validateConditional(conditional, args, action) button = true, form = true, stance = true, } - + if needsArgs[baseCond] and (not args or args == "") and (not action or action == "") then table.insert(errors, { type = ERROR_TYPES.MISSING_ARGUMENT, @@ -213,9 +232,9 @@ local function validateConditional(conditional, args, action) message = conditional .. " requires an argument" }) end - + -- Check operator syntax for numeric comparisons - if args and string.find(baseCond, "hp") or string.find(baseCond, "power") or + if args and string.find(baseCond, "hp") or string.find(baseCond, "power") or string.find(baseCond, "combo") or baseCond == "stat" then local hasOperator = string.find(args, "[<>=~]+") if args ~= "" and not hasOperator and not string.find(args, "^%d+$") then @@ -229,20 +248,20 @@ local function validateConditional(conditional, args, action) end end end - + return errors end -- Parse and validate a single line local function validateLine(line, lineNum) local errors = {} - + -- Skip comments and empty lines line = CleveRoids.Trim(line) - if line == "" or string.sub(line, 1, 2) == "--" then + if line == "" or string.sub(line, 1, 2) == "--" or string.sub(line, 1, 1) == "#" then return errors end - + -- Check for valid command local _, _, cmd = string.find(line, "^(/[a-z]+)") if cmd then @@ -256,7 +275,7 @@ local function validateLine(line, lineNum) }) end end - + -- Check brackets local bracketsOk, bracketError = checkBrackets(line) if not bracketsOk then @@ -266,7 +285,7 @@ local function validateLine(line, lineNum) message = bracketError }) end - + -- Check quotes local quotesOk, quoteError = checkQuotes(line) if not quotesOk then @@ -276,61 +295,94 @@ local function validateLine(line, lineNum) message = quoteError }) end - - -- Parse conditionals if present - local _, cbEnd, conditionBlock = string.find(line, "%[(.+)%]") - if conditionBlock then - if CleveRoids.Trim(conditionBlock) == "" then - table.insert(errors, { - type = ERROR_TYPES.EMPTY_CONDITIONAL, - line = lineNum, - message = "Empty conditional block []" - }) - else - -- Check for invalid @ target syntax - local _, _, target = string.find(conditionBlock, "(@[^%s,]+)") - if target and not string.find(target, "^@[a-z]+") then - table.insert(errors, { - type = ERROR_TYPES.INVALID_TARGET, - line = lineNum, - message = "Invalid target: " .. target - }) - end - - -- Parse individual conditionals - local seenConditionals = {} - for _, condGroup in CleveRoids.splitStringIgnoringQuotes(conditionBlock, {",", " "}) do - if condGroup ~= "" and condGroup ~= target then - local parts = CleveRoids.splitStringIgnoringQuotes(condGroup, ":") - local cond = string.lower(CleveRoids.Trim(parts[1] or "")) - local args = CleveRoids.Trim(parts[2] or "") - - if cond ~= "" then - seenConditionals[cond] = true - - -- Validate the conditional - local condErrors = validateConditional(cond, args, nil) - for _, err in condErrors do - err.line = lineNum - table.insert(errors, err) + + -- Split by semicolons to handle multiple actions per line + local actions = CleveRoids.splitStringIgnoringQuotes(line, ";") + + for _, actionPart in ipairs(actions) do + actionPart = CleveRoids.Trim(actionPart) + if actionPart ~= "" and string.sub(actionPart, 1, 1) ~= "/" then + actionPart = "/" .. actionPart -- Add leading slash if missing after split + end + + -- Parse conditionals if present - use non-greedy match + local condStart = string.find(actionPart, "%[") + local condEnd = nil + local conditionBlock = nil + + if condStart then + -- Find matching closing bracket + local depth = 0 + local inQuotes = false + for i = condStart, string.len(actionPart) do + local char = string.sub(actionPart, i, i) + if char == '"' then + inQuotes = not inQuotes + elseif not inQuotes then + if char == "[" then + depth = depth + 1 + elseif char == "]" then + depth = depth - 1 + if depth == 0 then + condEnd = i + conditionBlock = string.sub(actionPart, condStart + 1, i - 1) + break end end end end end - - -- Check for action after conditionals - local actionPart = string.sub(line, (cbEnd or 0) + 1) - local _, _, action = string.find(actionPart, "^%s*[!~]?(.+)") - if not action or CleveRoids.Trim(action) == "" then - table.insert(errors, { - type = ERROR_TYPES.EMPTY_ACTION, - line = lineNum, - message = "Conditional has no action" - }) + + if conditionBlock then + if CleveRoids.Trim(conditionBlock) == "" then + table.insert(errors, { + type = ERROR_TYPES.EMPTY_CONDITIONAL, + line = lineNum, + message = "Empty conditional block []" + }) + else + -- Check for invalid @ target syntax + local _, _, target = string.find(conditionBlock, "(@[^%s,]+)") + if target and not string.find(target, "^@[a-z]+%d*") then + table.insert(errors, { + type = ERROR_TYPES.INVALID_TARGET, + line = lineNum, + message = "Invalid target: " .. target + }) + end + + -- Parse individual conditionals + for _, condGroup in CleveRoids.splitStringIgnoringQuotes(conditionBlock, {",", " "}) do + if condGroup ~= "" and condGroup ~= target then + local parts = CleveRoids.splitStringIgnoringQuotes(condGroup, ":") + local cond = string.lower(CleveRoids.Trim(parts[1] or "")) + local args = CleveRoids.Trim(parts[2] or "") + + if cond ~= "" then + -- Validate the conditional + local condErrors = validateConditional(cond, args, nil) + for _, err in condErrors do + err.line = lineNum + table.insert(errors, err) + end + end + end + end + end + + -- Check for action after conditionals + local afterCond = string.sub(actionPart, (condEnd or 0) + 1) + local _, _, action = string.find(afterCond, "^%s*[!~?]?(.+)") + if not action or CleveRoids.Trim(action) == "" then + table.insert(errors, { + type = ERROR_TYPES.EMPTY_ACTION, + line = lineNum, + message = "Conditional has no action" + }) + end end end - + return errors end @@ -338,14 +390,14 @@ end function CleveRoids.ValidateMacro(macroName) local errors = {} local macroID = GetMacroIndexByName(macroName) - + if not macroID or macroID == 0 then return {{ type = "ERROR", message = "Macro not found: " .. tostring(macroName) }} end - + local name, texture, body = GetMacroInfo(macroID) if not body or body == "" then return {{ @@ -353,17 +405,17 @@ function CleveRoids.ValidateMacro(macroName) message = "Macro is empty" }} end - + -- Split into lines local lines = CleveRoids.splitString(body, "\n") - + for lineNum, line in ipairs(lines) do local lineErrors = validateLine(line, lineNum) for _, err in lineErrors do table.insert(errors, err) end end - + return errors end @@ -371,7 +423,7 @@ end function CleveRoids.ValidateAllMacros() local results = {} local totalErrors = 0 - + local numMacros = GetNumMacros() for i = 1, numMacros do local name = GetMacroInfo(i) @@ -383,21 +435,21 @@ function CleveRoids.ValidateAllMacros() end end end - + return results, totalErrors end -- Print errors for a macro function CleveRoids.PrintMacroErrors(macroName) local errors = CleveRoids.ValidateMacro(macroName) - + if not errors or table.getn(errors) == 0 then CleveRoids.Print("|cff00ff00✓|r Macro '" .. macroName .. "' has no syntax errors") return end - + CleveRoids.Print("|cffff0000✗|r Macro '" .. macroName .. "' has " .. table.getn(errors) .. " error(s):") - + for _, err in errors do local line = err.line and ("Line " .. err.line .. ": ") or "" local msg = "|cffffaa00" .. line .. "|r" .. err.message @@ -408,18 +460,18 @@ end -- Print all macro errors function CleveRoids.PrintAllMacroErrors() local results, totalErrors = CleveRoids.ValidateAllMacros() - + if totalErrors == 0 then CleveRoids.Print("|cff00ff00✓|r All macros are error-free!") return end - + CleveRoids.Print("|cffff0000Found " .. totalErrors .. " error(s) in " .. table.getn(results) .. " macro(s):|r") - + for macroName, errors in pairs(results) do DEFAULT_CHAT_FRAME:AddMessage(" ") DEFAULT_CHAT_FRAME:AddMessage("|cffff8800" .. macroName .. "|r (" .. table.getn(errors) .. " error(s)):") - + for _, err in errors do local line = err.line and ("Line " .. err.line .. ": ") or "" local msg = " |cffffaa00" .. line .. "|r" .. err.message @@ -432,7 +484,7 @@ end SLASH_MACROCHECK1 = "/macrocheck" SlashCmdList.MACROCHECK = function(msg) msg = CleveRoids.Trim(msg or "") - + if msg == "" or msg == "all" then CleveRoids.PrintAllMacroErrors() else From 264633063a23122f896851ce2620f04bbe634425 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Wed, 29 Oct 2025 15:53:29 -0400 Subject: [PATCH 3/9] fix improper slashcommands --- MacroErrorChecker.lua | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/MacroErrorChecker.lua b/MacroErrorChecker.lua index c9923aa..88b28ba 100644 --- a/MacroErrorChecker.lua +++ b/MacroErrorChecker.lua @@ -79,22 +79,13 @@ local VALID_COMMANDS = { ["/cast"] = true, ["/castpet"] = true, ["/cancelaura"] = true, - ["/cancelform"] = true, ["/castsequence"] = true, - ["/castrandom"] = true, - ["/changeactionbar"] = true, - ["/clearfocus"] = true, - ["/cleartarget"] = true, - ["/click"] = true, - ["/dismount"] = true, ["/equip"] = true, ["/equipmh"] = true, ["/equipoh"] = true, - ["/equipslot"] = true, ["/focus"] = true, ["/petattack"] = true, ["/petfollow"] = true, - ["/petstay"] = true, ["/petwait"] = true, ["/petpassive"] = true, ["/petaggressive"] = true, @@ -107,19 +98,9 @@ local VALID_COMMANDS = { ["/stopattack"] = true, ["/stopcasting"] = true, ["/stopmacro"] = true, - ["/stopspelltarget"] = true, - ["/swapactionbar"] = true, ["/target"] = true, - ["/targetenemy"] = true, - ["/targetfriend"] = true, - ["/targetlastenemy"] = true, - ["/targetlastfriend"] = true, - ["/targetlasttarget"] = true, - ["/targetparty"] = true, - ["/targetraid"] = true, ["/unqueue"] = true, ["/use"] = true, - ["/userandom"] = true, ["/unbuff"] = true, ["/unshift"] = true, ["/retarget"] = true, From 246407183eb1ca61648799a2b43cc0de028a8eb8 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Tue, 11 Nov 2025 10:29:35 -0500 Subject: [PATCH 4/9] intercept addon errors from string parsing malformed macros --- MacroErrorChecker.lua | 521 ++++++++++++++++++++++++++---------------- 1 file changed, 326 insertions(+), 195 deletions(-) diff --git a/MacroErrorChecker.lua b/MacroErrorChecker.lua index 88b28ba..eb6ca93 100644 --- a/MacroErrorChecker.lua +++ b/MacroErrorChecker.lua @@ -11,41 +11,31 @@ local CleveRoids = _G.CleveRoids or {} -- Known valid conditionals local VALID_CONDITIONALS = { -- General - actionbar = true, noactionbar = true, - button = true, channeling = true, nochanneling = true, class = true, noclass = true, cdgcd = true, nocdgcd = true, combo = true, nocombo = true, cooldown = true, nocooldown = true, equipped = true, noequipped = true, - flyable = true, noflyable = true, - flying = true, noflying = true, form = true, noform = true, - group = true, nogroup = true, - indoors = true, noindoors = true, + group = true, known = true, noknown = true, mod = true, nomod = true, - mounted = true, nomounted = true, mybuff = true, nomybuff = true, mydebuff = true, nomydebuff = true, - myhp = true, nomyhp = true, - myhplost = true, nomyhplost = true, - mypower = true, nomypower = true, - mypowerlost = true, nomypowerlost = true, - myrawhp = true, nomyrawhp = true, - myrawpower = true, nomyrawpower = true, - outdoors = true, nooutdoors = true, + myhp = true, + myhplost = true, + mypower = true, + mypowerlost = true, + myrawhp = true, + myrawpower = true, pet = true, nopet = true, - petbuff = true, nopetbuff = true, - petdebuff = true, nopetdebuff = true, reactive = true, noreactive = true, resting = true, noresting = true, stance = true, nostance = true, - stat = true, nostat = true, + stat = true, stealth = true, nostealth = true, swimming = true, noswimming = true, - talent = true, notalent = true, zone = true, nozone = true, -- Unit based @@ -57,18 +47,18 @@ local VALID_CONDITIONALS = { debuff = true, nodebuff = true, harm = true, noharm = true, help = true, nohelp = true, - hp = true, nohp = true, - hplost = true, nohplost = true, + hp = true, + hplost = true, inrange = true, noinrange = true, - isnpc = true, noisnpc = true, - isplayer = true, noisplayer = true, - member = true, nomember = true, + isnpc = true, + isplayer = true, + member = true, party = true, noparty = true, - power = true, nopower = true, - powerlost = true, nopowerlost = true, + power = true, + powerlost = true, raid = true, noraid = true, - rawhp = true, norawhp = true, - rawpower = true, norawpower = true, + rawhp = true, + rawpower = true, type = true, notype = true, targeting = true, notargeting = true, exists = true, noexists = true, @@ -106,6 +96,36 @@ local VALID_COMMANDS = { ["/retarget"] = true, } +-- Safe string operations to prevent addon errors from malformed macros +local function safeStringSub(str, startPos, endPos) + if not str or type(str) ~= "string" then return "" end + local len = string.len(str) + if startPos < 1 then startPos = 1 end + if endPos and endPos > len then endPos = len end + return string.sub(str, startPos, endPos) +end + +local function safeStringFind(str, pattern, init) + if not str or type(str) ~= "string" then return nil end + local success, result1, result2, result3 = pcall(string.find, str, pattern, init) + if success then + return result1, result2, result3 + end + return nil +end + +local function safeStringLen(str) + if not str or type(str) ~= "string" then return 0 end + return string.len(str) +end + +local function safeTrim(str) + if not str or type(str) ~= "string" then return "" end + local success, result = pcall(CleveRoids.Trim, str) + if success then return result end + return str +end + -- Error types local ERROR_TYPES = { INVALID_CONDITIONAL = "Invalid conditional", @@ -124,11 +144,15 @@ CleveRoids.MacroErrors = {} -- Check if a string has balanced brackets local function checkBrackets(text) + if not text or type(text) ~= "string" then return true end + local openCount = 0 local inQuotes = false + local len = safeStringLen(text) - for i = 1, string.len(text) do - local char = string.sub(text, i, i) + for i = 1, len do + local char = safeStringSub(text, i, i) + if not char or char == "" then break end if char == '"' then inQuotes = not inQuotes @@ -155,11 +179,15 @@ end -- Check if quotes are balanced local function checkQuotes(text) + if not text or type(text) ~= "string" then return true end + local quoteCount = 0 local escaped = false + local len = safeStringLen(text) - for i = 1, string.len(text) do - local char = string.sub(text, i, i) + for i = 1, len do + local char = safeStringSub(text, i, i) + if not char or char == "" then break end if escaped then escaped = false @@ -181,9 +209,13 @@ end -- Validate conditional syntax local function validateConditional(conditional, args, action) local errors = {} + + if not conditional or conditional == "" then + return errors + end -- Check if conditional is valid - local baseCond = string.lower(conditional) + local baseCond = string.lower(safeTrim(conditional)) if not VALID_CONDITIONALS[baseCond] then table.insert(errors, { type = ERROR_TYPES.INVALID_CONDITIONAL, @@ -215,17 +247,20 @@ local function validateConditional(conditional, args, action) end -- Check operator syntax for numeric comparisons - if args and string.find(baseCond, "hp") or string.find(baseCond, "power") or - string.find(baseCond, "combo") or baseCond == "stat" then - local hasOperator = string.find(args, "[<>=~]+") - if args ~= "" and not hasOperator and not string.find(args, "^%d+$") then - -- Might be missing operator - if not string.find(args, "[a-zA-Z]") then - table.insert(errors, { - type = ERROR_TYPES.INVALID_OPERATOR, - conditional = conditional, - message = conditional .. " may need an operator (>, <, =, >=, <=)" - }) + if args and type(args) == "string" then + local hasHpOrPower = safeStringFind(baseCond, "hp") or safeStringFind(baseCond, "power") or + safeStringFind(baseCond, "combo") or baseCond == "stat" + if hasHpOrPower then + local hasOperator = safeStringFind(args, "[<>=~]+") + if args ~= "" and not hasOperator and not safeStringFind(args, "^%d+$") then + -- Might be missing operator + if not safeStringFind(args, "[a-zA-Z]") then + table.insert(errors, { + type = ERROR_TYPES.INVALID_OPERATOR, + conditional = conditional, + message = conditional .. " may need an operator (>, <, =, >=, <=)" + }) + end end end end @@ -238,130 +273,164 @@ local function validateLine(line, lineNum) local errors = {} -- Skip comments and empty lines - line = CleveRoids.Trim(line) - if line == "" or string.sub(line, 1, 2) == "--" or string.sub(line, 1, 1) == "#" then + if not line or type(line) ~= "string" then return errors end - -- Check for valid command - local _, _, cmd = string.find(line, "^(/[a-z]+)") - if cmd then - local lowerCmd = string.lower(cmd) - if not VALID_COMMANDS[lowerCmd] then - table.insert(errors, { - type = ERROR_TYPES.INVALID_COMMAND, - line = lineNum, - command = cmd, - message = "Unknown command: " .. cmd - }) - end + line = safeTrim(line) + if line == "" or safeStringSub(line, 1, 2) == "--" or safeStringSub(line, 1, 1) == "#" then + return errors end - -- Check brackets - local bracketsOk, bracketError = checkBrackets(line) - if not bracketsOk then - table.insert(errors, { - type = ERROR_TYPES.MISMATCHED_BRACKETS, - line = lineNum, - message = bracketError - }) - end + -- Wrap the entire validation in pcall to catch any unexpected errors + local success, result = pcall(function() + local localErrors = {} - -- Check quotes - local quotesOk, quoteError = checkQuotes(line) - if not quotesOk then - table.insert(errors, { - type = ERROR_TYPES.MALFORMED_QUOTES, - line = lineNum, - message = quoteError - }) - end - - -- Split by semicolons to handle multiple actions per line - local actions = CleveRoids.splitStringIgnoringQuotes(line, ";") - - for _, actionPart in ipairs(actions) do - actionPart = CleveRoids.Trim(actionPart) - if actionPart ~= "" and string.sub(actionPart, 1, 1) ~= "/" then - actionPart = "/" .. actionPart -- Add leading slash if missing after split - end - - -- Parse conditionals if present - use non-greedy match - local condStart = string.find(actionPart, "%[") - local condEnd = nil - local conditionBlock = nil - - if condStart then - -- Find matching closing bracket - local depth = 0 - local inQuotes = false - for i = condStart, string.len(actionPart) do - local char = string.sub(actionPart, i, i) - if char == '"' then - inQuotes = not inQuotes - elseif not inQuotes then - if char == "[" then - depth = depth + 1 - elseif char == "]" then - depth = depth - 1 - if depth == 0 then - condEnd = i - conditionBlock = string.sub(actionPart, condStart + 1, i - 1) - break - end - end - end + -- Check for valid command + local _, _, cmd = safeStringFind(line, "^(/[a-z]+)") + if cmd then + local lowerCmd = string.lower(cmd) + if not VALID_COMMANDS[lowerCmd] then + table.insert(localErrors, { + type = ERROR_TYPES.INVALID_COMMAND, + line = lineNum, + command = cmd, + message = "Unknown command: " .. cmd + }) end end - if conditionBlock then - if CleveRoids.Trim(conditionBlock) == "" then - table.insert(errors, { - type = ERROR_TYPES.EMPTY_CONDITIONAL, - line = lineNum, - message = "Empty conditional block []" - }) - else - -- Check for invalid @ target syntax - local _, _, target = string.find(conditionBlock, "(@[^%s,]+)") - if target and not string.find(target, "^@[a-z]+%d*") then - table.insert(errors, { - type = ERROR_TYPES.INVALID_TARGET, - line = lineNum, - message = "Invalid target: " .. target - }) - end + -- Check brackets + local bracketsOk, bracketError = checkBrackets(line) + if not bracketsOk then + table.insert(localErrors, { + type = ERROR_TYPES.MISMATCHED_BRACKETS, + line = lineNum, + message = bracketError or "Bracket mismatch" + }) + end - -- Parse individual conditionals - for _, condGroup in CleveRoids.splitStringIgnoringQuotes(conditionBlock, {",", " "}) do - if condGroup ~= "" and condGroup ~= target then - local parts = CleveRoids.splitStringIgnoringQuotes(condGroup, ":") - local cond = string.lower(CleveRoids.Trim(parts[1] or "")) - local args = CleveRoids.Trim(parts[2] or "") + -- Check quotes + local quotesOk, quoteError = checkQuotes(line) + if not quotesOk then + table.insert(localErrors, { + type = ERROR_TYPES.MALFORMED_QUOTES, + line = lineNum, + message = quoteError or "Quote mismatch" + }) + end - if cond ~= "" then - -- Validate the conditional - local condErrors = validateConditional(cond, args, nil) - for _, err in condErrors do - err.line = lineNum - table.insert(errors, err) + -- Split by semicolons to handle multiple actions per line + local actions = CleveRoids.splitStringIgnoringQuotes(line, ";") + if not actions then + return localErrors + end + + for _, actionPart in ipairs(actions) do + actionPart = safeTrim(actionPart) + if actionPart ~= "" and safeStringSub(actionPart, 1, 1) ~= "/" then + actionPart = "/" .. actionPart -- Add leading slash if missing after split + end + + -- Parse conditionals if present - use non-greedy match + local condStart = safeStringFind(actionPart, "%[") + local condEnd = nil + local conditionBlock = nil + + if condStart then + -- Find matching closing bracket + local depth = 0 + local inQuotes = false + local len = safeStringLen(actionPart) + + for i = condStart, len do + local char = safeStringSub(actionPart, i, i) + if not char or char == "" then break end + + if char == '"' then + inQuotes = not inQuotes + elseif not inQuotes then + if char == "[" then + depth = depth + 1 + elseif char == "]" then + depth = depth - 1 + if depth == 0 then + condEnd = i + conditionBlock = safeStringSub(actionPart, condStart + 1, i - 1) + break end end end end end - -- Check for action after conditionals - local afterCond = string.sub(actionPart, (condEnd or 0) + 1) - local _, _, action = string.find(afterCond, "^%s*[!~?]?(.+)") - if not action or CleveRoids.Trim(action) == "" then - table.insert(errors, { - type = ERROR_TYPES.EMPTY_ACTION, - line = lineNum, - message = "Conditional has no action" - }) + if conditionBlock then + if safeTrim(conditionBlock) == "" then + table.insert(localErrors, { + type = ERROR_TYPES.EMPTY_CONDITIONAL, + line = lineNum, + message = "Empty conditional block []" + }) + else + -- Check for invalid @ target syntax + local _, _, target = safeStringFind(conditionBlock, "(@[^%s,]+)") + if target and not safeStringFind(target, "^@[a-z]+%d*") then + table.insert(localErrors, { + type = ERROR_TYPES.INVALID_TARGET, + line = lineNum, + message = "Invalid target: " .. target + }) + end + + -- Parse individual conditionals + local condGroups = CleveRoids.splitStringIgnoringQuotes(conditionBlock, {",", " "}) + if condGroups then + for _, condGroup in condGroups do + if condGroup ~= "" and condGroup ~= target then + local parts = CleveRoids.splitStringIgnoringQuotes(condGroup, ":") + if parts then + local cond = string.lower(safeTrim(parts[1] or "")) + local args = safeTrim(parts[2] or "") + + if cond ~= "" then + -- Validate the conditional + local condErrors = validateConditional(cond, args, nil) + for _, err in condErrors do + err.line = lineNum + table.insert(localErrors, err) + end + end + end + end + end + end + end + + -- Check for action after conditionals + local afterCond = safeStringSub(actionPart, (condEnd or 0) + 1) + local _, _, action = safeStringFind(afterCond, "^%s*[!~?]?(.+)") + if not action or safeTrim(action) == "" then + table.insert(localErrors, { + type = ERROR_TYPES.EMPTY_ACTION, + line = lineNum, + message = "Conditional has no action" + }) + end end end + + return localErrors + end) + + if success and result then + return result + elseif not success then + -- An error occurred during validation + return {{ + type = "VALIDATION_ERROR", + line = lineNum, + message = "Internal error validating line: " .. tostring(result) + }} end return errors @@ -369,35 +438,64 @@ end -- Validate an entire macro function CleveRoids.ValidateMacro(macroName) - local errors = {} - local macroID = GetMacroIndexByName(macroName) - - if not macroID or macroID == 0 then - return {{ - type = "ERROR", - message = "Macro not found: " .. tostring(macroName) - }} - end - - local name, texture, body = GetMacroInfo(macroID) - if not body or body == "" then - return {{ - type = "ERROR", - message = "Macro is empty" - }} - end - - -- Split into lines - local lines = CleveRoids.splitString(body, "\n") - - for lineNum, line in ipairs(lines) do - local lineErrors = validateLine(line, lineNum) - for _, err in lineErrors do - table.insert(errors, err) + -- Wrap entire function in pcall for safety + local success, result = pcall(function() + local errors = {} + + if not macroName or macroName == "" then + return {{ + type = "ERROR", + message = "No macro name provided" + }} end - end + + local macroID = GetMacroIndexByName(macroName) - return errors + if not macroID or macroID == 0 then + return {{ + type = "ERROR", + message = "Macro not found: " .. tostring(macroName) + }} + end + + local name, texture, body = GetMacroInfo(macroID) + if not body or body == "" then + return {{ + type = "ERROR", + message = "Macro is empty" + }} + end + + -- Split into lines + local lines = CleveRoids.splitString(body, "\n") + if not lines then + return {{ + type = "ERROR", + message = "Failed to parse macro body" + }} + end + + for lineNum, line in ipairs(lines) do + local lineErrors = validateLine(line, lineNum) + if lineErrors then + for _, err in lineErrors do + table.insert(errors, err) + end + end + end + + return errors + end) + + if success then + return result + else + -- Return a safe error message if validation itself fails + return {{ + type = "CRITICAL_ERROR", + message = "Critical error validating macro: " .. tostring(result) + }} + end end -- Validate all macros @@ -405,11 +503,19 @@ function CleveRoids.ValidateAllMacros() local results = {} local totalErrors = 0 - local numMacros = GetNumMacros() + local success, numMacros = pcall(GetNumMacros) + if not success or not numMacros then + return results, 0 + end + for i = 1, numMacros do - local name = GetMacroInfo(i) - if name then + -- Wrap each macro validation in pcall so one bad macro doesn't stop all validation + local nameSuccess, name = pcall(GetMacroInfo, i) + + if nameSuccess and name and name ~= "" then + -- Validate this macro local errors = CleveRoids.ValidateMacro(name) + if errors and table.getn(errors) > 0 then results[name] = errors totalErrors = totalErrors + table.getn(errors) @@ -422,7 +528,12 @@ end -- Print errors for a macro function CleveRoids.PrintMacroErrors(macroName) - local errors = CleveRoids.ValidateMacro(macroName) + local success, errors = pcall(CleveRoids.ValidateMacro, macroName) + + if not success then + CleveRoids.Print("|cffff0000Error|r: Failed to validate macro '" .. tostring(macroName) .. "': " .. tostring(errors)) + return + end if not errors or table.getn(errors) == 0 then CleveRoids.Print("|cff00ff00✓|r Macro '" .. macroName .. "' has no syntax errors") @@ -432,31 +543,45 @@ function CleveRoids.PrintMacroErrors(macroName) CleveRoids.Print("|cffff0000✗|r Macro '" .. macroName .. "' has " .. table.getn(errors) .. " error(s):") for _, err in errors do - local line = err.line and ("Line " .. err.line .. ": ") or "" - local msg = "|cffffaa00" .. line .. "|r" .. err.message - DEFAULT_CHAT_FRAME:AddMessage(" " .. msg) + if err and err.message then + local line = err.line and ("Line " .. err.line .. ": ") or "" + local msg = "|cffffaa00" .. line .. "|r" .. err.message + pcall(DEFAULT_CHAT_FRAME.AddMessage, DEFAULT_CHAT_FRAME, " " .. msg) + end end end -- Print all macro errors function CleveRoids.PrintAllMacroErrors() - local results, totalErrors = CleveRoids.ValidateAllMacros() + local success, results, totalErrors = pcall(CleveRoids.ValidateAllMacros) + + if not success then + CleveRoids.Print("|cffff0000Error|r: Failed to validate macros: " .. tostring(results)) + return + end if totalErrors == 0 then CleveRoids.Print("|cff00ff00✓|r All macros are error-free!") return end - CleveRoids.Print("|cffff0000Found " .. totalErrors .. " error(s) in " .. table.getn(results) .. " macro(s):|r") + local macroCount = 0 + for _ in pairs(results) do macroCount = macroCount + 1 end + + CleveRoids.Print("|cffff0000Found " .. totalErrors .. " error(s) in " .. macroCount .. " macro(s):|r") for macroName, errors in pairs(results) do - DEFAULT_CHAT_FRAME:AddMessage(" ") - DEFAULT_CHAT_FRAME:AddMessage("|cffff8800" .. macroName .. "|r (" .. table.getn(errors) .. " error(s)):") + if macroName and errors then + pcall(DEFAULT_CHAT_FRAME.AddMessage, DEFAULT_CHAT_FRAME, " ") + pcall(DEFAULT_CHAT_FRAME.AddMessage, DEFAULT_CHAT_FRAME, "|cffff8800" .. macroName .. "|r (" .. table.getn(errors) .. " error(s)):") - for _, err in errors do - local line = err.line and ("Line " .. err.line .. ": ") or "" - local msg = " |cffffaa00" .. line .. "|r" .. err.message - DEFAULT_CHAT_FRAME:AddMessage(msg) + for _, err in errors do + if err and err.message then + local line = err.line and ("Line " .. err.line .. ": ") or "" + local msg = " |cffffaa00" .. line .. "|r" .. err.message + pcall(DEFAULT_CHAT_FRAME.AddMessage, DEFAULT_CHAT_FRAME, msg) + end + end end end end @@ -464,12 +589,18 @@ end -- Slash command SLASH_MACROCHECK1 = "/macrocheck" SlashCmdList.MACROCHECK = function(msg) - msg = CleveRoids.Trim(msg or "") + local success, result = pcall(function() + msg = safeTrim(msg or "") - if msg == "" or msg == "all" then - CleveRoids.PrintAllMacroErrors() - else - CleveRoids.PrintMacroErrors(msg) + if msg == "" or msg == "all" then + CleveRoids.PrintAllMacroErrors() + else + CleveRoids.PrintMacroErrors(msg) + end + end) + + if not success then + CleveRoids.Print("|cffff0000Error|r: Macro check failed: " .. tostring(result)) end end From 5c9e00e24364812ad0fb8b31741e76012c5deeb2 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Tue, 11 Nov 2025 10:51:50 -0500 Subject: [PATCH 5/9] update errorchecker to parse all thirty six macros --- MacroErrorChecker.lua | 44 +++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/MacroErrorChecker.lua b/MacroErrorChecker.lua index eb6ca93..0148497 100644 --- a/MacroErrorChecker.lua +++ b/MacroErrorChecker.lua @@ -62,6 +62,10 @@ local VALID_CONDITIONALS = { type = true, notype = true, targeting = true, notargeting = true, exists = true, noexists = true, + onswingpending = true, noonswingpending = true, + mybuffcount = true, nomybuffcount = true, + mhimbue = true, nomhimbue = true, + ohimbue = true, noohimbue = true, } -- Known valid commands @@ -94,6 +98,17 @@ local VALID_COMMANDS = { ["/unbuff"] = true, ["/unshift"] = true, ["/retarget"] = true, + ["/macrocheck"] = true, + ["/s"] = true, + ["/y"] = true, + ["/r"] = true, + ["/bg"] = true, + ["/e"] = true, + ["/w"] = true, + ["/g"] = true, + ["/p"] = true, + ["/invite"] = true, + ["/trade"] = true, } -- Safe string operations to prevent addon errors from malformed macros @@ -503,20 +518,25 @@ function CleveRoids.ValidateAllMacros() local results = {} local totalErrors = 0 - local success, numMacros = pcall(GetNumMacros) - if not success or not numMacros then - return results, 0 - end - - for i = 1, numMacros do - -- Wrap each macro validation in pcall so one bad macro doesn't stop all validation + -- Account-wide macros are indexed from 1 up to GetNumMacros(). + -- Character-specific macros occupy the slots immediately following the account-wide ones. + -- In Classic clients, the macro UI has 18 General (Account) slots and 18 Character-Specific slots. + local numAccountMacros = GetNumMacros() + + -- The WoW API GetMacroInfo(index) supports indexing up to 36 (1-18 for General, 19-36 for Character) + -- in Classic clients, even though the total is GetNumMacros() + GetNumCharacterMacros() in Retail. + -- To ensure we check all 36 possible slots: + local totalSlots = 36 + + for i = 1, totalSlots do local nameSuccess, name = pcall(GetMacroInfo, i) - + + -- Check if GetMacroInfo returned a name (i.e., the slot is used) if nameSuccess and name and name ~= "" then - -- Validate this macro - local errors = CleveRoids.ValidateMacro(name) - - if errors and table.getn(errors) > 0 then + -- Wrap each macro validation in pcall so one bad macro doesn't stop all validation + local errorsSuccess, errors = pcall(CleveRoids.ValidateMacro, name) + + if errorsSuccess and errors and table.getn(errors) > 0 then results[name] = errors totalErrors = totalErrors + table.getn(errors) end From a7157fb17e29b061034ad623765a52255af08110 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Tue, 11 Nov 2025 14:13:23 -0500 Subject: [PATCH 6/9] fix checking errors on showtooltip lines and some conditionals not needing an argument for some slash commands --- MacroErrorChecker.lua | 104 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 84 insertions(+), 20 deletions(-) diff --git a/MacroErrorChecker.lua b/MacroErrorChecker.lua index 0148497..01ffc6c 100644 --- a/MacroErrorChecker.lua +++ b/MacroErrorChecker.lua @@ -109,6 +109,39 @@ local VALID_COMMANDS = { ["/p"] = true, ["/invite"] = true, ["/trade"] = true, + ["/db"] = true, + ["/roll"] = true, + ["/bow"] = true, + ["/qh"] = true, + ["/rinse"] = true, + ["/am"] = true, + ["/aux"] = true, + ["/instancetimers"] = true, + ["/umacro"] = true, + ["/camp"] = true, + ["/logout"] = true, + ["/exit"] = true, + ["/promote"] = true, +} + +-- Commands that can have conditionals without actions +-- e.g., /petattack [harm] or /target [exists,hp:<=20] +local COMMANDS_NO_ACTION_NEEDED = { + ["/petattack"] = true, + ["/petfollow"] = true, + ["/petwait"] = true, + ["/petpassive"] = true, + ["/petaggressive"] = true, + ["/petdefensive"] = true, + ["/target"] = true, + ["/focus"] = true, + ["/startattack"] = true, + ["/stopattack"] = true, + ["/stopcasting"] = true, + ["/unqueue"] = true, + ["/retarget"] = true, + ["/stopmacro"] = true, + ["/unshift"] = true, } -- Safe string operations to prevent addon errors from malformed macros @@ -160,7 +193,7 @@ CleveRoids.MacroErrors = {} -- Check if a string has balanced brackets local function checkBrackets(text) if not text or type(text) ~= "string" then return true end - + local openCount = 0 local inQuotes = false local len = safeStringLen(text) @@ -195,7 +228,7 @@ end -- Check if quotes are balanced local function checkQuotes(text) if not text or type(text) ~= "string" then return true end - + local quoteCount = 0 local escaped = false local len = safeStringLen(text) @@ -224,7 +257,7 @@ end -- Validate conditional syntax local function validateConditional(conditional, args, action) local errors = {} - + if not conditional or conditional == "" then return errors end @@ -293,7 +326,25 @@ local function validateLine(line, lineNum) end line = safeTrim(line) - if line == "" or safeStringSub(line, 1, 2) == "--" or safeStringSub(line, 1, 1) == "#" then + if line == "" or safeStringSub(line, 1, 2) == "--" then + return errors + end + + -- Check for # directives - only #showtooltip is valid + if safeStringSub(line, 1, 1) == "#" then + local _, _, directive = safeStringFind(line, "^(#[a-z]+)") + if directive then + local lowerDirective = string.lower(directive) + if lowerDirective ~= "#showtooltip" then + table.insert(errors, { + type = ERROR_TYPES.INVALID_COMMAND, + line = lineNum, + command = directive, + message = "Unknown directive: " .. directive .. " (did you mean #showtooltip?)" + }) + end + end + -- Valid #showtooltip or other # lines are skipped from further validation return errors end @@ -357,11 +408,11 @@ local function validateLine(line, lineNum) local depth = 0 local inQuotes = false local len = safeStringLen(actionPart) - + for i = condStart, len do local char = safeStringSub(actionPart, i, i) if not char or char == "" then break end - + if char == '"' then inQuotes = not inQuotes elseif not inQuotes then @@ -422,14 +473,27 @@ local function validateLine(line, lineNum) end -- Check for action after conditionals - local afterCond = safeStringSub(actionPart, (condEnd or 0) + 1) - local _, _, action = safeStringFind(afterCond, "^%s*[!~?]?(.+)") - if not action or safeTrim(action) == "" then - table.insert(localErrors, { - type = ERROR_TYPES.EMPTY_ACTION, - line = lineNum, - message = "Conditional has no action" - }) + -- Extract the command from this action part + local _, _, cmdFromAction = safeStringFind(actionPart, "^(/[a-z]+)") + local needsAction = true + + if cmdFromAction then + local lowerCmdFromAction = string.lower(cmdFromAction) + if COMMANDS_NO_ACTION_NEEDED[lowerCmdFromAction] then + needsAction = false + end + end + + if needsAction then + local afterCond = safeStringSub(actionPart, (condEnd or 0) + 1) + local _, _, action = safeStringFind(afterCond, "^%s*[!~?]?(.+)") + if not action or safeTrim(action) == "" then + table.insert(localErrors, { + type = ERROR_TYPES.EMPTY_ACTION, + line = lineNum, + message = "Conditional has no action" + }) + end end end end @@ -456,14 +520,14 @@ function CleveRoids.ValidateMacro(macroName) -- Wrap entire function in pcall for safety local success, result = pcall(function() local errors = {} - + if not macroName or macroName == "" then return {{ type = "ERROR", message = "No macro name provided" }} end - + local macroID = GetMacroIndexByName(macroName) if not macroID or macroID == 0 then @@ -501,7 +565,7 @@ function CleveRoids.ValidateMacro(macroName) return errors end) - + if success then return result else @@ -549,7 +613,7 @@ end -- Print errors for a macro function CleveRoids.PrintMacroErrors(macroName) local success, errors = pcall(CleveRoids.ValidateMacro, macroName) - + if not success then CleveRoids.Print("|cffff0000Error|r: Failed to validate macro '" .. tostring(macroName) .. "': " .. tostring(errors)) return @@ -574,7 +638,7 @@ end -- Print all macro errors function CleveRoids.PrintAllMacroErrors() local success, results, totalErrors = pcall(CleveRoids.ValidateAllMacros) - + if not success then CleveRoids.Print("|cffff0000Error|r: Failed to validate macros: " .. tostring(results)) return @@ -618,7 +682,7 @@ SlashCmdList.MACROCHECK = function(msg) CleveRoids.PrintMacroErrors(msg) end end) - + if not success then CleveRoids.Print("|cffff0000Error|r: Macro check failed: " .. tostring(result)) end From f7a4f6e5a29ab31717c24dbbad7a34b2c69ac709 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Sat, 7 Feb 2026 13:16:25 -0500 Subject: [PATCH 7/9] update --- MacroErrorChecker.lua | 125 ++++++++++++++++++++++-------------------- 1 file changed, 66 insertions(+), 59 deletions(-) diff --git a/MacroErrorChecker.lua b/MacroErrorChecker.lua index 01ffc6c..50e1396 100644 --- a/MacroErrorChecker.lua +++ b/MacroErrorChecker.lua @@ -9,67 +9,35 @@ local _G = _G or getfenv(0) local CleveRoids = _G.CleveRoids or {} -- Known valid conditionals +-- Minimal static entries for special cases not in CleveRoids.Keywords +-- The bulk of valid conditionals are auto-populated from Keywords below local VALID_CONDITIONALS = { - -- General - channeling = true, nochanneling = true, - class = true, noclass = true, - cdgcd = true, nocdgcd = true, - combo = true, nocombo = true, - cooldown = true, nocooldown = true, - equipped = true, noequipped = true, - form = true, noform = true, - group = true, - known = true, noknown = true, - mod = true, nomod = true, - mybuff = true, nomybuff = true, - mydebuff = true, nomydebuff = true, - myhp = true, - myhplost = true, - mypower = true, - mypowerlost = true, - myrawhp = true, - myrawpower = true, - pet = true, nopet = true, - reactive = true, noreactive = true, - resting = true, noresting = true, - stance = true, nostance = true, - stat = true, - stealth = true, nostealth = true, - swimming = true, noswimming = true, - zone = true, nozone = true, - - -- Unit based - alive = true, noalive = true, - buff = true, nobuff = true, - casting = true, nocasting = true, - combat = true, nocombat = true, - dead = true, nodead = true, - debuff = true, nodebuff = true, - harm = true, noharm = true, - help = true, nohelp = true, - hp = true, - hplost = true, - inrange = true, noinrange = true, - isnpc = true, - isplayer = true, - member = true, - party = true, noparty = true, - power = true, - powerlost = true, - raid = true, noraid = true, - rawhp = true, - rawpower = true, - type = true, notype = true, - targeting = true, notargeting = true, - exists = true, noexists = true, - onswingpending = true, noonswingpending = true, - mybuffcount = true, nomybuffcount = true, - mhimbue = true, nomhimbue = true, - ohimbue = true, noohimbue = true, + -- multiscan is processed in Core.lua before Keywords loop (target resolution) + -- It's in ignoreKeywords, not Keywords, but users write it in macros + multiscan = true, } +-- Auto-populate from CleveRoids.Keywords (all registered conditionals) +-- MacroErrorChecker.lua loads after Conditionals.lua so Keywords is populated +if CleveRoids.Keywords then + for keyword, _ in pairs(CleveRoids.Keywords) do + VALID_CONDITIONALS[keyword] = true + end +end + +-- Also add user-facing entries from ignoreKeywords (multiscan already added above) +if CleveRoids.ignoreKeywords then + for keyword, _ in pairs(CleveRoids.ignoreKeywords) do + -- Skip internal metadata keys that users never type in macros + if keyword ~= "_operators" and keyword ~= "_groups" and keyword ~= "action" then + VALID_CONDITIONALS[keyword] = true + end + end +end + -- Known valid commands local VALID_COMMANDS = { + -- Core addon commands ["/cast"] = true, ["/castpet"] = true, ["/cancelaura"] = true, @@ -77,6 +45,10 @@ local VALID_COMMANDS = { ["/equip"] = true, ["/equipmh"] = true, ["/equipoh"] = true, + ["/equip11"] = true, + ["/equip12"] = true, + ["/equip13"] = true, + ["/equip14"] = true, ["/focus"] = true, ["/petattack"] = true, ["/petfollow"] = true, @@ -92,13 +64,28 @@ local VALID_COMMANDS = { ["/stopattack"] = true, ["/stopcasting"] = true, ["/stopmacro"] = true, + ["/skipmacro"] = true, ["/target"] = true, + ["/cleartarget"] = true, ["/unqueue"] = true, ["/use"] = true, ["/unbuff"] = true, ["/unshift"] = true, ["/retarget"] = true, + ["/firstaction"] = true, + ["/nofirstaction"] = true, + ["/applymain"] = true, + ["/applyoff"] = true, + ["/clearequipqueue"] = true, + ["/equipqueuestatus"] = true, + ["/quickheal"] = true, + ["/qh"] = true, + ["/rl"] = true, + ["/combotrack"] = true, + ["/cleveroid"] = true, + ["/cleveroidmacros"] = true, ["/macrocheck"] = true, + -- Chat / emotes / common WoW commands ["/s"] = true, ["/y"] = true, ["/r"] = true, @@ -112,7 +99,6 @@ local VALID_COMMANDS = { ["/db"] = true, ["/roll"] = true, ["/bow"] = true, - ["/qh"] = true, ["/rinse"] = true, ["/am"] = true, ["/aux"] = true, @@ -134,6 +120,7 @@ local COMMANDS_NO_ACTION_NEEDED = { ["/petaggressive"] = true, ["/petdefensive"] = true, ["/target"] = true, + ["/cleartarget"] = true, ["/focus"] = true, ["/startattack"] = true, ["/stopattack"] = true, @@ -141,7 +128,21 @@ local COMMANDS_NO_ACTION_NEEDED = { ["/unqueue"] = true, ["/retarget"] = true, ["/stopmacro"] = true, + ["/skipmacro"] = true, ["/unshift"] = true, + ["/firstaction"] = true, + ["/nofirstaction"] = true, + ["/clearequipqueue"] = true, + ["/equipqueuestatus"] = true, + ["/quickheal"] = true, + ["/qh"] = true, + ["/rl"] = true, + ["/combotrack"] = true, + ["/cleveroid"] = true, + ["/cleveroidmacros"] = true, + ["/print"] = true, + ["/run"] = true, + ["/script"] = true, } -- Safe string operations to prevent addon errors from malformed macros @@ -284,6 +285,12 @@ local function validateConditional(conditional, args, action) actionbar = true, button = true, form = true, stance = true, + level = true, mylevel = true, + distance = true, nodistance = true, + swingtimer = true, stimer = true, + threat = true, + ttk = true, tte = true, + spellcasttime = true, nospellcasttime = true, } if needsArgs[baseCond] and (not args or args == "") and (not action or action == "") then @@ -353,7 +360,7 @@ local function validateLine(line, lineNum) local localErrors = {} -- Check for valid command - local _, _, cmd = safeStringFind(line, "^(/[a-z]+)") + local _, _, cmd = safeStringFind(line, "^(/[a-z]+%d*)") if cmd then local lowerCmd = string.lower(cmd) if not VALID_COMMANDS[lowerCmd] then @@ -474,7 +481,7 @@ local function validateLine(line, lineNum) -- Check for action after conditionals -- Extract the command from this action part - local _, _, cmdFromAction = safeStringFind(actionPart, "^(/[a-z]+)") + local _, _, cmdFromAction = safeStringFind(actionPart, "^(/[a-z]+%d*)") local needsAction = true if cmdFromAction then From b0cd1c06571d282f9512599b4687de8607f5e5eb Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Sun, 8 Feb 2026 21:24:46 -0500 Subject: [PATCH 8/9] ingame error checker gui --- Extensions/MacroErrorUI.lua | 648 ++++++++++++++++++++++++++++++++++++ MacroErrorChecker.lua | 108 +++++- SuperCleveRoidMacros.toc | 1 + 3 files changed, 756 insertions(+), 1 deletion(-) create mode 100644 Extensions/MacroErrorUI.lua diff --git a/Extensions/MacroErrorUI.lua b/Extensions/MacroErrorUI.lua new file mode 100644 index 0000000..b2304db --- /dev/null +++ b/Extensions/MacroErrorUI.lua @@ -0,0 +1,648 @@ +--[[ + Macro Error UI - Real-time visual error feedback in the macro editor + Author: Mewtiny + License: MIT License + + Provides: + - Error summary panel anchored below MacroFrame + - Red semi-transparent backdrop on error lines in the EditBox +]] +local _G = _G or getfenv(0) +local CleveRoids = _G.CleveRoids or {} + +local Extension = CleveRoids.RegisterExtension("MacroErrorUI") + +-- Constants +local DEBOUNCE_DELAY = 0.3 +local MAX_PANEL_HEIGHT = 120 +local MAX_DISPLAY_ERRORS = 5 +local HIGHLIGHT_POOL_SIZE = 20 +local ERROR_FONT_POOL_SIZE = 6 -- header + max errors + +-- State +local errorPanel = nil +local headerText = nil +local errorFontStrings = {} +local lineHighlights = {} +local lastKeystroke = 0 +local pendingValidation = false +local lastSelectedMacro = nil +local updateFrame = nil +local hooked = false + +-- Cache for current errors (avoids re-validation on every frame) +local currentErrors = nil +local nameHighlight = nil -- Yellow backdrop behind macro name when name has errors + +-- ============================================================================ +-- Error Panel (Option A) +-- ============================================================================ + +local function CreateErrorPanel() + if errorPanel then return end + + local panel = CreateFrame("Frame", "CleveRoidsErrorPanel", MacroFrame) + -- Anchor to the scroll frame (text edit area) instead of the full MacroFrame, + -- which can extend far below the visible UI on extended macro clients + local scrollRef = MacroFrameScrollFrame or MacroFrame + panel:SetPoint("TOPLEFT", scrollRef, "BOTTOMLEFT", 0, -27) + panel:SetPoint("TOPRIGHT", scrollRef, "BOTTOMRIGHT", 0, -27) + panel:SetHeight(1) + panel:SetFrameStrata("DIALOG") + panel:SetBackdrop({ + bgFile = "Interface\\Tooltips\\UI-Tooltip-Background", + edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", + tile = true, tileSize = 16, edgeSize = 12, + insets = { left = 3, right = 3, top = 3, bottom = 3 }, + }) + panel:SetBackdropColor(0.1, 0.05, 0.05, 0.92) + panel:SetBackdropBorderColor(0.6, 0.1, 0.1, 0.8) + panel:Hide() + + -- Header: "N error(s) found" + headerText = panel:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall") + headerText:SetPoint("TOPLEFT", panel, "TOPLEFT", 8, -8) + headerText:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -8, -8) + headerText:SetTextColor(1, 0.3, 0.3, 1) + + -- Pre-allocate error message FontStrings + for i = 1, MAX_DISPLAY_ERRORS do + local fs = panel:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall") + fs:SetPoint("TOPLEFT", headerText, "BOTTOMLEFT", 0, -2 - (i - 1) * 12) + fs:SetPoint("TOPRIGHT", headerText, "BOTTOMRIGHT", 0, -2 - (i - 1) * 12) + fs:SetJustifyH("LEFT") + fs:Hide() + errorFontStrings[i] = fs + end + + errorPanel = panel +end + +local function UpdateErrorPanel(errors) + if not errorPanel then return end + + local count = errors and table.getn(errors) or 0 + + if count == 0 then + errorPanel:Hide() + return + end + + -- Header + if count == 1 then + headerText:SetText("1 error found") + else + headerText:SetText(count .. " errors found") + end + + -- Populate error lines + local displayed = 0 + for i = 1, MAX_DISPLAY_ERRORS do + local fs = errorFontStrings[i] + if i <= count then + local err = errors[i] + local linePrefix = err.line and ("Line " .. err.line .. ": ") or "" + local msg = linePrefix .. (err.message or "Unknown error") + -- Truncate long messages + if string.len(msg) > 80 then + msg = string.sub(msg, 1, 77) .. "..." + end + -- Name errors in yellow, syntax errors in red + local color = err.type == "NAME_ERROR" and "|cffffcc60" or "|cffffa0a0" + fs:SetText(color .. msg .. "|r") + fs:Show() + displayed = displayed + 1 + else + fs:SetText("") + fs:Hide() + end + end + + -- Show overflow indicator + if count > MAX_DISPLAY_ERRORS then + local lastFs = errorFontStrings[MAX_DISPLAY_ERRORS] + lastFs:SetText("|cff888888... and " .. (count - MAX_DISPLAY_ERRORS + 1) .. " more|r") + lastFs:Show() + end + + -- Dynamic height: header(16) + padding(8+6) + lines(12 each) + local linesShown = displayed + if linesShown > MAX_DISPLAY_ERRORS then linesShown = MAX_DISPLAY_ERRORS end + local height = 8 + 16 + 2 + (linesShown * 12) + 6 + if height > MAX_PANEL_HEIGHT then height = MAX_PANEL_HEIGHT end + errorPanel:SetHeight(height) + errorPanel:Show() +end + +-- ============================================================================ +-- Line Highlights (Option B) +-- ============================================================================ + +local function GetLineHeight() + if not MacroFrameText then return 13 end + local _, fontSize = MacroFrameText:GetFont() + -- Use raw font size - this matches the EditBox's actual line spacing + return fontSize or 13 +end + +local function GetTopTextInset() + if not MacroFrameText then return 0 end + -- GetTextInsets returns left, right, top, bottom padding inside the EditBox + local success, l, r, t, b = pcall(MacroFrameText.GetTextInsets, MacroFrameText) + if success and t then + return t + end + return 0 +end + +local function EnsureHighlightPool() + if table.getn(lineHighlights) >= HIGHLIGHT_POOL_SIZE then return end + + for i = table.getn(lineHighlights) + 1, HIGHLIGHT_POOL_SIZE do + local tex = MacroFrameText:CreateTexture(nil, "BACKGROUND") + tex:SetTexture(0.6, 0.08, 0.08, 0.25) + tex:Hide() + lineHighlights[i] = tex + end +end + +local function UpdateLineHighlights(errors) + if not MacroFrameText then return end + + EnsureHighlightPool() + + -- Collect which lines have errors (deduplicate) + local errorLines = {} + if errors then + for _, err in ipairs(errors) do + if err.line then + errorLines[err.line] = true + end + end + end + + local lineHeight = GetLineHeight() + local topInset = GetTopTextInset() + local editWidth = MacroFrameText:GetWidth() + if editWidth < 10 then editWidth = 260 end -- fallback + + local highlightIdx = 1 + for lineNum, _ in pairs(errorLines) do + if highlightIdx > HIGHLIGHT_POOL_SIZE then break end + + local tex = lineHighlights[highlightIdx] + -- Position: top text inset + (lineNum-1) lines down + local yOffset = -topInset - ((lineNum - 1) * lineHeight) + + tex:ClearAllPoints() + tex:SetPoint("TOPLEFT", MacroFrameText, "TOPLEFT", -2, yOffset) + tex:SetWidth(editWidth + 4) + tex:SetHeight(lineHeight) + tex:Show() + + highlightIdx = highlightIdx + 1 + end + + -- Hide unused highlights + for i = highlightIdx, HIGHLIGHT_POOL_SIZE do + if lineHighlights[i] then + lineHighlights[i]:Hide() + end + end +end + +-- ============================================================================ +-- Name Highlight (yellow backdrop on macro name) +-- ============================================================================ + +local function CreateNameHighlight() + if nameHighlight then return end + -- Find the macro name display element + -- Standard Blizzard_MacroUI uses MacroFrameSelectedMacroName (FontString) + -- and MacroFrameSelectedMacroButton (icon) + local nameFrame = getglobal("MacroFrameSelectedMacroName") + if not nameFrame then return end + + -- FontStrings can't own textures, so create on their parent + local parent = nameFrame:GetParent() or MacroFrame + local tex = parent:CreateTexture(nil, "BACKGROUND") + tex:SetTexture(0.7, 0.6, 0.1, 0.3) + tex:SetPoint("TOPLEFT", nameFrame, "TOPLEFT", -3, 3) + tex:SetPoint("BOTTOMRIGHT", nameFrame, "BOTTOMRIGHT", 3, -3) + tex:Hide() + nameHighlight = tex +end + +local function UpdateNameHighlight(hasNameErrors) + if not nameHighlight then + CreateNameHighlight() + end + if nameHighlight then + if hasNameErrors then + nameHighlight:Show() + else + nameHighlight:Hide() + end + end +end + +-- ============================================================================ +-- Validation & Debounce +-- ============================================================================ + +-- Build a set of player spell names (lowercase) for name conflict checks +local function GetPlayerSpellNames() + local spellNames = {} + -- Primary source: CleveRoids.Spells indexed table + if CleveRoids.Spells then + for bookType, spells in CleveRoids.Spells do + if type(spells) == "table" then + for spellName, _ in pairs(spells) do + if type(spellName) == "string" then + spellNames[string.lower(spellName)] = true + end + end + end + end + end + -- Fallback: iterate spellbook directly + if not next(spellNames) then + local i = 1 + while true do + local name, rank = GetSpellName(i, "spell") + if not name then break end + spellNames[string.lower(name)] = true + i = i + 1 + end + end + return spellNames +end + +-- Build a set of player item names (lowercase) for name conflict checks +local function GetPlayerItemNames() + local itemNames = {} + if CleveRoids.Items then + for key, value in pairs(CleveRoids.Items) do + if type(key) == "string" and type(value) == "table" and value.name then + itemNames[string.lower(value.name)] = true + end + end + end + return itemNames +end + +-- Validate the current macro's name and return any name errors +local function ValidateMacroName() + local nameErrors = {} + if not MacroFrame or not MacroFrame.selectedMacro then return nameErrors end + + local selectedSlot = MacroFrame.selectedMacro + local nameOk, name = pcall(GetMacroInfo, selectedSlot) + if not nameOk or not name then return nameErrors end + + -- Blank/whitespace name + local trimmedName = CleveRoids.Trim and CleveRoids.Trim(name) or name + if trimmedName == "" then + table.insert(nameErrors, { + type = "NAME_ERROR", + message = "Macro name is blank or only spaces" + }) + end + + -- Spell conflict + local lowerName = string.lower(name) + local spellNames = GetPlayerSpellNames() + if spellNames[lowerName] then + table.insert(nameErrors, { + type = "NAME_ERROR", + message = "Name '" .. name .. "' conflicts with a spell/ability" + }) + end + + -- Item conflict + local itemNames = GetPlayerItemNames() + if itemNames[lowerName] then + table.insert(nameErrors, { + type = "NAME_ERROR", + message = "Name '" .. name .. "' conflicts with an item" + }) + end + + -- Duplicate name check + local dupeCount = 0 + for i = 1, 36 do + local ok, otherName = pcall(GetMacroInfo, i) + if ok and otherName and string.lower(otherName) == lowerName then + dupeCount = dupeCount + 1 + end + end + if dupeCount > 1 then + table.insert(nameErrors, { + type = "NAME_ERROR", + message = "Duplicate name '" .. name .. "' (used " .. dupeCount .. " times)" + }) + end + + return nameErrors +end + +local function RunValidation() + if not MacroFrameText then return end + + local bodyText = MacroFrameText:GetText() + if not bodyText or bodyText == "" then + currentErrors = nil + UpdateErrorPanel(nil) + UpdateLineHighlights(nil) + UpdateNameHighlight(false) + return + end + + -- Combine name errors + body errors + local errors = {} + + local nameErrors = ValidateMacroName() + local hasNameErrors = table.getn(nameErrors) > 0 + for _, err in ipairs(nameErrors) do + table.insert(errors, err) + end + + local bodyErrors = CleveRoids.ValidateMacroBody(bodyText) + if bodyErrors then + for _, err in ipairs(bodyErrors) do + table.insert(errors, err) + end + end + + if table.getn(errors) == 0 then errors = nil end + currentErrors = errors + + UpdateErrorPanel(errors) + UpdateLineHighlights(errors) + UpdateNameHighlight(hasNameErrors) +end + +local function RequestValidation() + lastKeystroke = GetTime() + pendingValidation = true +end + +local function OnUpdateTick() + if not MacroFrame or not MacroFrame:IsVisible() then return end + + -- Debounced keystroke validation + if pendingValidation and (GetTime() - lastKeystroke) >= DEBOUNCE_DELAY then + pendingValidation = false + RunValidation() + end + + -- Detect macro selection change (poll-based, safer than hooking unknown functions) + if MacroFrame.selectedMacro ~= lastSelectedMacro then + lastSelectedMacro = MacroFrame.selectedMacro + -- Immediate validation on selection change + pendingValidation = false + RunValidation() + end +end + +-- ============================================================================ +-- Cleanup +-- ============================================================================ + +-- Report all macro errors to chat on frame close +local function ReportAllMacroErrors() + local accountEntries = {} -- { {name=, slot=, errors=}, ... } + local characterEntries = {} + local nameCount = {} -- lowercase name -> count (for duplicate detection) + local nameSlots = {} -- lowercase name -> { slot1, slot2, ... } + local spellNames = GetPlayerSpellNames() + local itemNames = GetPlayerItemNames() + + -- First pass: collect all macro names and body errors + for i = 1, 36 do + local nameOk, name = pcall(GetMacroInfo, i) + if nameOk and name and name ~= "" then + local errors = {} + + -- Validate body + local _, _, body = GetMacroInfo(i) + if body and body ~= "" then + local bodyErrors = CleveRoids.ValidateMacroBody(body) + if bodyErrors then + for _, err in ipairs(bodyErrors) do + table.insert(errors, err) + end + end + end + + -- Check blank/whitespace name + local trimmedName = CleveRoids.Trim and CleveRoids.Trim(name) or name + if trimmedName == "" then + table.insert(errors, { + type = "NAME_ERROR", + message = "Macro name is blank or only spaces" + }) + end + + -- Check if name matches a player spell + local lowerNameCheck = string.lower(name) + if spellNames[lowerNameCheck] then + table.insert(errors, { + type = "NAME_ERROR", + message = "Name '" .. name .. "' conflicts with a known spell/ability" + }) + end + + -- Check if name matches a player item + if itemNames[lowerNameCheck] then + table.insert(errors, { + type = "NAME_ERROR", + message = "Name '" .. name .. "' conflicts with an inventory item" + }) + end + + -- Track name for duplicate detection + local lowerName = string.lower(name) + nameCount[lowerName] = (nameCount[lowerName] or 0) + 1 + if not nameSlots[lowerName] then nameSlots[lowerName] = {} end + table.insert(nameSlots[lowerName], i) + + local entry = { name = name, slot = i, errors = errors } + if i <= 18 then + table.insert(accountEntries, entry) + else + table.insert(characterEntries, entry) + end + end + end + + -- Second pass: inject duplicate name errors + for lowerName, count in pairs(nameCount) do + if count > 1 then + local slots = nameSlots[lowerName] + for _, slot in ipairs(slots) do + -- Find the entry for this slot and add the error + local list = slot <= 18 and accountEntries or characterEntries + for _, entry in ipairs(list) do + if entry.slot == slot then + table.insert(entry.errors, 1, { + type = "NAME_ERROR", + message = "Duplicate name '" .. entry.name .. "' (used " .. count .. " times)" + }) + break + end + end + end + end + end + + -- Output: filter to only entries with errors + local accountErrors = {} + local characterErrors = {} + for _, entry in ipairs(accountEntries) do + if table.getn(entry.errors) > 0 then + table.insert(accountErrors, entry) + end + end + for _, entry in ipairs(characterEntries) do + if table.getn(entry.errors) > 0 then + table.insert(characterErrors, entry) + end + end + + local totalMacros = table.getn(accountErrors) + table.getn(characterErrors) + if totalMacros == 0 then return end + + DEFAULT_CHAT_FRAME:AddMessage("|cffff6060[MacroErrorChecker]|r Found errors in " .. totalMacros .. " macro(s):", 1, 0.8, 0.4) + + local function PrintSection(label, entries) + if table.getn(entries) == 0 then return end + DEFAULT_CHAT_FRAME:AddMessage(" |cff88aaff--- " .. label .. " ---|r") + for _, entry in ipairs(entries) do + local count = table.getn(entry.errors) + DEFAULT_CHAT_FRAME:AddMessage(" |cffffffff" .. entry.name .. "|r - " .. count .. " error(s)") + for _, err in ipairs(entry.errors) do + local linePrefix = err.line and ("L" .. err.line .. ": ") or "" + local msg = linePrefix .. (err.message or "Unknown error") + if string.len(msg) > 90 then + msg = string.sub(msg, 1, 87) .. "..." + end + -- Name errors in yellow, syntax errors in red + local color = err.type == "NAME_ERROR" and "|cffffcc60" or "|cffffa0a0" + DEFAULT_CHAT_FRAME:AddMessage(" " .. color .. msg .. "|r") + end + end + end + + PrintSection("General Macros", accountErrors) + PrintSection("Character Macros", characterErrors) +end + +local function ClearAll() + currentErrors = nil + pendingValidation = false + lastSelectedMacro = nil + + if errorPanel then + errorPanel:Hide() + end + + for i = 1, MAX_DISPLAY_ERRORS do + if errorFontStrings[i] then + errorFontStrings[i]:SetText("") + errorFontStrings[i]:Hide() + end + end + + for i = 1, table.getn(lineHighlights) do + if lineHighlights[i] then + lineHighlights[i]:Hide() + end + end + + if nameHighlight then + nameHighlight:Hide() + end +end + +-- ============================================================================ +-- Hook Installation +-- ============================================================================ + +local function InstallHooks() + if hooked then return end + if not MacroFrameText or not MacroFrame then return end + + -- Skip if SuperMacro is active (it replaces the macro editor entirely) + if SuperMacroFrame ~= nil then return end + + -- OnTextChanged: trigger debounced validation on every keystroke + local origOnTextChanged = MacroFrameText:GetScript("OnTextChanged") + MacroFrameText:SetScript("OnTextChanged", function() + if origOnTextChanged then + origOnTextChanged() + end + RequestValidation() + end) + + -- OnShow: validate immediately when macro frame opens + local origOnShow = MacroFrame:GetScript("OnShow") + MacroFrame:SetScript("OnShow", function() + if origOnShow then + origOnShow() + end + -- Create panel lazily on first show + CreateErrorPanel() + -- Reset state and validate + lastSelectedMacro = MacroFrame.selectedMacro + RunValidation() + end) + + -- OnHide: clean up everything + local origOnHide = MacroFrame:GetScript("OnHide") + MacroFrame:SetScript("OnHide", function() + if origOnHide then + origOnHide() + end + ClearAll() + -- Report all macro errors to chat when closing the editor + pcall(ReportAllMacroErrors) + end) + + -- OnUpdate for debounce timer and selection change polling + updateFrame = CreateFrame("Frame") + updateFrame:SetScript("OnUpdate", function() + local success, err = pcall(OnUpdateTick) + if not success then + -- Silently fail - don't spam errors every frame + pendingValidation = false + end + end) + + hooked = true +end + +-- ============================================================================ +-- Extension Entry Points +-- ============================================================================ + +function Extension.OnAddonLoaded() + if arg1 == "Blizzard_MacroUI" then + InstallHooks() + end +end + +function Extension.OnLoad() + -- Skip if SuperMacro is loaded (detected at load time) + if SuperMacroFrame ~= nil then return end + + -- Listen for macro UI loading + Extension.RegisterEvent("ADDON_LOADED", "OnAddonLoaded") + + -- If MacroFrame already exists (unlikely but safe), hook immediately + if MacroFrame and MacroFrameText then + InstallHooks() + end +end + +_G["CleveRoids"] = CleveRoids diff --git a/MacroErrorChecker.lua b/MacroErrorChecker.lua index 50e1396..ed14097 100644 --- a/MacroErrorChecker.lua +++ b/MacroErrorChecker.lua @@ -393,6 +393,60 @@ local function validateLine(line, lineNum) }) end + -- Check for semicolons inside brackets (must check full line before semicolon split) + -- e.g., [nomybuff;battleshout] is wrong - semicolons separate actions, not conditionals + if bracketsOk then + local depth = 0 + local inQuotes = false + local lineLen = safeStringLen(line) + for i = 1, lineLen do + local ch = safeStringSub(line, i, i) + if ch == '"' then + inQuotes = not inQuotes + elseif not inQuotes then + if ch == "[" then + depth = depth + 1 + elseif ch == "]" then + depth = depth - 1 + elseif ch == ";" and depth > 0 then + table.insert(localErrors, { + type = ERROR_TYPES.INVALID_SYNTAX, + line = lineNum, + message = "';' inside brackets is invalid - use spaces to separate conditionals, ':' for arguments" + }) + break + end + end + end + end + + -- Check for missing semicolons between bracket groups + -- e.g., /cast [cond]Backstab[cond2]Garrote should use ; between actions + if bracketsOk then + local pos = 1 + while true do + local closePos = safeStringFind(line, "%]", pos) + if not closePos then break end + + local nextOpenPos = safeStringFind(line, "%[", closePos + 1) + if not nextOpenPos then break end + + local between = safeStringSub(line, closePos + 1, nextOpenPos - 1) + if not safeStringFind(between, ";") then + local trimmed = safeTrim(between) + if trimmed ~= "" then + table.insert(localErrors, { + type = ERROR_TYPES.INVALID_SYNTAX, + line = lineNum, + message = "Missing ';' before '[' - use '" .. trimmed .. ";' to separate actions" + }) + end + end + + pos = nextOpenPos + 1 + end + end + -- Split by semicolons to handle multiple actions per line local actions = CleveRoids.splitStringIgnoringQuotes(line, ";") if not actions then @@ -465,7 +519,21 @@ local function validateLine(line, lineNum) local cond = string.lower(safeTrim(parts[1] or "")) local args = safeTrim(parts[2] or "") - if cond ~= "" then + -- Check for missing ':' between conditional and its argument + -- e.g., "combo>0" should be "combo:>0", "hp50" should be "hp:50" + if cond ~= "" and (not parts[2] or args == "") and not safeStringFind(cond, "^@") then + local _, _, condPrefix, valueSuffix = safeStringFind(cond, "^([a-z]+)([<>=~%d].+)$") + if condPrefix and valueSuffix and VALID_CONDITIONALS[condPrefix] then + table.insert(localErrors, { + type = ERROR_TYPES.INVALID_SYNTAX, + line = lineNum, + message = "Missing ':' after " .. condPrefix .. " (use " .. condPrefix .. ":" .. valueSuffix .. ")" + }) + cond = nil -- Skip further validation, we identified the issue + end + end + + if cond and cond ~= "" then -- Validate the conditional local condErrors = validateConditional(cond, args, nil) for _, err in condErrors do @@ -584,6 +652,44 @@ function CleveRoids.ValidateMacro(macroName) end end +-- Validate raw macro body text (for live editing in the macro frame) +-- bodyText: The raw text from the EditBox (not yet saved) +-- Returns: Array of error tables with .type, .line, .message fields +function CleveRoids.ValidateMacroBody(bodyText) + local success, result = pcall(function() + local errors = {} + + if not bodyText or bodyText == "" then + return errors + end + + local lines = CleveRoids.splitString(bodyText, "\n") + if not lines then + return errors + end + + for lineNum, line in ipairs(lines) do + local lineErrors = validateLine(line, lineNum) + if lineErrors then + for _, err in lineErrors do + table.insert(errors, err) + end + end + end + + return errors + end) + + if success then + return result + else + return {{ + type = "CRITICAL_ERROR", + message = "Critical error validating macro body: " .. tostring(result) + }} + end +end + -- Validate all macros function CleveRoids.ValidateAllMacros() local results = {} diff --git a/SuperCleveRoidMacros.toc b/SuperCleveRoidMacros.toc index 1b9dcce..34c01bd 100644 --- a/SuperCleveRoidMacros.toc +++ b/SuperCleveRoidMacros.toc @@ -22,6 +22,7 @@ Compatibility\Bongos.lua Compatibility\UltimaMacros.lua Extensions\MacroLengthWarn.lua +Extensions\MacroErrorUI.lua Extensions\Mouseover\GameTooltip.lua Extensions\Mouseover\Blizzard.lua Extensions\Mouseover\CT_RaidAssist.lua From c001c89e0c4a3ab024c8c324ac5c4b5f22805310 Mon Sep 17 00:00:00 2001 From: Jrc13245 Date: Mon, 9 Feb 2026 14:45:30 -0500 Subject: [PATCH 9/9] fix finding same name items to equip in different slots when you have more than 1 --- Core.lua | 91 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/Core.lua b/Core.lua index 4b7916f..09876f9 100644 --- a/Core.lua +++ b/Core.lua @@ -3068,6 +3068,27 @@ function CleveRoids.DoUse(msg) return false end +-- Find an item in bags by name (ignoring equipped items) +-- Used to prefer bag copies over swapping from paired equipped slots +-- (e.g., dual-wielding the same weapon with /equipmh + /equipoh) +local function FindItemInBagsByName(itemName) + if not itemName then return nil, nil end + local lowerName = string_lower(itemName) + for bag = 0, 4 do + local numSlots = GetContainerNumSlots(bag) or 0 + for slot = 1, numSlots do + local link = GetContainerItemLink(bag, slot) + if link then + local _, _, name = string_find(link, "|h%[(.-)%]|h") + if name and string_lower(name) == lowerName then + return bag, slot + end + end + end + end + return nil, nil +end + function CleveRoids.EquipBagItem(msg, slotOrOffhand) if CleveRoids.equipDebugLog then CleveRoids.Print("|cff00ffff[EquipLog] EquipBagItem called: '" .. tostring(msg) .. "' slot=" .. tostring(slotOrOffhand) .. "|r") @@ -3122,7 +3143,22 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand) if itemInfo.inventoryID == invslot then return true -- Already in correct slot end - -- Pick up from current slot and equip to target + -- Before swapping from another equipped slot, check bags for another copy + -- This handles dual-wielding the same weapon (e.g., /equipmh Scimitar + /equipoh Scimitar) + local bagCopyBag, bagCopySlot = FindItemInBagsByName(itemInfo.name or searchTerm) + if bagCopyBag then + ClearCursor() + PickupContainerItem(bagCopyBag, bagCopySlot) + if CursorHasItem and CursorHasItem() then + EquipCursorItem(invslot) + ClearCursor() + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cff00ff00[EquipLog] Equipped bag copy of '" .. tostring(msg) .. "' from bag " .. bagCopyBag .. " slot " .. bagCopySlot .. " (preferred over swap)|r") + end + return true + end + end + -- No bag copy found - swap from equipped slot ClearCursor() PickupInventoryItem(itemInfo.inventoryID) if CursorHasItem and CursorHasItem() then @@ -3202,6 +3238,7 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand) -- Check if item is already equipped in the paired slot (swap case) -- EquipItemByName doesn't handle swapping equipped items, so we must do it manually -- Paired slots: trinkets (13<->14), weapons (16<->17), rings (11<->12) + -- BUT: check bags first for another copy (e.g., dual-wielding same weapon) local pairedSlots = {[13] = 14, [14] = 13, [16] = 17, [17] = 16, [11] = 12, [12] = 11} local checkSlot = pairedSlots[invslot] if checkSlot then @@ -3209,23 +3246,45 @@ function CleveRoids.EquipBagItem(msg, slotOrOffhand) if link then local _, _, slotItemName = string_find(link, "|h%[(.-)%]|h") if slotItemName and string_lower(slotItemName) == string_lower(msg) then - -- Found item in paired slot - swap it manually - if CleveRoids.equipDebugLog then - CleveRoids.Print("|cff00ffff[EquipLog] Swapping from slot " .. checkSlot .. " to slot " .. invslot .. "|r") - end - ClearCursor() - PickupInventoryItem(checkSlot) - if CursorHasItem and CursorHasItem() then - EquipCursorItem(invslot) - ClearCursor() - if CleveRoids.Items then - CleveRoids.Items[msg] = nil - CleveRoids.Items[string_lower(msg)] = nil + -- Item found in paired slot - but prefer a bag copy if one exists + local bagCopyBag, bagCopySlot = FindItemInBagsByName(msg) + if bagCopyBag then + -- Bag copy available - equip from bag instead of swapping + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cff00ffff[EquipLog] Found bag copy, equipping from bag " .. bagCopyBag .. " slot " .. bagCopySlot .. " instead of swapping from slot " .. checkSlot .. "|r") end - InvalidateDisplacedItem() - return true + ClearCursor() + PickupContainerItem(bagCopyBag, bagCopySlot) + if CursorHasItem and CursorHasItem() then + EquipCursorItem(invslot) + ClearCursor() + if CleveRoids.Items then + CleveRoids.Items[msg] = nil + CleveRoids.Items[string_lower(msg)] = nil + end + InvalidateDisplacedItem() + return true + end + ClearCursor() + else + -- No bag copy - swap from paired slot + if CleveRoids.equipDebugLog then + CleveRoids.Print("|cff00ffff[EquipLog] Swapping from slot " .. checkSlot .. " to slot " .. invslot .. "|r") + end + ClearCursor() + PickupInventoryItem(checkSlot) + if CursorHasItem and CursorHasItem() then + EquipCursorItem(invslot) + ClearCursor() + if CleveRoids.Items then + CleveRoids.Items[msg] = nil + CleveRoids.Items[string_lower(msg)] = nil + end + InvalidateDisplacedItem() + return true + end + ClearCursor() end - ClearCursor() end end end