Files
anjianjingling/Lua/LuaEngine.cs
T
2026-10-24 13:02:19 +08:00

277 lines
9.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
}
}
}