Implement root reload and title debug launcher
This commit is contained in:
183
engine/Age.Engine.Tests/DebugSceneLaunchTests.cs
Normal file
183
engine/Age.Engine.Tests/DebugSceneLaunchTests.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
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 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]
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -17,14 +17,19 @@ public class MovieOpcodeTests
|
||||
(0x130, new[] { new Operand(3, 0x100) }),
|
||||
(0x9, System.Array.Empty<Operand>()),
|
||||
}, System.Array.Empty<string>());
|
||||
var vm = new VirtualMachine(root, table, new RecordingHost());
|
||||
var reloadedRoot = ScriptAssembler.Assemble(table, "SYSTEM4", new List<(int, Operand[])>
|
||||
{
|
||||
(0x130, new[] { new Operand(3, 0x101) }),
|
||||
(0x2, System.Array.Empty<Operand>()),
|
||||
}, System.Array.Empty<string>());
|
||||
var host = new RecordingHost();
|
||||
var vm = new VirtualMachine(root, table, host,
|
||||
provider: new MapProvider(new Dictionary<long, Script> { [0] = reloadedRoot }));
|
||||
|
||||
vm.Run();
|
||||
Assert.Equal(1, vm.Globals[0x100]);
|
||||
|
||||
vm.Globals[0x100] = -1;
|
||||
vm.Run();
|
||||
Assert.Equal(0, vm.Globals[0x100]);
|
||||
Assert.Equal(0, vm.Globals[0x101]);
|
||||
Assert.Equal(1, host.SceneContextResets);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
115
engine/Age.Engine.Tests/RootReloadTests.cs
Normal file
115
engine/Age.Engine.Tests/RootReloadTests.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using Age.Engine.Diagnostics;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Vm;
|
||||
using Xunit;
|
||||
|
||||
public class RootReloadTests
|
||||
{
|
||||
private static Operand I(long value) => new(0, value);
|
||||
private static Operand G(long address) => new(3, address);
|
||||
|
||||
[Fact]
|
||||
public void ExitScriptDiscardsEveryCallerAndReloadsRawScriptZero()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var system4 = ScriptAssembler.Assemble(table, "SYSTEM4", new List<(int, Operand[])>
|
||||
{
|
||||
(0x1b6, new[] { G(0x220) }),
|
||||
(0x19a, new[] { G(0x221) }),
|
||||
(0x55, new[] { G(0x202), I(0x99) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var deepest = ScriptAssembler.Assemble(table, "DEEPEST", new List<(int, Operand[])>
|
||||
{
|
||||
(0x55, new[] { G(0x203), I(3) }),
|
||||
(0x9, Array.Empty<Operand>()),
|
||||
(0x55, new[] { G(0x204), I(4) }),
|
||||
}, Array.Empty<string>());
|
||||
var child = ScriptAssembler.Assemble(table, "CHILD", new List<(int, Operand[])>
|
||||
{
|
||||
(0x3, new[] { I(2) }),
|
||||
(0x55, new[] { G(0x205), I(5) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var initial = ScriptAssembler.Assemble(table, "INITIAL", new List<(int, Operand[])>
|
||||
{
|
||||
(0x1b7, new[] { I(1) }),
|
||||
(0x88, new[] { I(1) }),
|
||||
(0x55, new[] { G(0x200), I(1) }),
|
||||
(0x3, new[] { I(1) }),
|
||||
(0x55, new[] { G(0x201), I(2) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var provider = new MapProvider(new Dictionary<long, Script>
|
||||
{
|
||||
[0] = system4,
|
||||
[1] = child,
|
||||
[2] = deepest,
|
||||
});
|
||||
var host = new RecordingHost();
|
||||
var trace = new RecordingTraceSink();
|
||||
var vm = new VirtualMachine(initial, table, host, provider: provider, sink: trace);
|
||||
vm.Globals[0x2ff] = 0x1234;
|
||||
vm.ExternalGlobals[7] = 0x5678;
|
||||
vm.Gfx.SetSurface(5, 0x33, 0);
|
||||
vm.Gfx.BindDraw(0x100, 5, 0, 0, 1, 1, 0, 0);
|
||||
vm.TextHistory.DefineLayout(1, 10, 10, 0, 0);
|
||||
vm.TextHistory.AppendText(1, 0, "preserved", AdvTextStyle.Default);
|
||||
vm.TextHistory.SetRecordingEnabled(false);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal("exit", vm.HaltReason);
|
||||
Assert.Equal(1, vm.Globals[0x200]);
|
||||
Assert.Equal(3, vm.Globals[0x203]);
|
||||
Assert.Equal(0x99, vm.Globals[0x202]);
|
||||
Assert.False(vm.Globals.ContainsKey(0x201));
|
||||
Assert.False(vm.Globals.ContainsKey(0x204));
|
||||
Assert.False(vm.Globals.ContainsKey(0x205));
|
||||
Assert.Equal(0x1234, vm.Globals[0x2ff]);
|
||||
Assert.Equal(0x5678, vm.ExternalGlobals[7]);
|
||||
Assert.Equal(0, vm.Globals[0x220]);
|
||||
Assert.Equal(0, vm.Globals[0x221]);
|
||||
Assert.Empty(vm.Gfx.SnapshotVisibleObjects());
|
||||
Assert.Single(vm.TextHistory.Records, r => r.Text == "preserved");
|
||||
Assert.False(vm.TextHistory.RecordingSuppressed);
|
||||
Assert.Equal(1, host.SceneContextResets);
|
||||
Assert.False(host.MessageSkip);
|
||||
|
||||
var rootEnter = Assert.Single(trace.Events,
|
||||
e => e.Kind == TraceEventKind.FrameEnter && e.Name == "SYSTEM4");
|
||||
Assert.Equal(FrameCause.RootReload, rootEnter.Cause);
|
||||
Assert.Equal(3, trace.Events.Count(e =>
|
||||
e.Kind == TraceEventKind.FrameExit && e.Text == "RootReload"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HimegariRawScriptZeroIsSystem4()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var scripts = Sys4ScriptProvider.Load(table);
|
||||
|
||||
Assert.Equal("SYSTEM4.BIN", scripts.GetById(0)?.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ExitScriptResetsSceneBeforeAnUnresolvedRootLoadFails()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var script = ScriptAssembler.Assemble(table, "NO_ROOT", new List<(int, Operand[])>
|
||||
{
|
||||
(0x9, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var host = new RecordingHost();
|
||||
var vm = new VirtualMachine(script, table, host);
|
||||
vm.Gfx.SetSurface(5, 0x33, 0);
|
||||
vm.Gfx.BindDraw(0x100, 5, 0, 0, 1, 1, 0, 0);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal("root-reload-unresolved:0x0", vm.HaltReason);
|
||||
Assert.Equal(1, host.SceneContextResets);
|
||||
Assert.Empty(vm.Gfx.SnapshotVisibleObjects());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Diagnostics;
|
||||
using Xunit;
|
||||
|
||||
public class Sys4ScriptProviderTests
|
||||
@@ -46,4 +47,44 @@ public class Sys4ScriptProviderTests
|
||||
Assert.Null(provider.GetByName("../FIELD.BIN"));
|
||||
Assert.Equal(481, provider.ScriptNames.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DebugCatalogPreservesPackedIdentityAcrossBaseAndAppendPacks()
|
||||
{
|
||||
var catalog = Sys4AssetCatalog.Load(Paths.Sys4Ini);
|
||||
var entries = DebugSceneCatalog.Build(catalog);
|
||||
|
||||
Assert.Equal(catalog.EnumerateScripts().Count, entries.Count);
|
||||
Assert.Equal(481, entries.Count(entry => entry.PackId == 0));
|
||||
Assert.Contains(entries, entry => entry.Name == "$1$SC1260.BIN"
|
||||
&& entry.PackedId == (0x01000000L | (uint)entry.RawIndex)
|
||||
&& entry.Kind == DebugScriptKind.Scenario);
|
||||
Assert.All(entries, entry => Assert.Same(
|
||||
catalog.ResolvePacked(entry.PackedId),
|
||||
catalog.AppendPacks.GetValueOrDefault(entry.PackId, catalog).ResolveRaw(entry.RawIndex)));
|
||||
Assert.Equal(entries.Count, entries.Select(entry => entry.PackedId).Distinct().Count());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DebugCatalogFiltersByProfileCategoryNameAndPackedId()
|
||||
{
|
||||
var entries = DebugSceneCatalog.Build(Sys4AssetCatalog.Load(Paths.Sys4Ini));
|
||||
|
||||
var scenarios = DebugSceneCatalog.Filter(entries, DebugScriptFilter.Scenario, "");
|
||||
Assert.NotEmpty(scenarios);
|
||||
Assert.All(scenarios, entry => Assert.Equal(DebugScriptKind.Scenario, entry.Kind));
|
||||
Assert.Contains(scenarios, entry => entry.Name.Equals("SC0000.BIN", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
var debug = DebugSceneCatalog.Filter(entries, DebugScriptFilter.Debug, "DEBUG.BIN");
|
||||
var exact = Assert.Single(debug);
|
||||
Assert.Equal("DEBUG.BIN", exact.Name);
|
||||
Assert.True(exact.Launchable);
|
||||
Assert.Equal(exact, Assert.Single(DebugSceneCatalog.Filter(entries, DebugScriptFilter.All,
|
||||
$"0x{exact.PackedId:x}")));
|
||||
Assert.Equal(exact, Assert.Single(DebugSceneCatalog.Filter(entries, DebugScriptFilter.All,
|
||||
exact.PackedId.ToString())));
|
||||
|
||||
Assert.False(entries.Single(entry => entry.Name == "SYSTEM4.BIN").Launchable);
|
||||
Assert.False(entries.Single(entry => entry.Name == "TITLE.BIN").Launchable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ internal class RecordingHost : IHost
|
||||
public readonly List<long> CursorResources = new();
|
||||
public readonly List<bool> AdvPagePresentationSuspended = new();
|
||||
public int CursorClearCount;
|
||||
public int SceneContextResets;
|
||||
public void ShowText(int offset, string text) => Lines.Add((offset, text));
|
||||
public void SetAdvTextCursor(int layoutSlot, int x, int y) => TextCursors.Add((layoutSlot, x, y));
|
||||
public void DrawStringToSurface(int surfaceSlot, int x, int y, string text)
|
||||
@@ -88,6 +89,7 @@ internal class RecordingHost : IHost
|
||||
public void ClearCursorResource() => CursorClearCount++;
|
||||
public virtual void Sleep(long duration) => SleptDurations.Add(duration);
|
||||
public virtual void FrameYield() { }
|
||||
public void ResetSceneContext() => SceneContextResets++;
|
||||
public bool IsMessageSkipActive => MessageSkip;
|
||||
public void SetMessageSkipActive(bool active)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user