using System.Text; using System.Text.Json; using System.Text.RegularExpressions; namespace anjianjingling.Lua { public enum ArgKind { /// 数字,原样输出。 Number, /// 字符串,生成时自动加引号转义。 Text, /// 键名 / 选择项,加引号。 Key, /// 表达式,原样输出。 Expr, /// 原样文本(变量名、注释),不加引号。 Raw } public enum BlockShape { /// 单行命令。 Simple, /// if <条件> then … end(可切换 else 分支)。 If, /// 如果屏幕上存在图片 … then … end。 IfExists, /// for i = 1, N do … end。 LoopN, /// while <条件> do … end。 LoopWhile, /// break。 Break } public sealed record BlockArg(string Name, string Label, ArgKind Kind, string Default, string[]? Options = null); public sealed class BlockDef { public required string Key { get; init; } public required string Category { get; init; } /// 显示标题,含 {0} {1} 参数占位。 public required string Title { get; init; } /// Lua 模板,含 {0} {1} 参数占位;容器块为起始行。 public string Template { get; init; } = ""; public BlockShape Shape { get; init; } = BlockShape.Simple; public BlockArg[] Args { get; init; } = Array.Empty(); public string Help { get; init; } = ""; public bool IsContainer => Shape is BlockShape.If or BlockShape.IfExists or BlockShape.LoopN or BlockShape.LoopWhile; } /// 画布上的一个积木实例。 public sealed class LuaBlock { public string Key { get; set; } = ""; public List Values { get; set; } = new(); /// 容器块的子块。 public List Children { get; set; } = new(); /// If 块是否有 else 分支。 public bool HasElse { get; set; } public List ElseChildren { get; set; } = new(); public LuaBlock Clone() { var b = new LuaBlock { Key = Key, HasElse = HasElse, Values = new List(Values) }; foreach (var c in Children) b.Children.Add(c.Clone()); foreach (var c in ElseChildren) b.ElseChildren.Add(c.Clone()); return b; } public string Value(int i) { var def = BlockCatalog.Find(Key); if (def == null || i >= def.Args.Length) return ""; return i < Values.Count && !string.IsNullOrEmpty(Values[i]) ? Values[i] : def.Args[i].Default; } } /// 积木目录:定义表 + Lua 生成 + JSON 读写。 public static class BlockCatalog { public static readonly List All = new(); private static readonly Dictionary ByKey = new(); private static void Add(BlockDef d) { All.Add(d); ByKey[d.Key] = d; } public static BlockDef? Find(string key) => ByKey.TryGetValue(key, out var d) ? d : null; public static IEnumerable Categories => All.Select(d => d.Category).Distinct(); static BlockCatalog() { // ==================== 流程 ==================== Add(new() { Key = "delay", Category = "流程", Title = "等待 {0} 毫秒", Template = "delay({0})", Args = new[] { new BlockArg("ms", "毫秒", ArgKind.Number, "500") }, Help = "暂停指定时间" }); Add(new() { Key = "delayRandom", Category = "流程", Title = "随机等待 {0} ~ {1} 毫秒", Template = "delayRandom({0}, {1})", Args = new[] { new BlockArg("min", "最小", ArgKind.Number, "300"), new BlockArg("max", "最大", ArgKind.Number, "800") } }); Add(new() { Key = "loopN", Category = "流程", Title = "循环 {0} 次", Shape = BlockShape.LoopN, Args = new[] { new BlockArg("n", "次数", ArgKind.Number, "10") } }); Add(new() { Key = "loopWhile", Category = "流程", Title = "当 {0} 时循环", Shape = BlockShape.LoopWhile, Args = new[] { new BlockArg("cond", "条件", ArgKind.Expr, "i < 10") } }); Add(new() { Key = "loopForever", Category = "流程", Title = "无限循环", Shape = BlockShape.LoopWhile }); Add(new() { Key = "if", Category = "流程", Title = "如果 {0}", Shape = BlockShape.If, Args = new[] { new BlockArg("cond", "条件", ArgKind.Expr, "x >= 0") } }); Add(new() { Key = "break", Category = "流程", Title = "跳出循环", Shape = BlockShape.Break }); Add(new() { Key = "varSet", Category = "流程", Title = "设置变量 {0} = {1}", Template = "{0} = {1}", Args = new[] { new BlockArg("name", "变量名", ArgKind.Raw, "x"), new BlockArg("val", "值", ArgKind.Expr, "0") } }); Add(new() { Key = "varAdd", Category = "流程", Title = "变量 {0} 增加 {1}", Template = "{0} = {0} + {1}", Args = new[] { new BlockArg("name", "变量名", ArgKind.Raw, "x"), new BlockArg("val", "增量", ArgKind.Number, "1") } }); Add(new() { Key = "comment", Category = "流程", Title = "注释:{0}", Template = "-- {0}", Args = new[] { new BlockArg("text", "内容", ArgKind.Raw, "说明") } }); // Lua 里无法识别的行,原样保留(脚本 ⇄ 积木互转时不丢代码) Add(new() { Key = "lua", Category = "流程", Title = "自定义代码 {0}", Template = "{0}", Args = new[] { new BlockArg("code", "代码", ArgKind.Raw, "-- 原生 Lua") }, Help = "脚本转换时无法识别的语句会变成此块,原样执行" }); // ==================== 鼠标 ==================== Add(new() { Key = "moveTo", Category = "鼠标", Title = "移动鼠标到 ({0}, {1})", Template = "moveTo({0}, {1})", Args = new[] { new BlockArg("x", "X", ArgKind.Number, "500"), new BlockArg("y", "Y", ArgKind.Number, "300") } }); Add(new() { Key = "moveR", Category = "鼠标", Title = "相对移动 ({0}, {1})", Template = "moveR({0}, {1})", Args = new[] { new BlockArg("dx", "X 偏移", ArgKind.Number, "10"), new BlockArg("dy", "Y 偏移", ArgKind.Number, "10") } }); Add(new() { Key = "leftClickXY", Category = "鼠标", Title = "左键点击 ({0}, {1})", Template = "leftClick({0}, {1})", Args = new[] { new BlockArg("x", "X", ArgKind.Number, "500"), new BlockArg("y", "Y", ArgKind.Number, "300") } }); Add(new() { Key = "leftClick", Category = "鼠标", Title = "左键点击(当前位置)", Template = "leftClick()" }); Add(new() { Key = "rightClickXY", Category = "鼠标", Title = "右键点击 ({0}, {1})", Template = "rightClick({0}, {1})", Args = new[] { new BlockArg("x", "X", ArgKind.Number, "500"), new BlockArg("y", "Y", ArgKind.Number, "300") } }); Add(new() { Key = "rightClick", Category = "鼠标", Title = "右键点击(当前位置)", Template = "rightClick()" }); Add(new() { Key = "middleClick", Category = "鼠标", Title = "中键点击(当前位置)", Template = "middleClick()" }); Add(new() { Key = "doubleClick", Category = "鼠标", Title = "双击 ({0}, {1})", Template = "doubleClick({0}, {1})", Args = new[] { new BlockArg("x", "X", ArgKind.Number, "500"), new BlockArg("y", "Y", ArgKind.Number, "300") } }); Add(new() { Key = "mouseDown", Category = "鼠标", Title = "按下鼠标 {0}", Template = "mouse.down({0})", Args = new[] { new BlockArg("btn", "按键", ArgKind.Key, "left", new[] { "left", "right", "middle" }) } }); Add(new() { Key = "mouseUp", Category = "鼠标", Title = "弹起鼠标 {0}", Template = "mouse.up({0})", Args = new[] { new BlockArg("btn", "按键", ArgKind.Key, "left", new[] { "left", "right", "middle" }) } }); Add(new() { Key = "wheel", Category = "鼠标", Title = "滚轮 {0} 格(正上负下)", Template = "wheel({0})", Args = new[] { new BlockArg("n", "格数", ArgKind.Number, "-1") } }); Add(new() { Key = "drag", Category = "鼠标", Title = "拖拽 ({0},{1}) → ({2},{3}) 用时 {4} 毫秒", Template = "drag({0}, {1}, {2}, {3}, {4})", Args = new[] { new BlockArg("x1", "起点X", ArgKind.Number, "100"), new BlockArg("y1", "起点Y", ArgKind.Number, "100"), new BlockArg("x2", "终点X", ArgKind.Number, "600"), new BlockArg("y2", "终点Y", ArgKind.Number, "400"), new BlockArg("ms", "时长", ArgKind.Number, "500") } }); // ==================== 键盘 ==================== Add(new() { Key = "keyPress", Category = "键盘", Title = "按键 {0}", Template = "keyPress({0})", Args = new[] { new BlockArg("k", "键名", ArgKind.Key, "ENTER") }, Help = "ENTER / SPACE / TAB / ESC / F1~F12 / A~Z / LEFT / UP …" }); Add(new() { Key = "hotkey", Category = "键盘", Title = "组合键 {0}", Template = "hotkey({0})", Args = new[] { new BlockArg("k", "组合", ArgKind.Key, "Ctrl+A") } }); Add(new() { Key = "typeText", Category = "键盘", Title = "输入文本 {0}", Template = "typeText({0})", Args = new[] { new BlockArg("t", "文本", ArgKind.Text, "你好,世界") }, Help = "支持中文,使用 Unicode 注入,不受输入法影响" }); Add(new() { Key = "keyDown", Category = "键盘", Title = "按下键 {0}", Template = "keyDown({0})", Args = new[] { new BlockArg("k", "键名", ArgKind.Key, "SHIFT") } }); Add(new() { Key = "keyUp", Category = "键盘", Title = "弹起键 {0}", Template = "keyUp({0})", Args = new[] { new BlockArg("k", "键名", ArgKind.Key, "SHIFT") } }); // ==================== 视觉 ==================== Add(new() { Key = "findImage", Category = "视觉", Title = "找图 {0},相似度 {1} → 变量 {2}, {3}", Template = "{2}, {3} = findImage({0}, {1})", Args = new[] { new BlockArg("path", "图片", ArgKind.Text, "images/btn.png"), new BlockArg("sim", "相似度", ArgKind.Number, "0.85"), new BlockArg("vx", "X 变量", ArgKind.Raw, "x"), new BlockArg("vy", "Y 变量", ArgKind.Raw, "y") }, Help = "找不到时变量为 -1;模板图片放程序目录 images/ 下" }); Add(new() { Key = "waitImage", Category = "视觉", Title = "等待图片 {0},超时 {1} 毫秒 → 变量 {2}, {3}", Template = "{2}, {3} = waitImage({0}, {1})", Args = new[] { new BlockArg("path", "图片", ArgKind.Text, "images/btn.png"), new BlockArg("ms", "超时", ArgKind.Number, "10000"), new BlockArg("vx", "X 变量", ArgKind.Raw, "x"), new BlockArg("vy", "Y 变量", ArgKind.Raw, "y") } }); Add(new() { Key = "findColor", Category = "视觉", Title = "找色 {0},容差 {1} → 变量 {2}, {3}", Template = "{2}, {3} = findColor({0}, {1})", Args = new[] { new BlockArg("hex", "颜色", ArgKind.Text, "FF0000"), new BlockArg("tol", "容差", ArgKind.Number, "10"), new BlockArg("vx", "X 变量", ArgKind.Raw, "x"), new BlockArg("vy", "Y 变量", ArgKind.Raw, "y") } }); Add(new() { Key = "ifExists", Category = "视觉", Title = "如果屏幕上有图片 {0}(相似度 {1})", Shape = BlockShape.IfExists, Args = new[] { new BlockArg("path", "图片", ArgKind.Text, "images/btn.png"), new BlockArg("sim", "相似度", ArgKind.Number, "0.85") } }); Add(new() { Key = "screenshot", Category = "视觉", Title = "截图保存到 {0}", Template = "screenshot({0})", Args = new[] { new BlockArg("path", "路径", ArgKind.Text, "shot.png") } }); Add(new() { Key = "pickRegion", Category = "视觉", Title = "框选截图存为 {0}", Template = "screen.pick({0})", Args = new[] { new BlockArg("path", "路径", ArgKind.Text, "images/btn.png") }, Help = "运行时弹窗框选一块屏幕区域,直接存成模板图" }); // ==================== 系统 ==================== Add(new() { Key = "run", Category = "系统", Title = "启动程序 {0}", Template = "run({0})", Args = new[] { new BlockArg("path", "路径", ArgKind.Text, "notepad.exe") } }); Add(new() { Key = "activate", Category = "系统", Title = "激活窗口 {0}", Template = "activate({0})", Args = new[] { new BlockArg("title", "标题关键字", ArgKind.Text, "记事本") } }); Add(new() { Key = "log", Category = "系统", Title = "日志 {0}", Template = "log({0})", Args = new[] { new BlockArg("msg", "内容", ArgKind.Text, "提示信息") } }); Add(new() { Key = "clipSet", Category = "系统", Title = "复制到剪贴板 {0}", Template = "clipSet({0})", Args = new[] { new BlockArg("text", "内容", ArgKind.Text, "") } }); Add(new() { Key = "stop", Category = "系统", Title = "停止脚本", Template = "stop()" }); // ==================== 浏览器 ==================== Add(new() { Key = "browserOpen", Category = "浏览器", Title = "打开网页 {0}", Template = "browser.open({0})", Args = new[] { new BlockArg("url", "网址", ArgKind.Text, "https://www.baidu.com") } }); Add(new() { Key = "browserClick", Category = "浏览器", Title = "点击网页元素 {0}", Template = "browser.click({0})", Args = new[] { new BlockArg("sel", "选择器", ArgKind.Text, "#btn") } }); Add(new() { Key = "browserType", Category = "浏览器", Title = "网页元素 {0} 输入 {1}", Template = "browser.type({0}, {1})", Args = new[] { new BlockArg("sel", "选择器", ArgKind.Text, "#input"), new BlockArg("text", "文本", ArgKind.Text, "内容") } }); Add(new() { Key = "browserWait", Category = "浏览器", Title = "等待网页元素 {0},超时 {1} 毫秒", Template = "browser.wait({0}, {1})", Args = new[] { new BlockArg("sel", "选择器", ArgKind.Text, ".loaded"), new BlockArg("ms", "超时", ArgKind.Number, "10000") } }); Add(new() { Key = "browserText", Category = "浏览器", Title = "读取网页元素 {0} → 变量 {1}", Template = "{1} = browser.text({0})", Args = new[] { new BlockArg("sel", "选择器", ArgKind.Text, ".result"), new BlockArg("v", "变量", ArgKind.Raw, "text") } }); Add(new() { Key = "browserJs", Category = "浏览器", Title = "执行 JS {0}", Template = "browser.js({0})", Args = new[] { new BlockArg("js", "JS 代码", ArgKind.Text, "document.title") } }); Add(new() { Key = "browserShot", Category = "浏览器", Title = "网页截图 {0}", Template = "browser.shot({0})", Args = new[] { new BlockArg("path", "路径", ArgKind.Text, "web.png") } }); } // ==================== 标题渲染 ==================== public static string RenderTitle(LuaBlock b) { var def = Find(b.Key); if (def == null) return "未知积木:" + b.Key; var args = new object[def.Args.Length]; for (int i = 0; i < def.Args.Length; i++) { string v = b.Value(i); if (def.Args[i].Kind == ArgKind.Text) v = "\"" + v + "\""; args[i] = v; } try { return string.Format(def.Title, args); } catch { return def.Title; } } // ==================== Lua 生成 ==================== public static string ToLua(IEnumerable blocks) { var sb = new StringBuilder(); Emit(blocks, 0, sb); return sb.ToString(); } private static void Emit(IEnumerable blocks, int level, StringBuilder sb) { string pad = new(' ', level * 4); foreach (var b in blocks) { var def = Find(b.Key); if (def == null) continue; switch (def.Shape) { case BlockShape.If: sb.AppendLine($"{pad}if {ArgText(def, b, 0)} then"); Emit(b.Children, level + 1, sb); if (b.HasElse) { sb.AppendLine($"{pad}else"); Emit(b.ElseChildren, level + 1, sb); } sb.AppendLine($"{pad}end"); break; case BlockShape.IfExists: sb.AppendLine($"{pad}do"); sb.AppendLine($"{pad} local __ix, __iy = findImage({ArgText(def, b, 0)}, {ArgText(def, b, 1)})"); sb.AppendLine($"{pad} if __ix >= 0 then"); Emit(b.Children, level + 2, sb); if (b.HasElse) { sb.AppendLine($"{pad} else"); Emit(b.ElseChildren, level + 2, sb); } sb.AppendLine($"{pad} end"); sb.AppendLine($"{pad}end"); break; case BlockShape.LoopN: sb.AppendLine($"{pad}for i = 1, {ArgText(def, b, 0)} do"); Emit(b.Children, level + 1, sb); sb.AppendLine($"{pad}end"); break; case BlockShape.LoopWhile: string cond = def.Args.Length == 0 ? "true" : ArgText(def, b, 0); sb.AppendLine($"{pad}while {cond} do"); Emit(b.Children, level + 1, sb); sb.AppendLine($"{pad}end"); break; case BlockShape.Break: sb.AppendLine($"{pad}break"); break; default: if (string.IsNullOrEmpty(def.Template)) break; var vals = new object[def.Args.Length]; for (int i = 0; i < def.Args.Length; i++) vals[i] = ArgText(def, b, i); string line; try { line = string.Format(def.Template, vals); } catch { line = def.Template; } sb.AppendLine(pad + line); break; } } } private static string ArgText(BlockDef def, LuaBlock b, int i) { if (i >= def.Args.Length) return ""; var arg = def.Args[i]; string v = b.Value(i); return arg.Kind switch { ArgKind.Number => string.IsNullOrWhiteSpace(v) ? "0" : v.Trim(), ArgKind.Expr or ArgKind.Raw => v, _ => Quote(v) }; } private static string Quote(string s) => "\"" + (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"") .Replace("\r", "").Replace("\n", "\\n") + "\""; // ==================== Lua → 积木(双向转换) ==================== private sealed class Frame { public LuaBlock Block = null!; public List ParentList = null!; public int Ends; // 还差几个 end 才结束此块 } private sealed class TplInfo { public BlockDef Def = null!; public string Call = ""; public int RetCount; public List ArgIdx = new(); } private static List? _tplCache; private static List TplCache => _tplCache ??= BuildTplCache(); private static List BuildTplCache() { var list = new List(); foreach (var def in All) { if (def.Shape != BlockShape.Simple || def.Template.Length == 0) continue; string callPart = def.Template, retPart = ""; int eq = def.Template.IndexOf(" = ", StringComparison.Ordinal); if (eq >= 0) { retPart = def.Template.Substring(0, eq); callPart = def.Template.Substring(eq + 3); } var m = Regex.Match(callPart, @"^([A-Za-z_][\w\.]*)\s*\((.*)\)$"); if (!m.Success) continue; // 左侧只允许纯占位符({2}, {3}),右侧只允许 "a, b" 形式的占位符列表 string retFixed = Regex.Replace(retPart, @"\{\d+\}", "").Trim().Replace(",", "").Replace(" ", ""); if (retPart.Length > 0 && retFixed.Length > 0) continue; string argFixed = Regex.Replace(m.Groups[2].Value, @"\{\d+\}", "").Trim().Replace(",", "").Replace(" ", ""); if (argFixed.Length > 0) continue; if (retPart.Length > 0 && !Regex.IsMatch(retPart, @"\{\d+\}")) continue; var info = new TplInfo { Def = def, Call = m.Groups[1].Value }; if (retPart.Length > 0) info.RetCount = Regex.Matches(retPart, @"\{\d+\}").Count; foreach (Match ph in Regex.Matches(m.Groups[2].Value, @"\{(\d+)\}")) info.ArgIdx.Add(int.Parse(ph.Groups[1].Value)); list.Add(info); } return list; } /// 把 Lua 脚本解析成积木。认识目录里的全部命令,认不出的行进「自定义代码」块。 public static List FromLua(string code) { var root = new List(); var stack = new Stack(); var cur = root; string[] lines = (code ?? "").Replace("\r\n", "\n").Split('\n'); for (int i = 0; i < lines.Length; i++) { string raw = lines[i].Trim(); if (raw.Length == 0) continue; if (raw.StartsWith("--")) { cur.Add(new LuaBlock { Key = "comment", Values = { raw.Substring(2).Trim() } }); continue; } string line = StripComment(raw).Trim(); if (line.Length == 0) continue; // ifExists 生成的三行结构:do / local __ix,__iy = findImage(…) / if __ix >= 0 then if (line == "do" && i + 2 < lines.Length) { string l1 = StripComment(lines[i + 1]).Trim(); string l2 = StripComment(lines[i + 2]).Trim(); var m1 = Regex.Match(l1, @"^local\s+__\w+\s*,\s*__\w+\s*=\s*findImage\s*\((.*)\)$"); if (m1.Success && Regex.IsMatch(l2, @"^if\s+__\w+\s*>=\s*0\s+then$")) { var b = new LuaBlock { Key = "ifExists" }; FillArgs(b, SplitArgs(m1.Groups[1].Value), new List { 0, 1 }); cur.Add(b); stack.Push(new Frame { Block = b, ParentList = cur, Ends = 2 }); cur = b.Children; i += 2; continue; } } if (line == "end") { if (stack.Count > 0) { stack.Peek().Ends--; while (stack.Count > 0 && stack.Peek().Ends <= 0) cur = stack.Pop().ParentList; } continue; } if (line == "else" || line.StartsWith("elseif ")) { bool handled = false; if (stack.Count > 0) { var top = stack.Peek(); if (top.Block.Key is "if" or "ifExists") { if (line == "else" && !top.Block.HasElse) { top.Block.HasElse = true; cur = top.Block.ElseChildren; handled = true; } else if (line.StartsWith("elseif ")) { // elseif → else 里嵌一个 if,共用原来的 end string cond = Regex.Match(line, @"^elseif\s+(.+?)\s+then$").Groups[1].Value; if (cond.Length > 0) { top.Ends--; top.Block.HasElse = true; var nested = new LuaBlock { Key = "if", Values = { cond.Trim() } }; cur.Add(nested); stack.Push(new Frame { Block = nested, ParentList = cur, Ends = 1 }); cur = nested.Children; handled = true; } } } } if (!handled) cur.Add(Custom(raw)); continue; } var mFor = Regex.Match(line, @"^for\s+\w+\s*=\s*([^,]+?)\s*,\s*(.+?)\s+do$"); if (mFor.Success) { var b = new LuaBlock { Key = "loopN", Values = { mFor.Groups[2].Value.Trim() } }; cur.Add(b); stack.Push(new Frame { Block = b, ParentList = cur, Ends = 1 }); cur = b.Children; continue; } var mWhile = Regex.Match(line, @"^while\s+(.+?)\s+do$"); if (mWhile.Success) { string cond = mWhile.Groups[1].Value.Trim(); var b = cond == "true" ? new LuaBlock { Key = "loopForever" } : new LuaBlock { Key = "loopWhile", Values = { cond } }; cur.Add(b); stack.Push(new Frame { Block = b, ParentList = cur, Ends = 1 }); cur = b.Children; continue; } var mIf = Regex.Match(line, @"^if\s+(.+?)\s+then$"); if (mIf.Success) { var b = new LuaBlock { Key = "if", Values = { mIf.Groups[1].Value.Trim() } }; cur.Add(b); stack.Push(new Frame { Block = b, ParentList = cur, Ends = 1 }); cur = b.Children; continue; } if (line == "break") { cur.Add(new LuaBlock { Key = "break" }); continue; } var tpl = MatchTemplate(line); if (tpl != null) { cur.Add(tpl); continue; } var mAdd = Regex.Match(line, @"^(\w+)\s*=\s*\1\s*\+\s*(.+)$"); if (mAdd.Success) { cur.Add(new LuaBlock { Key = "varAdd", Values = { mAdd.Groups[1].Value, mAdd.Groups[2].Value.Trim() } }); continue; } var mSet = Regex.Match(line, @"^([A-Za-z_]\w*)\s*=\s*(.+)$"); if (mSet.Success) { cur.Add(new LuaBlock { Key = "varSet", Values = { mSet.Groups[1].Value, mSet.Groups[2].Value.Trim() } }); continue; } cur.Add(Custom(raw)); } return root; } private static LuaBlock Custom(string line) => new() { Key = "lua", Values = { line.Trim() } }; /// 去掉行尾注释(不切字符串里的 --)。 private static string StripComment(string s) { bool inStr = false; for (int i = 0; i < s.Length - 1; i++) { char c = s[i]; if (c == '\\' && inStr) { i++; continue; } if (c == '"') inStr = !inStr; else if (!inStr && c == '-' && i + 1 < s.Length && s[i + 1] == '-') return s.Substring(0, i); } return s; } private static LuaBlock? MatchTemplate(string line) { foreach (var info in TplCache) { string callPat = Regex.Escape(info.Call).Replace(@"\.", @"\."); string pat = info.RetCount switch { 0 => "^(?:local\\s+)?" + callPat + @"\s*\((.*)\)$", 1 => "^(?:local\\s+)?([A-Za-z_][\\w\\.]*)\\s*=\\s*" + callPat + @"\s*\((.*)\)$", _ => "^(?:local\\s+)?([A-Za-z_][\\w\\.]*\\s*,\\s*[A-Za-z_][\\w\\.]*)\\s*=\\s*" + callPat + @"\s*\((.*)\)$" }; var m = Regex.Match(line, pat); if (!m.Success) continue; var args = SplitArgs(info.RetCount == 0 ? m.Groups[1].Value : m.Groups[2].Value); if (args.Count != info.ArgIdx.Count) continue; // 返回值变量数必须一致 if (info.RetCount > 0) { int lhs = m.Groups[1].Value.Split(',').Length; if (lhs != info.RetCount) continue; } var b = new LuaBlock { Key = info.Def.Key }; FillArgs(b, args, info.ArgIdx); return b; } return null; } private static void FillArgs(LuaBlock b, List args, List slotIdx) { var def = Find(b.Key)!; for (int k = 0; k < slotIdx.Count && k < args.Count; k++) { int slot = slotIdx[k]; while (b.Values.Count < def.Args.Length) b.Values.Add(""); b.Values[slot] = def.Args[slot].Kind is ArgKind.Text or ArgKind.Key ? Unquote(args[k]) : args[k].Trim(); } } /// 按顶层逗号拆参数(跳过字符串 / 括号内的逗号)。 private static List SplitArgs(string s) { var list = new List(); if (string.IsNullOrWhiteSpace(s)) return list; int depth = 0; bool inStr = false; var sb = new StringBuilder(); foreach (var c in s) { if (inStr) { sb.Append(c); if (c == '"') inStr = false; else if (c == '\\') { sb.Length--; /* 下一个字符原样进 */ } continue; } switch (c) { case '"': inStr = true; sb.Append(c); break; case '(' or '[' or '{': depth++; sb.Append(c); break; case ')' or ']' or '}': depth--; sb.Append(c); break; case ',' when depth == 0: list.Add(sb.ToString().Trim()); sb.Clear(); break; default: sb.Append(c); break; } } if (sb.Length > 0) list.Add(sb.ToString().Trim()); return list; } private static string Unquote(string s) { s = s.Trim(); if (s.Length >= 2 && ((s[0] == '"' && s[^1] == '"') || (s[0] == '\'' && s[^1] == '\''))) { s = s.Substring(1, s.Length - 2); s = s.Replace("\\\"", "\"").Replace("\\'", "'") .Replace("\\\\", "\\").Replace("\\n", "\n").Replace("\\t", "\t"); } return s; } // ==================== JSON ==================== private static readonly JsonSerializerOptions JsonOpt = new() { WriteIndented = true }; public static string ToJson(IEnumerable blocks) => JsonSerializer.Serialize(blocks, JsonOpt); public static List FromJson(string json) => JsonSerializer.Deserialize>(json) ?? new(); } }