Implement mounted append AUTORUN boot
This commit is contained in:
86
engine/Age.Engine.Tests/AppendAutorunTests.cs
Normal file
86
engine/Age.Engine.Tests/AppendAutorunTests.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using Age.Engine.Hosting;
|
||||
using Age.Engine.Model;
|
||||
using Age.Engine.Sys4;
|
||||
using Age.Engine.Vm;
|
||||
|
||||
public class AppendAutorunTests
|
||||
{
|
||||
private static Operand I(long value) => new(0, value);
|
||||
private static Operand G(long address) => new(3, address);
|
||||
|
||||
private sealed class MountedProvider : IScriptProvider
|
||||
{
|
||||
private readonly IReadOnlyDictionary<long, Script> _scripts;
|
||||
|
||||
public MountedProvider(IReadOnlyList<int> selectors, IReadOnlyDictionary<long, Script> scripts)
|
||||
{
|
||||
MountedAppendSelectors = selectors;
|
||||
_scripts = scripts;
|
||||
}
|
||||
|
||||
public IReadOnlyList<int> MountedAppendSelectors { get; }
|
||||
public List<long> Requests { get; } = new();
|
||||
|
||||
public Script? GetById(long id)
|
||||
{
|
||||
Requests.Add(id);
|
||||
return _scripts.GetValueOrDefault(id);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RunMountedAppendAutoruns_ExecutesRecordZeroSeriallyBySelectorThenResumesCaller()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var first = ScriptAssembler.Assemble(table, "APPEND_ONE_AUTORUN", new List<(int, Operand[])>
|
||||
{
|
||||
(0x55, new[] { G(0x100), I(7) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var second = ScriptAssembler.Assemble(table, "APPEND_TWO_AUTORUN", new List<(int, Operand[])>
|
||||
{
|
||||
(0x50, new[] { G(0x100), G(0x100), I(5) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var root = ScriptAssembler.Assemble(table, "ROOT", new List<(int, Operand[])>
|
||||
{
|
||||
(0x143, Array.Empty<Operand>()),
|
||||
(0x55, new[] { G(0x101), G(0x100) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var provider = new MountedProvider(new[] { 2, 1, 2 }, new Dictionary<long, Script>
|
||||
{
|
||||
[0x01000000] = first,
|
||||
[0x02000000] = second,
|
||||
});
|
||||
var vm = new VirtualMachine(root, table, new RecordingHost(), provider: provider);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(new[] { 0x01000000L, 0x02000000L }, provider.Requests);
|
||||
Assert.Equal(12, vm.Globals.GetValueOrDefault(0x100));
|
||||
Assert.Equal(12, vm.Globals.GetValueOrDefault(0x101));
|
||||
Assert.Equal(2, vm.CallScriptDispatches);
|
||||
Assert.Equal("exit", vm.HaltReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RunMountedAppendAutoruns_HaltsWhenMountedRecordZeroIsNotAScript()
|
||||
{
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var root = ScriptAssembler.Assemble(table, "ROOT", new List<(int, Operand[])>
|
||||
{
|
||||
(0x143, Array.Empty<Operand>()),
|
||||
(0x55, new[] { G(0x100), I(1) }),
|
||||
(0x2, Array.Empty<Operand>()),
|
||||
}, Array.Empty<string>());
|
||||
var provider = new MountedProvider(new[] { 1 }, new Dictionary<long, Script>());
|
||||
var vm = new VirtualMachine(root, table, new RecordingHost(), provider: provider);
|
||||
|
||||
vm.Run();
|
||||
|
||||
Assert.Equal(new[] { 0x01000000L }, provider.Requests);
|
||||
Assert.Equal(0, vm.Globals.GetValueOrDefault(0x100));
|
||||
Assert.Equal("append-autorun-unresolved:0x1000000", vm.HaltReason);
|
||||
}
|
||||
}
|
||||
@@ -155,6 +155,16 @@ public class NaturalBootIntegrationTests
|
||||
Assert.Contains("UNITECH.BIN", sink.Entered);
|
||||
Assert.Contains("CALCARR.BIN", sink.Entered);
|
||||
Assert.Equal("SC0000.BIN", sink.Entered[^1]);
|
||||
int baseBtanInit2 = sink.Entered.IndexOf("BTANINIT2.BIN");
|
||||
int appendAutorun = sink.Entered.IndexOf("$1$AUTORUN.BIN");
|
||||
int appendEbInit = sink.Entered.IndexOf("$1$EBINIT.BIN");
|
||||
int tune = sink.Entered.IndexOf("TUNE.BIN");
|
||||
Assert.Equal(new[] { 1 }, boot.Scripts.MountedAppendSelectors);
|
||||
Assert.True(baseBtanInit2 >= 0 && baseBtanInit2 < appendAutorun
|
||||
&& appendAutorun < appendEbInit && appendEbInit < tune,
|
||||
$"entered={string.Join(",", sink.Entered)}");
|
||||
Assert.Equal(40, vm.Globals.GetValueOrDefault(0x7a37e + 81)); // append unit starting level
|
||||
Assert.Equal(0x0100002d, vm.Globals.GetValueOrDefault(0x6fb86 + 81)); // packed battle sprite
|
||||
Assert.Equal(1, vm.Globals.GetValueOrDefault(0));
|
||||
Assert.Equal(0x22, vm.Globals.GetValueOrDefault(0x699));
|
||||
Assert.Equal(1, vm.Globals.GetValueOrDefault(0x6c1));
|
||||
|
||||
@@ -25,6 +25,7 @@ public class Sys4ScriptProviderTests
|
||||
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
|
||||
var provider = Sys4ScriptProvider.Load(table);
|
||||
var append = provider.Catalog.AppendPacks[1];
|
||||
Assert.Equal(new[] { 1 }, provider.MountedAppendSelectors);
|
||||
var entry = append.Files.Single(e => e.Name == "$1$SC1260.BIN");
|
||||
long packedId = 0x01000000L | (uint)entry.RawIndex;
|
||||
|
||||
|
||||
@@ -7,4 +7,8 @@ public interface IScriptProvider
|
||||
{
|
||||
/// <summary>The script for this id, or null if the id maps to no known script.</summary>
|
||||
Script? GetById(long id);
|
||||
|
||||
/// <summary>Selectors of currently mounted append catalogs. Opcode 0x143 scans these in
|
||||
/// ascending order and executes record zero from each catalog.</summary>
|
||||
IReadOnlyList<int> MountedAppendSelectors => Array.Empty<int>();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ public sealed class Sys4ScriptProvider : IScriptProvider
|
||||
|
||||
public Sys4AssetCatalog Catalog { get; }
|
||||
public IReadOnlyList<string> ScriptNames => Catalog.ScriptNames;
|
||||
public IReadOnlyList<int> MountedAppendSelectors => Catalog.AppendPacks.Keys.ToArray();
|
||||
|
||||
public Sys4ScriptProvider(OpcodeTable table, Sys4AssetCatalog catalog, IAssetStore store)
|
||||
{ _table = table; Catalog = catalog; _store = store; }
|
||||
|
||||
@@ -1060,6 +1060,45 @@ public sealed class VirtualMachine
|
||||
if (outcome == FrameOutcome.ExitRequested) throw new ProcessExitRequestedException();
|
||||
return pc + 1; // Returned / RanOff: resume caller
|
||||
}
|
||||
case "u00415FB0":
|
||||
case "run-mounted-append-autoruns": // 0x143: selector slots 1..255, packed record zero
|
||||
{
|
||||
if (_provider == null) return pc + 1;
|
||||
|
||||
// Native first scans every mounted selector into its launch queue, then dispatches
|
||||
// those packed scripts serially. Snapshot before running any child so script-side
|
||||
// effects cannot change the current batch.
|
||||
int[] selectors = _provider.MountedAppendSelectors
|
||||
.Where(selector => selector is > 0 and <= 0xff)
|
||||
.Distinct()
|
||||
.Order()
|
||||
.ToArray();
|
||||
foreach (int selector in selectors)
|
||||
{
|
||||
if (_depth >= _o.CallDepthCap)
|
||||
{
|
||||
HaltReason ??= "call-depth-exceeded";
|
||||
return HALT;
|
||||
}
|
||||
|
||||
long id = (long)selector << 24;
|
||||
CallScriptDispatches++;
|
||||
var child = _provider.GetById(id);
|
||||
_sink.Emit(TraceEvent.CallScript(id, child?.Name));
|
||||
if (child == null)
|
||||
{
|
||||
HaltReason ??= $"append-autorun-unresolved:0x{id:x}";
|
||||
return HALT;
|
||||
}
|
||||
|
||||
int entry = child.IndexByOffset.TryGetValue(0, out int childEntry) ? childEntry : 0;
|
||||
var outcome = RunFrame(new ExecFrame(child, entry), FrameCause.CallScript, id);
|
||||
if (outcome == FrameOutcome.Halted) return HALT;
|
||||
if (outcome == FrameOutcome.RootReload) return ROOT_RELOAD;
|
||||
if (outcome == FrameOutcome.ExitRequested) throw new ProcessExitRequestedException();
|
||||
}
|
||||
return pc + 1;
|
||||
}
|
||||
case "u00417E80":
|
||||
case "preload-script-slot": // 0x06 (script_id, frame_slot), valid slots 0..39
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user