first commit
This commit is contained in:
commit
761b95080f
404 files changed
+312608
No files matched your search
@@ -0,0 +1,63 @@
|
||||
namespace anjianjingling.Lua
|
||||
{
|
||||
/// <summary>
|
||||
/// Lua 脚本能力宿主:由主窗体实现,向 Lua 引擎提供系统级操作能力。
|
||||
/// 所有方法均在后台执行线程上被调用。
|
||||
/// </summary>
|
||||
public interface ILuaHost
|
||||
{
|
||||
void Log(string msg);
|
||||
|
||||
/// <summary>是否启用拟人化(落点随机偏移 + 随机微延时)。</summary>
|
||||
bool JitterEnabled { get; }
|
||||
|
||||
// ---- 鼠标 ----
|
||||
void MouseMove(int x, int y);
|
||||
void MouseMoveR(int dx, int dy);
|
||||
void MouseClick(string button, int? x, int? y);
|
||||
void MouseDoubleClick(int? x, int? y);
|
||||
void MouseDown(string button);
|
||||
void MouseUp(string button);
|
||||
void MouseWheel(int delta);
|
||||
void MouseDrag(int x1, int y1, int x2, int y2, int durationMs);
|
||||
(int X, int Y) MousePosition();
|
||||
|
||||
// ---- 键盘 ----
|
||||
void KeyPress(string key);
|
||||
void KeyDown(string key);
|
||||
void KeyUp(string key);
|
||||
void HotKey(string expression);
|
||||
void TypeText(string text);
|
||||
|
||||
// ---- 屏幕与视觉 ----
|
||||
(int W, int H) ScreenSize();
|
||||
string ColorAt(int x, int y);
|
||||
(int X, int Y) FindColor(int x1, int y1, int x2, int y2, string hex, int tolerance);
|
||||
(int X, int Y, double Score) FindImage(int x1, int y1, int x2, int y2, string templatePath, double similarity);
|
||||
void ScreenShot(string path);
|
||||
|
||||
/// <summary>弹出框选截图,返回保存路径(用户取消返回 null)。</summary>
|
||||
string? PickRegion(string? savePath);
|
||||
|
||||
// ---- 窗口 / 系统 ----
|
||||
void Launch(string path, string? arguments);
|
||||
bool ActivateWindow(string titleKeyword);
|
||||
string[] ListWindows();
|
||||
|
||||
// ---- 剪贴板 ----
|
||||
string ClipGet();
|
||||
void ClipSet(string text);
|
||||
|
||||
// ---- 内嵌浏览器 ----
|
||||
void BrowserOpen(string url);
|
||||
bool BrowserClick(string selector);
|
||||
bool BrowserType(string selector, string text);
|
||||
bool BrowserWaitFor(string selector, int timeoutMs);
|
||||
string BrowserGetText(string selector);
|
||||
string BrowserExecJs(string js);
|
||||
void BrowserShot(string path);
|
||||
void BrowserBack();
|
||||
void BrowserForward();
|
||||
void BrowserReload();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace anjianjingling.Lua
|
||||
{
|
||||
public enum ArgKind
|
||||
{
|
||||
/// <summary>数字,原样输出。</summary>
|
||||
Number,
|
||||
/// <summary>字符串,生成时自动加引号转义。</summary>
|
||||
Text,
|
||||
/// <summary>键名 / 选择项,加引号。</summary>
|
||||
Key,
|
||||
/// <summary>表达式,原样输出。</summary>
|
||||
Expr,
|
||||
/// <summary>原样文本(变量名、注释),不加引号。</summary>
|
||||
Raw
|
||||
}
|
||||
|
||||
public enum BlockShape
|
||||
{
|
||||
/// <summary>单行命令。</summary>
|
||||
Simple,
|
||||
/// <summary>if <条件> then … end(可切换 else 分支)。</summary>
|
||||
If,
|
||||
/// <summary>如果屏幕上存在图片 … then … end。</summary>
|
||||
IfExists,
|
||||
/// <summary>for i = 1, N do … end。</summary>
|
||||
LoopN,
|
||||
/// <summary>while <条件> do … end。</summary>
|
||||
LoopWhile,
|
||||
/// <summary>break。</summary>
|
||||
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; }
|
||||
/// <summary>显示标题,含 {0} {1} 参数占位。</summary>
|
||||
public required string Title { get; init; }
|
||||
/// <summary>Lua 模板,含 {0} {1} 参数占位;容器块为起始行。</summary>
|
||||
public string Template { get; init; } = "";
|
||||
public BlockShape Shape { get; init; } = BlockShape.Simple;
|
||||
public BlockArg[] Args { get; init; } = Array.Empty<BlockArg>();
|
||||
public string Help { get; init; } = "";
|
||||
|
||||
public bool IsContainer => Shape is BlockShape.If or BlockShape.IfExists
|
||||
or BlockShape.LoopN or BlockShape.LoopWhile;
|
||||
}
|
||||
|
||||
/// <summary>画布上的一个积木实例。</summary>
|
||||
public sealed class LuaBlock
|
||||
{
|
||||
public string Key { get; set; } = "";
|
||||
public List<string> Values { get; set; } = new();
|
||||
/// <summary>容器块的子块。</summary>
|
||||
public List<LuaBlock> Children { get; set; } = new();
|
||||
/// <summary>If 块是否有 else 分支。</summary>
|
||||
public bool HasElse { get; set; }
|
||||
public List<LuaBlock> ElseChildren { get; set; } = new();
|
||||
|
||||
public LuaBlock Clone()
|
||||
{
|
||||
var b = new LuaBlock { Key = Key, HasElse = HasElse, Values = new List<string>(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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>积木目录:定义表 + Lua 生成 + JSON 读写。</summary>
|
||||
public static class BlockCatalog
|
||||
{
|
||||
public static readonly List<BlockDef> All = new();
|
||||
private static readonly Dictionary<string, BlockDef> 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<string> 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<LuaBlock> blocks)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
Emit(blocks, 0, sb);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void Emit(IEnumerable<LuaBlock> 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<LuaBlock> ParentList = null!;
|
||||
public int Ends; // 还差几个 end 才结束此块
|
||||
}
|
||||
|
||||
private sealed class TplInfo
|
||||
{
|
||||
public BlockDef Def = null!;
|
||||
public string Call = "";
|
||||
public int RetCount;
|
||||
public List<int> ArgIdx = new();
|
||||
}
|
||||
|
||||
private static List<TplInfo>? _tplCache;
|
||||
private static List<TplInfo> TplCache => _tplCache ??= BuildTplCache();
|
||||
|
||||
private static List<TplInfo> BuildTplCache()
|
||||
{
|
||||
var list = new List<TplInfo>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>把 Lua 脚本解析成积木。认识目录里的全部命令,认不出的行进「自定义代码」块。</summary>
|
||||
public static List<LuaBlock> FromLua(string code)
|
||||
{
|
||||
var root = new List<LuaBlock>();
|
||||
var stack = new Stack<Frame>();
|
||||
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<int> { 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() } };
|
||||
|
||||
/// <summary>去掉行尾注释(不切字符串里的 --)。</summary>
|
||||
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<string> args, List<int> 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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>按顶层逗号拆参数(跳过字符串 / 括号内的逗号)。</summary>
|
||||
private static List<string> SplitArgs(string s)
|
||||
{
|
||||
var list = new List<string>();
|
||||
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<LuaBlock> blocks) => JsonSerializer.Serialize(blocks, JsonOpt);
|
||||
|
||||
public static List<LuaBlock> FromJson(string json)
|
||||
=> JsonSerializer.Deserialize<List<LuaBlock>>(json) ?? new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
using MoonSharp.Interpreter;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace anjianjingling.Lua
|
||||
{
|
||||
public enum LuaState { Idle, Running, Paused }
|
||||
|
||||
/// <summary>脚本中止信号(停止 / 急停 / 主动 stop)。</summary>
|
||||
internal sealed class AbortSignal : Exception
|
||||
{
|
||||
public bool ByPanic { get; }
|
||||
public AbortSignal(bool byPanic) : base(byPanic ? "鼠标触及屏幕左上角" : "脚本已停止")
|
||||
=> ByPanic = byPanic;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lua 脚本执行器:MoonSharp 宿主 + 暂停/停止/急停 + 任务队列。
|
||||
/// </summary>
|
||||
public sealed class LuaEngine
|
||||
{
|
||||
private readonly ILuaHost _host;
|
||||
private CancellationTokenSource? _cts;
|
||||
private readonly ManualResetEventSlim _pauseGate = new(true);
|
||||
private Task? _task;
|
||||
private volatile bool _paused;
|
||||
private volatile bool _byPanic;
|
||||
private long _lastPanicCheckTick;
|
||||
|
||||
public LuaState State { get; private set; } = LuaState.Idle;
|
||||
public event Action<LuaState>? StateChanged;
|
||||
|
||||
/// <summary>执行速度倍率:只影响 delay 等待时长。</summary>
|
||||
public double Speed { get; set; } = 1.0;
|
||||
|
||||
public LuaEngine(ILuaHost host) => _host = host;
|
||||
|
||||
// ===================== 语法检查 =====================
|
||||
|
||||
/// <summary>只编译不执行;返回 null 表示语法通过。</summary>
|
||||
public string? Validate(string code)
|
||||
{
|
||||
try
|
||||
{
|
||||
var script = CreateScript();
|
||||
script.LoadString(code, null, "脚本");
|
||||
return null;
|
||||
}
|
||||
catch (SyntaxErrorException ex) { return Localize(ex.Message); }
|
||||
catch (Exception ex) { return ex.Message; }
|
||||
}
|
||||
|
||||
// ===================== 运行 =====================
|
||||
|
||||
/// <summary>运行单段脚本(一次)。</summary>
|
||||
public void Run(string code)
|
||||
{
|
||||
if (State != LuaState.Idle) { _host.Log("已有脚本在运行"); return; }
|
||||
|
||||
DynValue chunk;
|
||||
try
|
||||
{
|
||||
var script = CreateScript();
|
||||
chunk = script.LoadString(code, null, "脚本");
|
||||
}
|
||||
catch (SyntaxErrorException ex)
|
||||
{
|
||||
_host.Log("语法错误:" + Localize(ex.Message));
|
||||
return;
|
||||
}
|
||||
|
||||
RunBackground(() =>
|
||||
{
|
||||
chunk.Function.Call();
|
||||
_host.Log("脚本执行完毕");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>按顺序执行勾选的任务;每个任务按自身循环设置重复。</summary>
|
||||
public void RunQueue(IReadOnlyList<LuaTask> tasks)
|
||||
{
|
||||
if (State != LuaState.Idle) { _host.Log("已有脚本在运行"); return; }
|
||||
|
||||
var enabled = tasks.Where(t => t.Enabled).ToList();
|
||||
if (enabled.Count == 0) { _host.Log("没有勾选的任务"); return; }
|
||||
|
||||
// 启动前统一编译,提前暴露语法错误
|
||||
var prepared = new List<(LuaTask Task, DynValue Chunk)>();
|
||||
foreach (var t in enabled)
|
||||
{
|
||||
try
|
||||
{
|
||||
var script = CreateScript();
|
||||
prepared.Add((t, script.LoadString(t.Script, null, t.Name)));
|
||||
}
|
||||
catch (SyntaxErrorException ex)
|
||||
{
|
||||
_host.Log($"任务「{t.Name}」语法错误:{Localize(ex.Message)}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
RunBackground(() =>
|
||||
{
|
||||
for (int i = 0; i < prepared.Count; i++)
|
||||
{
|
||||
var (task, chunk) = prepared[i];
|
||||
_host.Log($"▶ 开始任务 {i + 1}/{prepared.Count}:{task.Name}({task.ModeText})");
|
||||
|
||||
int round = 0;
|
||||
while (true)
|
||||
{
|
||||
Tick();
|
||||
if (!task.Infinite && round >= task.LoopCount) break;
|
||||
|
||||
chunk.Function.Call();
|
||||
round++;
|
||||
|
||||
bool more = task.Infinite || round < task.LoopCount;
|
||||
if (more && task.IntervalMs > 0) Sleep(task.IntervalMs);
|
||||
}
|
||||
_host.Log($"✔ 任务完成:{task.Name}({round} 轮)");
|
||||
}
|
||||
_host.Log("★ 任务队列全部执行完毕");
|
||||
});
|
||||
}
|
||||
|
||||
private void RunBackground(Action body)
|
||||
{
|
||||
_paused = false;
|
||||
_byPanic = false;
|
||||
_pauseGate.Set();
|
||||
_cts = new CancellationTokenSource();
|
||||
|
||||
SetState(LuaState.Running);
|
||||
_task = Task.Run(() =>
|
||||
{
|
||||
try { body(); }
|
||||
catch (AbortSignal ex)
|
||||
{
|
||||
_host.Log(ex.ByPanic ? "■ 鼠标触及屏幕左上角,紧急停止" : "■ 脚本已停止");
|
||||
}
|
||||
catch (SyntaxErrorException ex) { _host.Log("语法错误:" + Localize(ex.Message)); }
|
||||
catch (ScriptRuntimeException ex) { _host.Log("运行错误:" + Localize(ex.Message)); }
|
||||
catch (InterpreterException ex) { _host.Log("运行错误:" + Localize(ex.Message)); }
|
||||
catch (Exception ex) { _host.Log("运行错误:" + ex.Message); }
|
||||
finally
|
||||
{
|
||||
_cts?.Dispose();
|
||||
_cts = null;
|
||||
_pauseGate.Set();
|
||||
_paused = false;
|
||||
SetState(LuaState.Idle);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Script CreateScript()
|
||||
{
|
||||
var script = new Script(CoreModules.Preset_HardSandbox
|
||||
| CoreModules.String | CoreModules.Math
|
||||
| CoreModules.Table | CoreModules.Coroutine);
|
||||
script.Options.DebugPrint = s => _host.Log(s);
|
||||
ScriptApi.Bind(script, this, _host);
|
||||
return script;
|
||||
}
|
||||
|
||||
// ===================== 控制 =====================
|
||||
|
||||
public void PauseOrResume()
|
||||
{
|
||||
if (State == LuaState.Running)
|
||||
{
|
||||
_paused = true;
|
||||
_pauseGate.Reset();
|
||||
_host.Log("‖ 已暂停(F9 继续)");
|
||||
SetState(LuaState.Paused);
|
||||
}
|
||||
else if (State == LuaState.Paused)
|
||||
{
|
||||
_paused = false;
|
||||
_pauseGate.Set();
|
||||
_host.Log("▶ 继续执行");
|
||||
SetState(LuaState.Running);
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (State == LuaState.Idle) return;
|
||||
_byPanic = false;
|
||||
_paused = false;
|
||||
_pauseGate.Set();
|
||||
_cts?.Cancel();
|
||||
}
|
||||
|
||||
private void SetState(LuaState s)
|
||||
{
|
||||
State = s;
|
||||
StateChanged?.Invoke(s);
|
||||
}
|
||||
|
||||
// ===================== 中断点(API 回调内调用)=====================
|
||||
|
||||
internal void Tick()
|
||||
{
|
||||
var cts = _cts;
|
||||
if (cts is { IsCancellationRequested: true }) throw new AbortSignal(_byPanic);
|
||||
|
||||
if (_paused)
|
||||
{
|
||||
_pauseGate.Wait();
|
||||
if (cts is { IsCancellationRequested: true }) throw new AbortSignal(_byPanic);
|
||||
}
|
||||
|
||||
// 急停检查节流到 100ms 一次,避免每条命令都调系统 API
|
||||
long now = Environment.TickCount64;
|
||||
if (now - _lastPanicCheckTick < 100) return;
|
||||
_lastPanicCheckTick = now;
|
||||
|
||||
Native.Win32.GetCursorPos(out var p);
|
||||
if (p.X > 0 || p.Y > 0) return;
|
||||
|
||||
Thread.Sleep(120);
|
||||
Native.Win32.GetCursorPos(out var p2);
|
||||
if (p2.X <= 0 && p2.Y <= 0)
|
||||
{
|
||||
_byPanic = true;
|
||||
_cts?.Cancel();
|
||||
throw new AbortSignal(true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>可中断、可暂停的延时(分片 20ms,支持倍速)。</summary>
|
||||
internal void Sleep(double ms)
|
||||
{
|
||||
ms = Math.Max(0, ms / Math.Max(0.05, Speed));
|
||||
var sw = Stopwatch.StartNew();
|
||||
while (sw.Elapsed.TotalMilliseconds < ms)
|
||||
{
|
||||
Tick();
|
||||
int slice = (int)Math.Min(20, Math.Max(1, ms - sw.Elapsed.TotalMilliseconds));
|
||||
_pauseGate.Wait(slice);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 错误信息中文化 =====================
|
||||
|
||||
private static readonly (string From, string To)[] Dict =
|
||||
{
|
||||
("unexpected symbol near '<eof>'", "语句未写完(意外到达文件结尾)"),
|
||||
("unexpected symbol", "意外的符号"),
|
||||
("'end' expected", "缺少 end"),
|
||||
("'=' expected", "缺少 ="),
|
||||
("'then' expected", "缺少 then"),
|
||||
("'do' expected", "缺少 do"),
|
||||
("unfinished string", "字符串未闭合"),
|
||||
("malformed number", "数字格式错误"),
|
||||
("attempt to index a nil value", "访问了空值(nil)"),
|
||||
("attempt to call a nil value", "调用了不存在的函数"),
|
||||
("attempt to perform arithmetic", "算术运算类型错误"),
|
||||
("attempt to compare", "比较运算类型错误"),
|
||||
("index out of range", "索引越界"),
|
||||
("cannot convert", "类型转换失败"),
|
||||
("invalid arguments", "参数不合法"),
|
||||
};
|
||||
|
||||
private static string Localize(string msg)
|
||||
{
|
||||
if (string.IsNullOrEmpty(msg)) return msg;
|
||||
foreach (var (from, to) in Dict)
|
||||
if (msg.Contains(from, StringComparison.OrdinalIgnoreCase))
|
||||
msg = msg.Replace(from, to, StringComparison.OrdinalIgnoreCase);
|
||||
return msg.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace anjianjingling.Lua
|
||||
{
|
||||
/// <summary>任务列表中的一个脚本任务。</summary>
|
||||
public sealed class LuaTask
|
||||
{
|
||||
public string Name { get; set; } = "新任务";
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>true=无限循环(会阻塞后续任务,需手动停止)。</summary>
|
||||
public bool Infinite { get; set; } = false;
|
||||
|
||||
public int LoopCount { get; set; } = 1;
|
||||
public int IntervalMs { get; set; } = 1000;
|
||||
public string Script { get; set; } = "";
|
||||
|
||||
/// <summary>积木画布 JSON(有内容时优先于脚本)。</summary>
|
||||
public string Blocks { get; set; } = "";
|
||||
|
||||
/// <summary>启动热键(如 F8),空表示不绑定。</summary>
|
||||
public string Hotkey { get; set; } = "";
|
||||
|
||||
public string Source { get; set; } = "";
|
||||
|
||||
public string ModeText => Infinite ? "无限循环" : $"{LoopCount} 次";
|
||||
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
/// <summary>任务列表 JSON 读写(.ajt)。</summary>
|
||||
public static class LuaTaskFile
|
||||
{
|
||||
private static readonly JsonSerializerOptions Opt = new() { WriteIndented = true };
|
||||
|
||||
public static string ToJson(IEnumerable<LuaTask> tasks) => JsonSerializer.Serialize(tasks, Opt);
|
||||
|
||||
public static List<LuaTask> FromJson(string json)
|
||||
=> JsonSerializer.Deserialize<List<LuaTask>>(json) ?? new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
using MoonSharp.Interpreter;
|
||||
|
||||
namespace anjianjingling.Lua
|
||||
{
|
||||
/// <summary>
|
||||
/// 把自动化能力注册进 Lua 全局环境。
|
||||
/// 两套写法并存:
|
||||
/// 1) 全局简写(按键精灵风格) moveTo(100,200) leftClick() delay(500)
|
||||
/// 2) 命名空间写法 mouse.moveTo(100,200) mouse.click() key.press("ENTER")
|
||||
/// </summary>
|
||||
internal static class ScriptApi
|
||||
{
|
||||
private const double DefaultSim = 0.85;
|
||||
private const int DefaultTolerance = 10;
|
||||
|
||||
public static void Bind(Script script, LuaEngine engine, ILuaHost host)
|
||||
{
|
||||
var g = script.Globals;
|
||||
|
||||
// ===================== 基础 =====================
|
||||
g["log"] = (Action<string>)(m => host.Log(m));
|
||||
g["delay"] = (Action<double>)(ms => engine.Sleep(ms));
|
||||
g["sleep"] = (Action<double>)(ms => engine.Sleep(ms));
|
||||
g["delayRandom"] = (Action<double, double>)((a, b) =>
|
||||
{
|
||||
int lo = (int)Math.Min(a, b), hi = (int)Math.Max(a, b);
|
||||
engine.Sleep(lo >= hi ? lo : Random.Shared.Next(lo, hi + 1));
|
||||
});
|
||||
g["random"] = (Func<double, double, double>)((a, b) =>
|
||||
{
|
||||
int lo = (int)Math.Min(a, b), hi = (int)Math.Max(a, b);
|
||||
return lo >= hi ? lo : Random.Shared.Next(lo, hi + 1);
|
||||
});
|
||||
g["now"] = (Func<double>)(() => DateTimeOffset.Now.ToUnixTimeMilliseconds());
|
||||
g["stop"] = (Action)(() => throw new AbortSignal(false));
|
||||
|
||||
// ===================== 鼠标 =====================
|
||||
var mouse = new Table(script);
|
||||
mouse["moveTo"] = (Action<int, int>)((x, y) => { engine.Tick(); host.MouseMove(x, y); });
|
||||
mouse["moveR"] = (Action<int, int>)((dx, dy) => { engine.Tick(); host.MouseMoveR(dx, dy); });
|
||||
mouse["down"] = (Action<string>)(b => { engine.Tick(); host.MouseDown(NormBtn(b)); });
|
||||
mouse["up"] = (Action<string>)(b => { engine.Tick(); host.MouseUp(NormBtn(b)); });
|
||||
mouse["wheel"] = (Action<int>)(d => { engine.Tick(); host.MouseWheel(d); });
|
||||
mouse["drag"] = (Action<int, int, int, int, int>)((x1, y1, x2, y2, ms) =>
|
||||
{
|
||||
engine.Tick();
|
||||
host.MouseDrag(x1, y1, x2, y2, ms <= 0 ? 400 : ms);
|
||||
});
|
||||
mouse["pos"] = (Func<DynValue>)(() =>
|
||||
{
|
||||
engine.Tick();
|
||||
var p = host.MousePosition();
|
||||
return DynValue.NewTuple(DynValue.NewNumber(p.X), DynValue.NewNumber(p.Y));
|
||||
});
|
||||
|
||||
// click 支持多种参数形态
|
||||
mouse["click"] = DynValue.NewCallback((ctx, a) =>
|
||||
{
|
||||
engine.Tick();
|
||||
string btn = "left";
|
||||
int? x = null, y = null;
|
||||
if (a.Count >= 3 && a[0].Type == DataType.Number)
|
||||
{
|
||||
x = (int)Math.Round(a[0].Number);
|
||||
y = (int)Math.Round(a[1].Number);
|
||||
btn = a[2].Type == DataType.String ? a[2].String : "left";
|
||||
}
|
||||
else if (a.Count >= 2 && a[0].Type == DataType.Number)
|
||||
{
|
||||
x = (int)Math.Round(a[0].Number);
|
||||
y = (int)Math.Round(a[1].Number);
|
||||
}
|
||||
else if (a.Count >= 1 && a[0].Type == DataType.String)
|
||||
{
|
||||
btn = a[0].String;
|
||||
}
|
||||
host.MouseClick(NormBtn(btn), x, y);
|
||||
return DynValue.Nil;
|
||||
});
|
||||
|
||||
mouse["doubleClick"] = DynValue.NewCallback((ctx, a) =>
|
||||
{
|
||||
engine.Tick();
|
||||
int? x = a.Count >= 2 && a[0].Type == DataType.Number ? (int)Math.Round(a[0].Number) : null;
|
||||
int? y = a.Count >= 2 && a[0].Type == DataType.Number ? (int)Math.Round(a[1].Number) : null;
|
||||
host.MouseDoubleClick(x, y);
|
||||
return DynValue.Nil;
|
||||
});
|
||||
g["mouse"] = mouse;
|
||||
|
||||
// ===================== 键盘 =====================
|
||||
var key = new Table(script);
|
||||
key["press"] = (Action<string>)(k => { engine.Tick(); host.KeyPress(k); });
|
||||
key["down"] = (Action<string>)(k => { engine.Tick(); host.KeyDown(k); });
|
||||
key["up"] = (Action<string>)(k => { engine.Tick(); host.KeyUp(k); });
|
||||
key["hotkey"] = (Action<string>)(k => { engine.Tick(); host.HotKey(k); });
|
||||
key["type"] = (Action<string>)(t => { engine.Tick(); host.TypeText(t); });
|
||||
g["key"] = key;
|
||||
|
||||
// ===================== 屏幕 / 视觉 =====================
|
||||
var screen = new Table(script);
|
||||
screen["size"] = (Func<DynValue>)(() =>
|
||||
{
|
||||
engine.Tick();
|
||||
var (w, h) = host.ScreenSize();
|
||||
return DynValue.NewTuple(DynValue.NewNumber(w), DynValue.NewNumber(h));
|
||||
});
|
||||
screen["color"] = (Func<int, int, string>)((x, y) => { engine.Tick(); return host.ColorAt(x, y); });
|
||||
screen["shot"] = (Action<string>)(p => { engine.Tick(); host.ScreenShot(p); });
|
||||
screen["pick"] = (Func<string, string>)(p =>
|
||||
{
|
||||
engine.Tick();
|
||||
return host.PickRegion(string.IsNullOrWhiteSpace(p) ? null : p) ?? "";
|
||||
});
|
||||
|
||||
screen["findColor"] = DynValue.NewCallback((ctx, a) =>
|
||||
{
|
||||
engine.Tick();
|
||||
string hex = a.Count > 0 && a[0].Type == DataType.String ? a[0].String : "FF0000";
|
||||
int tol = a.Count > 1 && a[1].Type == DataType.Number ? (int)Math.Round(a[1].Number) : DefaultTolerance;
|
||||
var (w, h) = host.ScreenSize();
|
||||
int x1 = a.Count > 2 ? (int)Math.Round(a[2].Number) : 0;
|
||||
int y1 = a.Count > 3 ? (int)Math.Round(a[3].Number) : 0;
|
||||
int x2 = a.Count > 4 ? (int)Math.Round(a[4].Number) : w - 1;
|
||||
int y2 = a.Count > 5 ? (int)Math.Round(a[5].Number) : h - 1;
|
||||
var p = host.FindColor(x1, y1, x2, y2, hex, tol);
|
||||
return DynValue.NewTuple(DynValue.NewNumber(p.X), DynValue.NewNumber(p.Y));
|
||||
});
|
||||
|
||||
screen["findImage"] = DynValue.NewCallback((ctx, a) =>
|
||||
{
|
||||
engine.Tick();
|
||||
string path = a.Count > 0 && a[0].Type == DataType.String ? a[0].String : "";
|
||||
double sim = a.Count > 1 && a[1].Type == DataType.Number ? a[1].Number : DefaultSim;
|
||||
var (w, h) = host.ScreenSize();
|
||||
int x1 = a.Count > 2 ? (int)Math.Round(a[2].Number) : 0;
|
||||
int y1 = a.Count > 3 ? (int)Math.Round(a[3].Number) : 0;
|
||||
int x2 = a.Count > 4 ? (int)Math.Round(a[4].Number) : w - 1;
|
||||
int y2 = a.Count > 5 ? (int)Math.Round(a[5].Number) : h - 1;
|
||||
var r = host.FindImage(x1, y1, x2, y2, path, sim);
|
||||
return DynValue.NewTuple(
|
||||
DynValue.NewNumber(r.X), DynValue.NewNumber(r.Y), DynValue.NewNumber(r.Score));
|
||||
});
|
||||
|
||||
screen["waitImage"] = DynValue.NewCallback((ctx, a) =>
|
||||
{
|
||||
string path = a.Count > 0 && a[0].Type == DataType.String ? a[0].String : "";
|
||||
int timeout = a.Count > 1 && a[1].Type == DataType.Number ? (int)Math.Round(a[1].Number) : 10000;
|
||||
double sim = a.Count > 2 && a[2].Type == DataType.Number ? a[2].Number : DefaultSim;
|
||||
|
||||
var (w, h) = host.ScreenSize();
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
int rx = -1, ry = -1;
|
||||
double best = 0;
|
||||
while (sw.ElapsedMilliseconds < timeout)
|
||||
{
|
||||
engine.Tick();
|
||||
var r = host.FindImage(0, 0, w - 1, h - 1, path, sim);
|
||||
best = r.Score;
|
||||
if (r.X >= 0) { rx = r.X; ry = r.Y; break; }
|
||||
engine.Sleep(300);
|
||||
}
|
||||
host.Log(rx >= 0
|
||||
? $"等待图片出现:找到 ({rx},{ry}) 相似度 {best:0.00}"
|
||||
: $"等待图片超时:{path}(最高 {best:0.00})");
|
||||
return DynValue.NewTuple(
|
||||
DynValue.NewNumber(rx), DynValue.NewNumber(ry), DynValue.NewNumber(best));
|
||||
});
|
||||
|
||||
screen["exists"] = DynValue.NewCallback((ctx, a) =>
|
||||
{
|
||||
engine.Tick();
|
||||
string path = a.Count > 0 && a[0].Type == DataType.String ? a[0].String : "";
|
||||
double sim = a.Count > 1 && a[1].Type == DataType.Number ? a[1].Number : DefaultSim;
|
||||
var (w, h) = host.ScreenSize();
|
||||
var r = host.FindImage(0, 0, w - 1, h - 1, path, sim);
|
||||
return DynValue.NewBoolean(r.X >= 0);
|
||||
});
|
||||
g["screen"] = screen;
|
||||
|
||||
// ===================== 窗口 / 系统 =====================
|
||||
var win = new Table(script);
|
||||
win["launch"] = (Action<string, string>)((p, args) => { engine.Tick(); host.Launch(p, args); });
|
||||
win["activate"] = (Func<string, bool>)(t => { engine.Tick(); return host.ActivateWindow(t); });
|
||||
win["list"] = (Func<DynValue>)(() =>
|
||||
{
|
||||
engine.Tick();
|
||||
var list = new Table(script);
|
||||
var arr = host.ListWindows();
|
||||
for (int i = 0; i < arr.Length; i++) list[i + 1] = arr[i];
|
||||
return DynValue.NewTable(list);
|
||||
});
|
||||
g["win"] = win;
|
||||
|
||||
var sys = new Table(script);
|
||||
sys["time"] = (Func<double>)(() => DateTimeOffset.Now.ToUnixTimeMilliseconds());
|
||||
sys["sleep"] = (Action<double>)(ms => engine.Sleep(ms));
|
||||
sys["speed"] = (Func<double, double>)(s => { engine.Speed = s <= 0 ? 1 : s; return engine.Speed; });
|
||||
g["sys"] = sys;
|
||||
|
||||
// ===================== 剪贴板 =====================
|
||||
var clip = new Table(script);
|
||||
clip["get"] = (Func<string>)(() => host.ClipGet());
|
||||
clip["set"] = (Action<string>)(t => host.ClipSet(t));
|
||||
g["clip"] = clip;
|
||||
|
||||
// ===================== 浏览器 =====================
|
||||
var browser = new Table(script);
|
||||
browser["open"] = (Action<string>)(u => { engine.Tick(); host.BrowserOpen(u); });
|
||||
browser["click"] = (Func<string, bool>)(s => { engine.Tick(); return host.BrowserClick(s); });
|
||||
browser["type"] = (Func<string, string, bool>)((s, t) => { engine.Tick(); return host.BrowserType(s, t); });
|
||||
browser["wait"] = (Func<string, int, bool>)((s, ms) => { engine.Tick(); return host.BrowserWaitFor(s, ms <= 0 ? 10000 : ms); });
|
||||
browser["text"] = (Func<string, string>)(s => { engine.Tick(); return host.BrowserGetText(s); });
|
||||
browser["js"] = (Func<string, string>)(js => { engine.Tick(); return host.BrowserExecJs(js); });
|
||||
browser["shot"] = (Action<string>)(p => { engine.Tick(); host.BrowserShot(p); });
|
||||
browser["back"] = (Action)(() => { engine.Tick(); host.BrowserBack(); });
|
||||
browser["forward"] = (Action)(() => { engine.Tick(); host.BrowserForward(); });
|
||||
browser["reload"] = (Action)(() => { engine.Tick(); host.BrowserReload(); });
|
||||
g["browser"] = browser;
|
||||
|
||||
// ===================== 全局简写 =====================
|
||||
g["moveTo"] = mouse["moveTo"];
|
||||
g["moveR"] = mouse["moveR"];
|
||||
g["click"] = mouse["click"];
|
||||
g["leftClick"] = DynValue.NewCallback((ctx, a) =>
|
||||
{
|
||||
engine.Tick();
|
||||
int? x = a.Count >= 2 ? (int)Math.Round(a[0].Number) : null;
|
||||
int? y = a.Count >= 2 ? (int)Math.Round(a[1].Number) : null;
|
||||
host.MouseClick("left", x, y);
|
||||
return DynValue.Nil;
|
||||
});
|
||||
g["rightClick"] = DynValue.NewCallback((ctx, a) =>
|
||||
{
|
||||
engine.Tick();
|
||||
int? x = a.Count >= 2 ? (int)Math.Round(a[0].Number) : null;
|
||||
int? y = a.Count >= 2 ? (int)Math.Round(a[1].Number) : null;
|
||||
host.MouseClick("right", x, y);
|
||||
return DynValue.Nil;
|
||||
});
|
||||
g["middleClick"] = DynValue.NewCallback((ctx, a) =>
|
||||
{
|
||||
engine.Tick();
|
||||
int? x = a.Count >= 2 ? (int)Math.Round(a[0].Number) : null;
|
||||
int? y = a.Count >= 2 ? (int)Math.Round(a[1].Number) : null;
|
||||
host.MouseClick("middle", x, y);
|
||||
return DynValue.Nil;
|
||||
});
|
||||
g["doubleClick"] = mouse["doubleClick"];
|
||||
g["wheel"] = mouse["wheel"];
|
||||
g["drag"] = mouse["drag"];
|
||||
|
||||
g["keyPress"] = key["press"];
|
||||
g["keyDown"] = key["down"];
|
||||
g["keyUp"] = key["up"];
|
||||
g["hotkey"] = key["hotkey"];
|
||||
g["typeText"] = key["type"];
|
||||
g["sayText"] = key["type"];
|
||||
|
||||
g["findImage"] = screen["findImage"];
|
||||
g["findColor"] = screen["findColor"];
|
||||
g["waitImage"] = screen["waitImage"];
|
||||
g["exists"] = screen["exists"];
|
||||
g["colorAt"] = screen["color"];
|
||||
g["screenshot"] = screen["shot"];
|
||||
|
||||
g["run"] = win["launch"];
|
||||
g["activate"] = win["activate"];
|
||||
g["clipGet"] = clip["get"];
|
||||
g["clipSet"] = clip["set"];
|
||||
}
|
||||
|
||||
private static string NormBtn(string b) => b.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"右" or "right" or "r" or "rightclick" => "right",
|
||||
"中" or "middle" or "m" => "middle",
|
||||
_ => "left"
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user