277 lines
9.8 KiB
C#
277 lines
9.8 KiB
C#
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();
|
||
}
|
||
}
|
||
}
|