diff --git a/engine/Age.Cli/Program.cs b/engine/Age.Cli/Program.cs index babe1cb..eb0655b 100644 --- a/engine/Age.Cli/Program.cs +++ b/engine/Age.Cli/Program.cs @@ -250,6 +250,7 @@ sealed class AudioTraceHost : IHost } public void ShowText(int offset, string text) { } public void WaitForInput() { } + public void Sleep(long duration) { } public void CreateTexture(int slot, int width, int height) { } public void SetTexture(long resourceId, int slot) { } public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY) { } @@ -298,6 +299,7 @@ sealed class GfxTraceHost : IHost } public void ShowText(int offset, string text) { } public void WaitForInput() { } + public void Sleep(long duration) { } public void PlayBgm(long id) { } public void PlayVoice(long id) { } } diff --git a/engine/Age.Engine.Tests/CallScriptIntegrationTests.cs b/engine/Age.Engine.Tests/CallScriptIntegrationTests.cs index 593337b..9e9ebe8 100644 --- a/engine/Age.Engine.Tests/CallScriptIntegrationTests.cs +++ b/engine/Age.Engine.Tests/CallScriptIntegrationTests.cs @@ -9,6 +9,7 @@ public class CallScriptIntegrationTests { public void ShowText(int o, string t) { } public void WaitForInput() { } + public void Sleep(long duration) { } public void CreateTexture(int s, int w, int h) { } public void SetTexture(long r, int s) { } public void DrawTexture(int s, int sx, int sy, int w, int h, int dx, int dy) { } diff --git a/engine/Age.Engine.Tests/CallScriptIntegrationTests.cs.bak b/engine/Age.Engine.Tests/CallScriptIntegrationTests.cs.bak new file mode 100644 index 0000000..593337b --- /dev/null +++ b/engine/Age.Engine.Tests/CallScriptIntegrationTests.cs.bak @@ -0,0 +1,49 @@ +using Age.Engine.Hosting; +using Age.Engine.Sys4; +using Age.Engine.Vm; +using Xunit; + +public class CallScriptIntegrationTests +{ + private sealed class NullHost : IHost + { + public void ShowText(int o, string t) { } + public void WaitForInput() { } + public void CreateTexture(int s, int w, int h) { } + public void SetTexture(long r, int s) { } + public void DrawTexture(int s, int sx, int sy, int w, int h, int dx, int dy) { } + public (int Width, int Height) GetTextureSize(int s) => (0, 0); + public void PlayBgm(long id) { } + public void PlayVoice(long id) { } + } + + [Fact] + public void RealScriptExecutesRealSubroutinesAndReturns() + { + // ADDILL.BIN unconditionally call-scripts ADDILLSUB then CALCREVISE at entry, then exits. + // With execution on, both subroutines load, run, and return, so ADDILL reaches its own exit. + var t = OpcodeTableJson.Load(Paths.OpcodesJson); + var provider = Sys4ScriptProvider.Load(t); + var script = Sys4Loader.Load(Paths.Scripts()["ADDILL.BIN"], t); + var host = new NullHost(); + var vm = new VirtualMachine(script, t, host, null, provider); + vm.Run(); + Assert.Equal(2, vm.CallScriptDispatches); // ADDILLSUB + CALCREVISE both dispatched + Assert.Equal("exit", vm.HaltReason); // subroutines returned; ADDILL reached its own exit + } + + [Fact] + public void BunkiTopLevelRetReturnsCleanlyAsSubroutine() + { + // BUNKI.BIN ends with a top-level `ret` (empty intra-call stack). Called as a subroutine it + // must return to the caller, not underflow-halt. Drive it directly. + var t = OpcodeTableJson.Load(Paths.OpcodesJson); + var provider = Sys4ScriptProvider.Load(t); + var bunki = provider.GetById(0x143); // BUNKI.BIN + Assert.NotNull(bunki); + var vm = new VirtualMachine(bunki!, t, new NullHost(), null, provider); + vm.Run(); + // Reaching a frame-return at the top = clean "exit"; never "ret-underflow". + Assert.NotEqual("ret-underflow", vm.HaltReason); + } +} diff --git a/engine/Age.Engine.Tests/CallScriptTests.cs b/engine/Age.Engine.Tests/CallScriptTests.cs index b3a83b6..07ef8bb 100644 --- a/engine/Age.Engine.Tests/CallScriptTests.cs +++ b/engine/Age.Engine.Tests/CallScriptTests.cs @@ -15,6 +15,7 @@ public class CallScriptTests { public void ShowText(int o, string t) { } public void WaitForInput() { } + public void Sleep(long duration) { } public void CreateTexture(int s, int w, int h) { } public void SetTexture(long r, int s) { } public void DrawTexture(int s, int sx, int sy, int w, int h, int dx, int dy) { } diff --git a/engine/Age.Engine.Tests/CallScriptTests.cs.bak b/engine/Age.Engine.Tests/CallScriptTests.cs.bak new file mode 100644 index 0000000..b3a83b6 --- /dev/null +++ b/engine/Age.Engine.Tests/CallScriptTests.cs.bak @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using Age.Engine.Hosting; +using Age.Engine.Model; +using Age.Engine.Sys4; +using Age.Engine.Vm; +using Xunit; + +public class CallScriptTests +{ + // Opcodes (from build/opcodes.json): exit=0x2, call-script=0x3(argc1), mov=0x55(argc2). + // Operand types: imm=0, global-int=3, local-int=9. + private const uint OP_EXIT = 0x2, OP_CALLSCRIPT = 0x3, OP_MOV = 0x55; + + private sealed class NullHost : IHost + { + public void ShowText(int o, string t) { } + public void WaitForInput() { } + public void CreateTexture(int s, int w, int h) { } + public void SetTexture(long r, int s) { } + public void DrawTexture(int s, int sx, int sy, int w, int h, int dx, int dy) { } + public (int Width, int Height) GetTextureSize(int s) => (0, 0); + public void PlayBgm(long id) { } + public void PlayVoice(long id) { } + } + + private sealed class MapProvider : IScriptProvider + { + private readonly Dictionary _m; + public MapProvider(Dictionary m) => _m = m; + public Script? GetById(long id) => _m.TryGetValue(id, out var s) ? s : null; + } + + // Build a Script from raw dwords via the real loader (guarantees identical decode). + private static Script Asm(OpcodeTable t, string name, params uint[] body) + { + var bytes = new byte[0x3C + body.Length * 4]; + System.Text.Encoding.ASCII.GetBytes("SYS4422 ").CopyTo(bytes, 0); + // fields[8] (F8 = code end) at header offset 8 + 8*4 = 0x28; set to body length (all code). + System.BitConverter.GetBytes(body.Length).CopyTo(bytes, 8 + 8 * 4); + for (int i = 0; i < body.Length; i++) System.BitConverter.GetBytes(body[i]).CopyTo(bytes, 0x3C + i * 4); + return Sys4Loader.Parse(bytes, t, name); + } + + private static OpcodeTable Table() => OpcodeTableJson.Load(Paths.OpcodesJson); + + [Fact] + public void CalleeRunsAndControlResumesAfterTheCall() + { + var t = Table(); + // Callee (id 5): mov g[0x10] = 7, then exit. + var callee = Asm(t, "CALLEE", OP_MOV, 3, 0x10, 0, 7, OP_EXIT); + // Caller: call-script 5 ; mov g[0x11] = g[0x10] ; exit. + var caller = Asm(t, "CALLER", + OP_CALLSCRIPT, 0, 5, + OP_MOV, 3, 0x11, 3, 0x10, + OP_EXIT); + var host = new NullHost(); + var sink = new RecordingTraceSink(); + var vm = new VirtualMachine(caller, t, host, null, new MapProvider(new() { [5] = callee }), sink); + vm.Run(); + Assert.Equal(7, vm.Globals[0x10]); // callee wrote a shared global + Assert.Equal(7, vm.Globals[0x11]); // caller read it AFTER the call returned + Assert.Equal("exit", vm.HaltReason); // top-level exit + Assert.Contains(5L, sink.CallScriptIds); // dispatch observed via the trace sink + } + + [Fact] + public void MissingProviderFallsBackToStub() + { + var t = Table(); + var caller = Asm(t, "CALLER", OP_CALLSCRIPT, 0, 5, OP_EXIT); + var host = new NullHost(); + var sink = new RecordingTraceSink(); + var vm = new VirtualMachine(caller, t, host, null, null, sink); // no provider + vm.Run(); + Assert.Equal("exit", vm.HaltReason); // did not halt on the call; stub + continue + Assert.Contains(5L, sink.CallScriptIds); + } + + [Fact] + public void UnresolvedIdHalts() + { + var t = Table(); + var caller = Asm(t, "CALLER", OP_CALLSCRIPT, 0, 99, OP_EXIT); + var vm = new VirtualMachine(caller, t, new NullHost(), null, new MapProvider(new())); + vm.Run(); + Assert.StartsWith("callscript-unresolved", vm.HaltReason); + } + + [Fact] + public void LocalsDoNotLeakBetweenCallerAndCallee() + { + var t = Table(); + // Callee writes LOCAL-int 0 = 42 (mov to local-int, type 9), then exit. + var callee = Asm(t, "CALLEE", OP_MOV, 9, 0, 0, 42, OP_EXIT); + // Caller sets local-int 0 = 1, calls, then copies its own local-int 0 to global 0x20. + var caller = Asm(t, "CALLER", + OP_MOV, 9, 0, 0, 1, // l[0] = 1 + OP_CALLSCRIPT, 0, 5, // call-script 5 (callee sets ITS local 0 = 42) + OP_MOV, 3, 0x20, 9, 0, // g[0x20] = l[0] + OP_EXIT); + var vm = new VirtualMachine(caller, t, new NullHost(), null, new MapProvider(new() { [5] = callee })); + vm.Run(); + Assert.Equal(1, vm.Globals[0x20]); // caller's local 0 unchanged by callee's local 0 + } +} diff --git a/engine/Age.Engine.Tests/GameSessionTests.cs b/engine/Age.Engine.Tests/GameSessionTests.cs index bfd440e..a7b9d12 100644 --- a/engine/Age.Engine.Tests/GameSessionTests.cs +++ b/engine/Age.Engine.Tests/GameSessionTests.cs @@ -29,6 +29,7 @@ public class GameSessionTests public List Emitted = new(); public void ShowText(int o, string t) => Emitted.Add(o); public void WaitForInput() { } + public void Sleep(long duration) { } public void CreateTexture(int s, int w, int h) { } public void SetTexture(long r, int s) { } public void DrawTexture(int s, int sx, int sy, int w, int h, int dx, int dy) { } diff --git a/engine/Age.Engine.Tests/GameSessionTests.cs.bak b/engine/Age.Engine.Tests/GameSessionTests.cs.bak new file mode 100644 index 0000000..bfd440e --- /dev/null +++ b/engine/Age.Engine.Tests/GameSessionTests.cs.bak @@ -0,0 +1,132 @@ +using System.Collections.Generic; +using System.Linq; +using Age.Engine.Hosting; +using Age.Engine.Model; +using Age.Engine.Sys4; +using Age.Engine.Vm; +using Xunit; + +public class GameSessionTests +{ + private const int T_IMM = 0, T_GINT = 3; + private static readonly OpcodeTable Table = OpcodeTableJson.Load(Paths.OpcodesJson); + + // A minimal one-instruction script: `mov (global-int dst) src`. Halts pc-out-of-range after. + private static Script MovScript(int dst, int srcType, long srcVal) => new() + { + Header = new ScriptHeader(0, 0, 0, 0, 0, 0), + Instructions = new[] + { + new Instruction(0, 0x55, new[] { new Operand(T_GINT, dst), new Operand(srcType, srcVal) }), + }, + IndexByOffset = new Dictionary { { 0, 0 } }, + Strings = new Dictionary(), + }; + + private sealed class VoiceCountHost : IHost + { + public int Voices; + public List Emitted = new(); + public void ShowText(int o, string t) => Emitted.Add(o); + public void WaitForInput() { } + public void CreateTexture(int s, int w, int h) { } + public void SetTexture(long r, int s) { } + public void DrawTexture(int s, int sx, int sy, int w, int h, int dx, int dy) { } + public (int Width, int Height) GetTextureSize(int slot) => (0, 0); + public void PlayBgm(long id) { } + public void PlayVoice(long id) => Voices++; + } + + [Fact] + public void StatePersistsAcrossScenes() + { + var session = new GameSession(); + // Scene A writes G[0x5000] = 42. + session.RunScene(MovScript(0x5000, T_IMM, 0x2a), Table, new CaptureHost()); + Assert.Equal(0x2a, session.Globals[0x5000]); + // Scene B copies G[0x5000] -> G[0x5001]; it must SEE A's write. + session.RunScene(MovScript(0x5001, T_GINT, 0x5000), Table, new CaptureHost()); + Assert.Equal(0x2a, session.Globals[0x5001]); + } + + [Fact] + public void SeedIsVisibleToTheScene() + { + var session = new GameSession(); + session.Seed(0x5000, 99); + session.RunScene(MovScript(0x5001, T_GINT, 0x5000), Table, new CaptureHost()); + Assert.Equal(99, session.Globals[0x5001]); + } + + [Fact] + public void SC0000ViaSessionMatchesSingleRun() + { + var script = Sys4Loader.Load(Paths.Scripts()["SC0000.BIN"], Table); + // single run + var host1 = new CaptureHost(); + new VirtualMachine(script, Table, host1).Run(); + var single = host1.Emitted.Select(e => e.Offset).ToList(); + // via session + var host2 = new CaptureHost(); + var r = new GameSession().RunScene(Sys4Loader.Load(Paths.Scripts()["SC0000.BIN"], Table), Table, host2); + var viaSession = r.Emitted.Select(e => e.Offset).ToList(); + Assert.Equal(single, viaSession); + Assert.Equal("exit", r.Halt); + } + + [Fact] + public void BootingSkinitPopulatesDataTableGlobals() + { + // Running the SKINIT data script through the session populates the real skill table into the + // global bank (the game's boot behavior). Cross-check vs the static extraction (build/data/SKINIT.json): + // skill 0 = "飛行" at global-string 0x23a3, field G[0xa6e5b] = 30. + var session = new GameSession(); + var r = session.RunScene(Sys4Loader.Load(Paths.Scripts()["SKINIT.BIN"], Table), Table, new CaptureHost()); + Assert.Equal("exit", r.Halt); + Assert.Equal("飛行", session.GlobalStrings[0x23a3]); + Assert.Equal(30, session.Globals[0xa6e5b]); + Assert.True(session.Globals.Count > 1000, $"SKINIT should populate the skill table (got {session.Globals.Count})"); + } + + [Fact] + public void SnapshotRoundTripsState() + { + var s = new GameSession(); + s.Seed(0x10, 42); s.Seed(0x20, -7); s.SeedString(0x30, "リリィ"); + var t = GameSession.FromJson(s.ToJson()); + Assert.Equal(42, t.Globals[0x10]); + Assert.Equal(-7, t.Globals[0x20]); + Assert.Equal("リリィ", t.GlobalStrings[0x30]); + Assert.Equal(s.Globals.Count, t.Globals.Count); + } + + [Fact] + public void BootedStateSurvivesSnapshot() + { + var s = new GameSession(); + s.RunScene(Sys4Loader.Load(Paths.Scripts()["SKINIT.BIN"], Table), Table, new CaptureHost()); + var t = GameSession.FromJson(s.ToJson()); // snapshot the expensive booted state, rebuild + Assert.Equal("飛行", t.GlobalStrings[0x23a3]); + Assert.Equal(30, t.Globals[0xa6e5b]); + Assert.Equal(s.Globals.Count, t.Globals.Count); + Assert.Equal(s.GlobalStrings.Count, t.GlobalStrings.Count); + } + + [Fact] + public void SeedingFormFlagChangesBehavior() + { + // Lily's lines are gated on form flags G[0xa57/8/9]; unseeded => all skipped (0 voices on her lines). + // Seeding form A (0xa57=1) makes her lines execute -> more play-voice calls. Proven finding (audio). + var scriptPath = Paths.Scripts()["SC0000.BIN"]; + var unseeded = new VoiceCountHost(); + new GameSession().RunScene(Sys4Loader.Load(scriptPath, Table), Table, unseeded); + + var s = new GameSession(); + s.Seed(0xa57, 1); + var seeded = new VoiceCountHost(); + s.RunScene(Sys4Loader.Load(scriptPath, Table), Table, seeded); + + Assert.True(seeded.Voices > unseeded.Voices, + $"seeding form flag should fire more voices: unseeded={unseeded.Voices} seeded={seeded.Voices}"); + } +} diff --git a/engine/Age.Engine.Tests/SleepDispatchTests.cs b/engine/Age.Engine.Tests/SleepDispatchTests.cs new file mode 100644 index 0000000..c2153a0 --- /dev/null +++ b/engine/Age.Engine.Tests/SleepDispatchTests.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using Age.Engine.Model; +using Age.Engine.Sys4; +using Age.Engine.Vm; +using Xunit; + +public class SleepDispatchTests +{ + [Fact] + public void SleepOp_ForwardsRawOperand_ToHost_AndAdvancesLikeNoop() + { + var table = OpcodeTableJson.Load(Paths.OpcodesJson); + (int, Operand[]) Sleep(long ms) => (0xc8, new[] { new Operand(0, ms) }); + (int, Operand[]) Show(int s) => (0x6e, new[] { new Operand(2, s), new Operand(0, 0) }); + (int, Operand[]) Exit() => (0x2, System.Array.Empty()); + + var script = ScriptAssembler.Assemble(table, "SLEEPTEST", + new List<(int, Operand[])> { Sleep(200), Show(0), Sleep(1000), Exit() }, new[] { "hi" }); + + var host = new RecordingHost(); + var vm = new VirtualMachine(script, table, host); + vm.Run(); + + Assert.Equal(new List { 200, 1000 }, host.SleptDurations); + Assert.Single(host.Lines); // the show-text between the sleeps still ran + } +} diff --git a/engine/Age.Engine.Tests/TestSupport.cs b/engine/Age.Engine.Tests/TestSupport.cs index 2ff20e3..5eb70e4 100644 --- a/engine/Age.Engine.Tests/TestSupport.cs +++ b/engine/Age.Engine.Tests/TestSupport.cs @@ -10,8 +10,10 @@ internal sealed class RecordingHost : IHost { public int Waits; public readonly List<(int Offset, string Text)> Lines = new(); + public readonly List SleptDurations = new(); public void ShowText(int offset, string text) => Lines.Add((offset, text)); public void WaitForInput() => Waits++; + public void Sleep(long duration) => SleptDurations.Add(duration); public void CreateTexture(int slot, int w, int h) { } public void SetTexture(long resId, int slot) { } public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy) { } diff --git a/engine/Age.Engine.Tests/TextureGeometryTests.cs b/engine/Age.Engine.Tests/TextureGeometryTests.cs index 2ee9eba..19dc63a 100644 --- a/engine/Age.Engine.Tests/TextureGeometryTests.cs +++ b/engine/Age.Engine.Tests/TextureGeometryTests.cs @@ -11,6 +11,7 @@ public class TextureGeometryTests { public void ShowText(int o, string t) { } public void WaitForInput() { } + public void Sleep(long duration) { } public void CreateTexture(int slot, int w, int h) { } public void SetTexture(long resId, int slot) { } public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy) { } diff --git a/engine/Age.Engine.Tests/TextureGeometryTests.cs.bak b/engine/Age.Engine.Tests/TextureGeometryTests.cs.bak new file mode 100644 index 0000000..2ee9eba --- /dev/null +++ b/engine/Age.Engine.Tests/TextureGeometryTests.cs.bak @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Age.Engine.Hosting; +using Age.Engine.Model; +using Age.Engine.Sys4; +using Age.Engine.Vm; +using Xunit; + +public class TextureGeometryTests +{ + private sealed class FakeSizeHost : IHost + { + public void ShowText(int o, string t) { } + public void WaitForInput() { } + public void CreateTexture(int slot, int w, int h) { } + public void SetTexture(long resId, int slot) { } + public void DrawTexture(int slot, int sx, int sy, int w, int h, int dx, int dy) { } + public void PlayBgm(long id) { } + public void PlayVoice(long id) { } + public (int Width, int Height) GetTextureSize(int slot) => (0x140, 0xC8); + } + + [Fact] + public void GetTextureSizeWritesHostDimsIntoOutputGlobals() + { + var table = OpcodeTableJson.Load(Paths.OpcodesJson); + // 0x208 (global-int 50)(global-int 60)(global-int 61): slot=50, out_w=G[60], out_h=G[61] + const int T_GINT = 3; + var ins = new Instruction(0, 0x208, new[] + { + new Operand(T_GINT, 50), new Operand(T_GINT, 60), new Operand(T_GINT, 61), + }); + var script = new Script + { + Header = new ScriptHeader(0, 0, 0, 0, 0, 0), + Instructions = new[] { ins }, + IndexByOffset = new Dictionary { { 0, 0 } }, + Strings = new Dictionary(), + }; + var vm = new VirtualMachine(script, table, new FakeSizeHost()); + vm.Run(); + Assert.Equal(0x140, vm.Globals[60]); + Assert.Equal(0xC8, vm.Globals[61]); + } +} diff --git a/engine/Age.Engine.Tests/TextureOpsTests.cs b/engine/Age.Engine.Tests/TextureOpsTests.cs index b2dbb92..ed61920 100644 --- a/engine/Age.Engine.Tests/TextureOpsTests.cs +++ b/engine/Age.Engine.Tests/TextureOpsTests.cs @@ -13,6 +13,7 @@ public class TextureOpsTests public int Creates; public void ShowText(int o, string t) { } public void WaitForInput() { } + public void Sleep(long duration) { } public void CreateTexture(int slot, int w, int h) => Creates++; public void SetTexture(long resId, int slot) => Sets.Add((resId, slot)); public void DrawTexture(int slot, int srcX, int srcY, int w, int h, int dstX, int dstY) => Draws.Add((slot, w, h)); diff --git a/engine/Age.Engine.Tests/TextureOpsTests.cs.bak b/engine/Age.Engine.Tests/TextureOpsTests.cs.bak new file mode 100644 index 0000000..b2dbb92 --- /dev/null +++ b/engine/Age.Engine.Tests/TextureOpsTests.cs.bak @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Age.Engine.Hosting; +using Age.Engine.Sys4; +using Age.Engine.Vm; +using Xunit; + +public class TextureOpsTests +{ + private sealed class RecHost : IHost + { + public List<(long resId, int slot)> Sets = new(); + public List<(int slot, int w, int h)> Draws = new(); + public int Creates; + public void ShowText(int o, string t) { } + public void WaitForInput() { } + public void CreateTexture(int slot, int w, int h) => Creates++; + public void SetTexture(long resId, int slot) => Sets.Add((resId, slot)); + public void DrawTexture(int slot, int srcX, int srcY, int w, int h, int dstX, int dstY) => Draws.Add((slot, w, h)); + public (int Width, int Height) GetTextureSize(int slot) => (0, 0); + public void PlayBgm(long id) { } + public void PlayVoice(long id) { } + } + + [Fact] + public void SC0000FiresTextureOpsWithSlot0FullScreenSlideshow() + { + var table = OpcodeTableJson.Load(Paths.OpcodesJson); + var script = Sys4Loader.Load(Paths.Scripts()["SC0000.BIN"], table); + var host = new RecHost(); + new VirtualMachine(script, table, host).Run(); + Assert.True(host.Creates > 0, "create-texture should fire"); + // The intro loads a sequence of full-screen images into slot 0; res 0x23 is the first bg. + Assert.Contains(host.Sets, s => s.resId == 0x23 && s.slot == 0); + Assert.Contains(host.Draws, d => d.slot == 0 && d.w == 0x320 && d.h == 0x258); + } +} diff --git a/engine/Age.Engine/Hosting/CaptureHost.cs b/engine/Age.Engine/Hosting/CaptureHost.cs index e7195cd..42a5c48 100644 --- a/engine/Age.Engine/Hosting/CaptureHost.cs +++ b/engine/Age.Engine/Hosting/CaptureHost.cs @@ -4,6 +4,7 @@ public sealed class CaptureHost : IHost public List<(int Offset, string Text)> Emitted { get; } = new(); public void ShowText(int offset, string text) => Emitted.Add((offset, text)); public void WaitForInput() { } + public void Sleep(long duration) { } public void CreateTexture(int slot, int width, int height) { } public void SetTexture(long resourceId, int slot) { } public void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY) { } diff --git a/engine/Age.Engine/Hosting/IHost.cs b/engine/Age.Engine/Hosting/IHost.cs index ccc04f5..55022b1 100644 --- a/engine/Age.Engine/Hosting/IHost.cs +++ b/engine/Age.Engine/Hosting/IHost.cs @@ -3,6 +3,7 @@ public interface IHost { void ShowText(int offset, string text); void WaitForInput(); + void Sleep(long duration); void CreateTexture(int slot, int width, int height); void SetTexture(long resourceId, int slot); void DrawTexture(int slot, int srcX, int srcY, int width, int height, int dstX, int dstY); diff --git a/engine/Age.Engine/Vm/VirtualMachine.cs b/engine/Age.Engine/Vm/VirtualMachine.cs index 5d761c4..e0aa0d9 100644 --- a/engine/Age.Engine/Vm/VirtualMachine.cs +++ b/engine/Age.Engine/Vm/VirtualMachine.cs @@ -208,6 +208,8 @@ public sealed class VirtualMachine } return pc + 1; case "wait-for-input": _host.WaitForInput(); return pc + 1; + case "sleep": // 0xc8 (duration) — pause the host duration ms; headless hosts no-op (parity). Frame pacing. + _host.Sleep(Read(a[0])); return pc + 1; case "end-text-line": case "set-font": case "comment": case "display-furigana": case "dev_ukn": return pc + 1;