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();
}
}