diff --git a/engine/Age.Engine.Tests/RecoverTests.cs b/engine/Age.Engine.Tests/RecoverTests.cs new file mode 100644 index 0000000..1fa6a8a --- /dev/null +++ b/engine/Age.Engine.Tests/RecoverTests.cs @@ -0,0 +1,34 @@ +using Age.Engine.Hosting; +using Age.Engine.Sys4; +using Age.Engine.Vm; +using Xunit; + +public class RecoverTests +{ + private static long G(VirtualMachine vm, int k) => vm.Globals.TryGetValue(k, out var v) ? v : 0; + + [Fact] + public void RecoverUnitTestPasses() + { + var table = OpcodeTableJson.Load(Paths.OpcodesJson); + var script = Sys4Loader.Load(Path.Combine(Paths.Data1, "RECOVER.BIN"), table); + var vm = new VirtualMachine(script, table, new CaptureHost()); + + int unit = 0; + vm.Globals[0x152616] = unit; + int A = 0x4e11b, B = 0x4e085, C = 0x52383, E = 0x52f3b, F = 0x5295f, FL = 0xaacb4; + for (int k = 0; k < 3; k++) vm.Globals[A + unit * 14 + (11 + k)] = 100 + k; + vm.Globals[C + unit * 30 + 5] = 7; vm.Globals[FL + 5] = 1; vm.Globals[E + unit * 30 + 5] = 42; + vm.Globals[C + unit * 30 + 6] = 0; vm.Globals[FL + 6] = 1; vm.Globals[E + unit * 30 + 6] = 99; + vm.Globals[C + unit * 30 + 7] = 3; vm.Globals[FL + 7] = 0; vm.Globals[E + unit * 30 + 7] = 88; + vm.Run(); + + Assert.Equal(100, G(vm, B + unit * 3 + 0)); + Assert.Equal(101, G(vm, B + unit * 3 + 1)); + Assert.Equal(102, G(vm, B + unit * 3 + 2)); + Assert.Equal(42, G(vm, C + unit * 30 + 5)); + Assert.Equal(-1, G(vm, F + unit * 30 + 5)); + Assert.Equal(0, G(vm, C + unit * 30 + 6)); + Assert.Equal(3, G(vm, C + unit * 30 + 7)); + } +} diff --git a/engine/Age.Engine/Hosting/CaptureHost.cs b/engine/Age.Engine/Hosting/CaptureHost.cs new file mode 100644 index 0000000..14dbac3 --- /dev/null +++ b/engine/Age.Engine/Hosting/CaptureHost.cs @@ -0,0 +1,10 @@ +namespace Age.Engine.Hosting; +public sealed class CaptureHost : IHost +{ + public List<(int Offset, string Text)> Emitted { get; } = new(); + public int CallScriptCount { get; private set; } + public Dictionary Stubs { get; } = new(); + public void ShowText(int offset, string text) => Emitted.Add((offset, text)); + public void CallScript(long id) => CallScriptCount++; + public void OnStub(int opcode) { Stubs.TryGetValue(opcode, out var c); Stubs[opcode] = c + 1; } +} diff --git a/engine/Age.Engine/Hosting/IHost.cs b/engine/Age.Engine/Hosting/IHost.cs new file mode 100644 index 0000000..030276b --- /dev/null +++ b/engine/Age.Engine/Hosting/IHost.cs @@ -0,0 +1,7 @@ +namespace Age.Engine.Hosting; +public interface IHost +{ + void ShowText(int offset, string text); + void CallScript(long id); + void OnStub(int opcode); +} diff --git a/engine/Age.Engine/Vm/Frame.cs b/engine/Age.Engine/Vm/Frame.cs new file mode 100644 index 0000000..d8e3612 --- /dev/null +++ b/engine/Age.Engine/Vm/Frame.cs @@ -0,0 +1,8 @@ +namespace Age.Engine.Vm; +public sealed class Frame +{ + public Dictionary I = new(); // local-int + public Dictionary F = new(); // local-float (raw) + public Dictionary S = new(); // local-string + public Dictionary P = new(); // local-ptr (holds a global address) +} diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs new file mode 100644 index 0000000..9594993 --- /dev/null +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -0,0 +1,174 @@ +using Age.Engine.Hosting; +using Age.Engine.Model; +namespace Age.Engine.Vm; + +public sealed class VirtualMachine +{ + private const long NoJump = 0xFFFFFFFF; + private const int HALT = int.MinValue; + private const int T_IMM = 0, T_STR = 2, T_GINT = 3, T_GFLOAT = 4, T_GSTR = 5, T_GPTR = 6, + T_LINT = 9, T_LFLOAT = 10, T_LSTR = 11, T_LPTR = 12; + + private readonly Script _s; + private readonly OpcodeTable _t; + private readonly IHost _host; + private readonly VmOptions _o; + private readonly Frame _fr = new(); + private readonly List _callstack = new(); + private readonly Dictionary _emitSeen = new(); + + public Dictionary Globals { get; } = new(); + public Dictionary GlobalStrings { get; } = new(); + public List<(int Offset, string Text)> Emitted { get; } = new(); + public string? HaltReason { get; private set; } + public long Steps { get; private set; } + + public VirtualMachine(Script s, OpcodeTable t, IHost host, VmOptions? o = null) + { _s = s; _t = t; _host = host; _o = o ?? new VmOptions(); } + + private static long Gi(Dictionary d, int k) => d.TryGetValue(k, out var v) ? v : 0; + private static string Gs(Dictionary d, int k) => d.TryGetValue(k, out var v) ? v : ""; + private static long PyDiv(long a, long b) { if (b == 0) return 0; long q = a / b, r = a % b; if (r != 0 && (r < 0) != (b < 0)) q--; return q; } + private static long PyMod(long a, long b) { if (b == 0) return 0; long r = a % b; if (r != 0 && (r < 0) != (b < 0)) r += b; return r; } + + private static bool IsStr(Operand o) => o.Type == T_STR || o.Type == T_GSTR || o.Type == T_LSTR; + + private long Read(Operand op) => op.Type switch + { + T_IMM => op.Value, + T_GINT or T_GFLOAT => Gi(Globals, (int)op.Value), + T_GPTR => Gi(Globals, (int)Gi(Globals, (int)op.Value)), + T_LINT => Gi(_fr.I, (int)op.Value), + T_LFLOAT => Gi(_fr.F, (int)op.Value), + T_LPTR => Gi(Globals, (int)Gi(_fr.P, (int)op.Value)), + _ => op.Value, + }; + + private void Write(Operand op, long val) + { + switch (op.Type) + { + case T_GINT: case T_GFLOAT: Globals[(int)op.Value] = val; break; + case T_GPTR: Globals[(int)Gi(Globals, (int)op.Value)] = val; break; + case T_LINT: _fr.I[(int)op.Value] = val; break; + case T_LFLOAT: _fr.F[(int)op.Value] = val; break; + case T_LPTR: Globals[(int)Gi(_fr.P, (int)op.Value)] = val; break; + } + } + + private string ReadStr(Operand op) => op.Type switch + { + T_STR => _s.GetString((int)op.Value), + T_GSTR => Gs(GlobalStrings, (int)op.Value), + T_LSTR => Gs(_fr.S, (int)op.Value), + _ => "", + }; + + private void WriteStr(Operand op, string val) + { + switch (op.Type) + { + case T_GSTR: GlobalStrings[(int)op.Value] = val; break; + case T_LSTR: _fr.S[(int)op.Value] = val; break; + } + } + + private long BaseAddr(Operand op) => op.Type switch + { + T_IMM or T_GINT or T_GFLOAT or T_GSTR or T_GPTR => op.Value, + T_LINT => Gi(_fr.I, (int)op.Value), + T_LPTR => Gi(_fr.P, (int)op.Value), + _ => op.Value, + }; + + private void LookupStore(Operand dst, long addr) + { + switch (dst.Type) + { + case T_LPTR: _fr.P[(int)dst.Value] = addr; break; + case T_GPTR: Globals[(int)dst.Value] = addr; break; + default: Write(dst, Gi(Globals, (int)addr)); break; + } + } + + public void Run(int entryOffset = 0) + { + int pc = _s.IndexByOffset.TryGetValue(entryOffset, out var idx) ? idx : 0; + while (pc >= 0 && pc < _s.Instructions.Count) + { + if (Steps >= _o.MaxSteps) { HaltReason ??= "STEP-LIMIT"; return; } + Steps++; + int next = Step(_s.Instructions[pc], pc); + if (next == HALT) return; + pc = next; + } + HaltReason ??= "pc-out-of-range"; + } + + private int Step(Instruction ins, int pc) + { + int op = ins.Opcode; + var a = ins.Args; + switch (_t.Label(op)) + { + case "add": Write(a[0], Read(a[1]) + Read(a[2])); return pc + 1; + case "sub": Write(a[0], Read(a[1]) - Read(a[2])); return pc + 1; + case "mul": Write(a[0], Read(a[1]) * Read(a[2])); return pc + 1; + case "div": Write(a[0], PyDiv(Read(a[1]), Read(a[2]))); return pc + 1; + case "mod": Write(a[0], PyMod(Read(a[1]), Read(a[2]))); return pc + 1; + case "and": Write(a[0], Read(a[1]) & Read(a[2])); return pc + 1; + case "or": Write(a[0], Read(a[1]) | Read(a[2])); return pc + 1; + case "sar": Write(a[0], Read(a[1]) >> (int)(Read(a[2]) & 31)); return pc + 1; + case "shl": Write(a[0], Read(a[1]) << (int)(Read(a[2]) & 31)); return pc + 1; + case "eq": Write(a[0], Read(a[1]) == Read(a[2]) ? 1 : 0); return pc + 1; + case "ne": Write(a[0], Read(a[1]) != Read(a[2]) ? 1 : 0); return pc + 1; + case "lt": Write(a[0], Read(a[1]) < Read(a[2]) ? 1 : 0); return pc + 1; + case "lte": Write(a[0], Read(a[1]) <= Read(a[2]) ? 1 : 0); return pc + 1; + case "gr": Write(a[0], Read(a[1]) > Read(a[2]) ? 1 : 0); return pc + 1; + case "gre": Write(a[0], Read(a[1]) >= Read(a[2]) ? 1 : 0); return pc + 1; + case "mov": + case "set-string": + if (IsStr(a[0]) || IsStr(a[1])) WriteStr(a[0], ReadStr(a[1])); + else Write(a[0], Read(a[1])); + return pc + 1; + case "lookup-array": + LookupStore(a[0], BaseAddr(a[1]) + Read(a[2])); return pc + 1; + case "lookup-array-2d": + LookupStore(a[0], BaseAddr(a[1]) + Read(a[2]) * Read(a[3]) + Read(a[4])); return pc + 1; + case "bit-set": Write(a[0], Read(a[0]) | Read(a[1])); return pc + 1; + case "bit-reset": Write(a[0], Read(a[0]) & ~Read(a[1])); return pc + 1; + case "check-bit": Write(a[0], (Read(a[1]) >> (int)(Read(a[2]) & 31)) & 1); return pc + 1; + case "copy-to-global": Write(a[0], Read(a[1])); return pc + 1; + case "jmp": return _s.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1); + case "call": _callstack.Add(pc + 1); return _s.IndexByOffset.GetValueOrDefault((int)a[0].Value, pc + 1); + case "ret": + if (_callstack.Count > 0) { int r = _callstack[^1]; _callstack.RemoveAt(_callstack.Count - 1); return r; } + HaltReason = "ret-underflow"; return HALT; + case "jcc": + { + long tgt = Read(a[0]) != 0 ? a[1].Value : a[2].Value; + return tgt == NoJump ? pc + 1 : _s.IndexByOffset.GetValueOrDefault((int)tgt, pc + 1); + } + case "exit": + case "exit-script": HaltReason = "exit"; return HALT; + case "call-script": _host.CallScript(a.Count > 0 ? Read(a[0]) : 0); return pc + 1; + case "show-text": + foreach (var o in a) + { + if (o.Type != T_STR) continue; + int off = (int)o.Value; + _emitSeen.TryGetValue(off, out var c); c++; _emitSeen[off] = c; + if (c > _o.EmitCap) { HaltReason = $"LOOP:line@0x{off:x}×{c}"; return HALT; } + string text = _s.GetString(off); + Emitted.Add((off, text)); + _host.ShowText(off, text); + } + return pc + 1; + case "end-text-line": case "wait-for-input": case "set-font": + case "comment": case "display-furigana": case "dev_ukn": + return pc + 1; + default: + _host.OnStub(op); return pc + 1; + } + } +} diff --git a/engine/Age.Engine/Vm/VmOptions.cs b/engine/Age.Engine/Vm/VmOptions.cs new file mode 100644 index 0000000..329cab9 --- /dev/null +++ b/engine/Age.Engine/Vm/VmOptions.cs @@ -0,0 +1,2 @@ +namespace Age.Engine.Vm; +public sealed record VmOptions(int EmitCap = 2, long MaxSteps = 2_000_000);