Files
OpenMaidEngine/engine/Age.Engine.Tests/DebugSceneLaunchTests.cs
2026-08-15 10:30:32 -04:00

232 lines
9.4 KiB
C#

using Age.Engine.Diagnostics;
using Age.Engine.Hosting;
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Vm;
using Xunit;
public class DebugSceneLaunchTests
{
private sealed class ReachedDebugMapPage8ContinuationException : Exception { }
private sealed class LaunchDebugMapAndStopAfterPage8Sink : ITraceSink
{
public VirtualMachine Vm = null!;
public bool TracingSteps => true;
public void Emit(in TraceEvent e)
{
if (e.Kind == TraceEventKind.FrameEnter
&& e.Name?.Equals("TITLE.BIN", StringComparison.OrdinalIgnoreCase) == true)
{
DebugFrameSnapshot frame = Assert.IsType<DebugFrameSnapshot>(Vm.DebugFrame);
Assert.True(Vm.TryRequestDebugFrameReturn(frame.FrameId, new Dictionary<int, long>
{
[0] = 1,
[0xaba5c] = -1,
[0x62ccf] = 0,
[0x699] = 0x338c,
}));
}
if (e.Kind == TraceEventKind.Step
&& e.Ins?.Offset == 0x5b25
&& Vm.DebugFrame?.CurrentScript.Equals(
"SC0270.BIN", StringComparison.OrdinalIgnoreCase) == true)
throw new ReachedDebugMapPage8ContinuationException();
}
}
private static Operand I(long value) => new(0, value);
private static Operand G(long address) => new(3, address);
private static (int, Operand[]) Call(Operand id) => (0x3, new[] { id });
private static (int, Operand[]) Mov(int address, long value) => (0x55, new[] { G(address), I(value) });
private static (int, Operand[]) Wait() => (0x72, new[] { I(0) });
private static (int, Operand[]) Sleep() => (0xc8, new[] { I(1) });
private static (int, Operand[]) Exit() => (0x2, Array.Empty<Operand>());
[Fact]
[Trait("Category", "Workspace")]
public void RealDebugMapLaunchContinuesPastSc0270Page8Wait()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var scripts = Sys4ScriptProvider.Load(table);
var sink = new LaunchDebugMapAndStopAfterPage8Sink();
var vm = new VirtualMachine(
scripts.RequireByName("SYSTEM4.BIN"), table, new CaptureHost(),
new VmOptions(MaxSteps: 1_000_000), scripts, sink);
sink.Vm = vm;
Exception? exception = Record.Exception(() => vm.Run());
Assert.IsType<ReachedDebugMapPage8ContinuationException>(exception);
Assert.Null(vm.HaltReason);
}
[Fact]
public async Task ParkedTitleFrameReturnsToCoordinatorWhichDispatchesSelectedScript()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var root = ScriptAssembler.Assemble(table, "SYSTEM4.BIN", new List<(int, Operand[])>
{
Call(I(1)), Call(G(0x699)), Mov(0x7102, 1), Exit(),
}, Array.Empty<string>());
var title = ScriptAssembler.Assemble(table, "TITLE.BIN", new List<(int, Operand[])>
{
Wait(), Mov(0x7100, 1), Exit(),
}, Array.Empty<string>());
var selected = ScriptAssembler.Assemble(table, "DEBUG.BIN", new List<(int, Operand[])>
{
Mov(0x7101, 1), Exit(),
}, Array.Empty<string>());
var host = new BlockingWaitHost();
var trace = new RecordingTraceSink();
var vm = new VirtualMachine(root, table, host, provider: new MapProvider(new()
{
[1] = title,
[2] = selected,
}), sink: trace);
Assert.False(vm.TryRequestDebugFrameReturn(0, new Dictionary<int, long>()));
Task run = Task.Run(() => vm.Run());
await host.WaitEntered.Task.WaitAsync(TimeSpan.FromSeconds(5));
var parked = Assert.IsType<DebugFrameSnapshot>(vm.DebugFrame);
Assert.Equal("TITLE.BIN", parked.CurrentScript);
Assert.Equal(new[] { "SYSTEM4.BIN", "TITLE.BIN" }, parked.CallStack);
Assert.True(vm.TryRequestDebugFrameReturn(parked.FrameId, new Dictionary<int, long>
{
[0] = 1,
[0xaba5c] = -1,
[0x62ccf] = 0,
[0x699] = 2,
}));
Assert.False(vm.TryRequestDebugFrameReturn(parked.FrameId, new Dictionary<int, long>()));
host.SignalInput();
await run.WaitAsync(TimeSpan.FromSeconds(5));
Assert.Equal(0, vm.Globals.GetValueOrDefault(0x7100));
Assert.Equal(1, vm.Globals[0x7101]);
Assert.Equal(1, vm.Globals[0x7102]);
Assert.Null(vm.DebugFrame);
Assert.Contains(trace.Events, e => e.Kind == TraceEventKind.FrameExit
&& e.Name == "TITLE.BIN" && e.Text == "DebugReturned");
Assert.Contains(trace.Events, e => e.Kind == TraceEventKind.FrameEnter
&& e.Name == "DEBUG.BIN" && e.Cause == FrameCause.CallScript);
}
[Fact]
public async Task SelectedScriptExitScriptStillPerformsWholeStackRootReload()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var initialRoot = ScriptAssembler.Assemble(table, "SYSTEM4.BIN", new List<(int, Operand[])>
{
Call(I(1)), Call(G(0x699)), Mov(0x7200, 1), Exit(),
}, Array.Empty<string>());
var title = ScriptAssembler.Assemble(table, "TITLE.BIN", new List<(int, Operand[])>
{
Wait(), Exit(),
}, Array.Empty<string>());
var selected = ScriptAssembler.Assemble(table, "DEBUG.BIN", new List<(int, Operand[])>
{
(0x9, Array.Empty<Operand>()),
}, Array.Empty<string>());
var reloadedRoot = ScriptAssembler.Assemble(table, "SYSTEM4.BIN", new List<(int, Operand[])>
{
Mov(0x7201, 1), Exit(),
}, Array.Empty<string>());
var host = new BlockingWaitHost();
var trace = new RecordingTraceSink();
var vm = new VirtualMachine(initialRoot, table, host, provider: new MapProvider(new()
{
[0] = reloadedRoot,
[1] = title,
[2] = selected,
}), sink: trace);
Task run = Task.Run(() => vm.Run());
await host.WaitEntered.Task.WaitAsync(TimeSpan.FromSeconds(5));
var frame = Assert.IsType<DebugFrameSnapshot>(vm.DebugFrame);
Assert.True(vm.TryRequestDebugFrameReturn(frame.FrameId,
new Dictionary<int, long> { [0x699] = 2 }));
host.SignalInput();
await run.WaitAsync(TimeSpan.FromSeconds(5));
Assert.Equal(0, vm.Globals.GetValueOrDefault(0x7200));
Assert.Equal(1, vm.Globals[0x7201]);
Assert.Equal(1, host.SceneContextResets);
Assert.Contains(trace.Events, e => e.Kind == TraceEventKind.FrameEnter
&& e.Name == "SYSTEM4.BIN" && e.Cause == FrameCause.RootReload);
}
[Fact]
public async Task PollingTitleReturnsAtOpcodeBoundaryWithoutAdvInputWait()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var root = ScriptAssembler.Assemble(table, "SYSTEM4.BIN", new List<(int, Operand[])>
{
Call(I(1)), Call(G(0x699)), Mov(0x7302, 1), Exit(),
}, Array.Empty<string>());
var title = ScriptAssembler.Assemble(table, "TITLE.BIN", new List<(int, Operand[])>
{
Sleep(), Mov(0x7300, 1), Exit(),
}, Array.Empty<string>());
var selected = ScriptAssembler.Assemble(table, "DEBUG.BIN", new List<(int, Operand[])>
{
Mov(0x7301, 1), Exit(),
}, Array.Empty<string>());
var host = new BlockingSleepHost();
var vm = new VirtualMachine(root, table, host, provider: new MapProvider(new()
{
[1] = title,
[2] = selected,
}));
Task run = Task.Run(() => vm.Run());
await host.SleepEntered.Task.WaitAsync(TimeSpan.FromSeconds(5));
var frame = Assert.IsType<DebugFrameSnapshot>(vm.DebugFrame);
Assert.Equal(new[] { "SYSTEM4.BIN", "TITLE.BIN" }, frame.CallStack);
Assert.True(vm.TryRequestDebugFrameReturn(frame.FrameId,
new Dictionary<int, long> { [0x699] = 2 }));
host.CompleteSleep();
await run.WaitAsync(TimeSpan.FromSeconds(5));
Assert.Equal(0, vm.Globals.GetValueOrDefault(0x7300));
Assert.Equal(1, vm.Globals[0x7301]);
Assert.Equal(1, vm.Globals[0x7302]);
}
private sealed class BlockingWaitHost : RecordingHost
{
private readonly SemaphoreSlim _gate = new(0, 1);
public TaskCompletionSource WaitEntered { get; } =
new(TaskCreationOptions.RunContinuationsAsynchronously);
public override void WaitForInput(int layoutSlot, Func<bool> serviceInputCallback,
Func<AdvAutoWaitState> autoWaitState)
{
Waits++;
WaitEntered.TrySetResult();
if (!_gate.Wait(TimeSpan.FromSeconds(5))) throw new TimeoutException("test input wait timed out");
}
public void SignalInput() => _gate.Release();
}
private sealed class BlockingSleepHost : RecordingHost
{
private readonly SemaphoreSlim _gate = new(0, 1);
public TaskCompletionSource SleepEntered { get; } =
new(TaskCreationOptions.RunContinuationsAsynchronously);
public override void Sleep(long duration)
{
SleptDurations.Add(duration);
SleepEntered.TrySetResult();
if (!_gate.Wait(TimeSpan.FromSeconds(5))) throw new TimeoutException("test sleep timed out");
}
public void CompleteSleep() => _gate.Release();
}
}