Implement startup string and block copy opcodes

This commit is contained in:
gamer147
2026-07-21 09:23:18 -04:00
parent e86abfbf65
commit a29bd52045
8 changed files with 288 additions and 31 deletions

View File

@@ -10,11 +10,17 @@ public class NaturalBootIntegrationTests
private sealed class StopAtSc0000Sink : ITraceSink
{
public readonly List<string> Entered = new();
public bool SawStringEqualsStub;
public bool SawUnitDataCopyStub;
public Action<string>? OnEnter;
public bool TracingSteps => false;
public bool TracingSteps => true;
public void Emit(in TraceEvent e)
{
if (e.Kind == TraceEventKind.Stub && e.Opcode == 0x194)
SawStringEqualsStub = true;
if (e.Kind == TraceEventKind.Stub && e.Opcode is 0x63 or 0x1b0)
SawUnitDataCopyStub = true;
if (e.Kind == TraceEventKind.FrameEnter && e.Name != null)
{
Entered.Add(e.Name);
@@ -114,5 +120,7 @@ public class NaturalBootIntegrationTests
Assert.Equal(1, vm.Globals.GetValueOrDefault(0));
Assert.Equal(0x22, vm.Globals.GetValueOrDefault(0x699));
Assert.Equal(1, vm.Globals.GetValueOrDefault(0x6c1));
Assert.False(sink.SawStringEqualsStub);
Assert.False(sink.SawUnitDataCopyStub);
}
}

View File

@@ -0,0 +1,87 @@
using System;
using System.Collections.Generic;
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Vm;
using Xunit;
public class PointerAndBlockCopyOpsTests
{
private const int Immediate = 0, InlineString = 2, GlobalInt = 3, GlobalString = 5,
LocalInt = 9, LocalPointer = 12, LocalStringPointer = 14;
[Fact]
public void TakeAddressAndCopyDwords_PreserveLocalAndGlobalAddressDomains()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
int move = table.ByLabel("mov")!.Value;
int lookup = table.ByLabel("lookup-array")!.Value;
var script = ScriptAssembler.Assemble(table, "POINTER_BLOCK_COPY", new List<(int, Operand[])>
{
(move, new[] { new Operand(LocalInt, 0), new Operand(Immediate, 11) }),
(move, new[] { new Operand(LocalInt, 1), new Operand(Immediate, 22) }),
(move, new[] { new Operand(LocalInt, 2), new Operand(Immediate, 33) }),
(0x63, new[] { new Operand(LocalPointer, 0), new Operand(LocalInt, 0) }),
(0x63, new[] { new Operand(LocalPointer, 1), new Operand(GlobalInt, 0x700) }),
(0x1b0, new[]
{
new Operand(LocalPointer, 0), new Operand(LocalPointer, 1), new Operand(Immediate, 3),
}),
// A pointer source is aliased to its target, rather than to the pointer slot itself.
(lookup, new[]
{
new Operand(LocalPointer, 2), new Operand(GlobalInt, 0x710), new Operand(Immediate, 2),
}),
(0x63, new[] { new Operand(LocalPointer, 3), new Operand(LocalPointer, 2) }),
(0x1b0, new[]
{
new Operand(LocalPointer, 3), new Operand(GlobalInt, 0x720), new Operand(Immediate, 2),
}),
// Direct operands resolve as the starts of consecutive cell spans.
(0x1b0, new[]
{
new Operand(GlobalInt, 0x700), new Operand(LocalInt, 10), new Operand(Immediate, 3),
}),
(move, new[] { new Operand(GlobalInt, 0x730), new Operand(LocalInt, 10) }),
(move, new[] { new Operand(GlobalInt, 0x731), new Operand(LocalInt, 11) }),
(move, new[] { new Operand(GlobalInt, 0x732), new Operand(LocalInt, 12) }),
(0x2, Array.Empty<Operand>()),
}, Array.Empty<string>());
var vm = new VirtualMachine(script, table, new RecordingHost());
vm.Globals[0x712] = 44;
vm.Globals[0x713] = 55;
vm.Run();
Assert.Equal(new long[] { 11, 22, 33 }, new[]
{
vm.Globals[0x700], vm.Globals[0x701], vm.Globals[0x702],
});
Assert.Equal(new long[] { 44, 55 }, new[] { vm.Globals[0x720], vm.Globals[0x721] });
Assert.Equal(new long[] { 11, 22, 33 }, new[]
{
vm.Globals[0x730], vm.Globals[0x731], vm.Globals[0x732],
});
}
[Fact]
public void TakeAddress_SupportsTheNativeStringPointerDestination()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
int move = table.ByLabel("mov")!.Value;
var script = ScriptAssembler.Assemble(table, "STRING_ADDRESS", new List<(int, Operand[])>
{
(0x63, new[] { new Operand(LocalStringPointer, 0), new Operand(GlobalString, 0x740) }),
(move, new[] { new Operand(LocalStringPointer, 0), new Operand(InlineString, 0) }),
(0x2, Array.Empty<Operand>()),
}, new[] { "aliased" });
var vm = new VirtualMachine(script, table, new RecordingHost());
vm.GlobalStrings[0x740] = "before";
vm.Run();
Assert.Equal("aliased", vm.GlobalStrings[0x740]);
}
}

View File

@@ -0,0 +1,97 @@
using System;
using System.Collections.Generic;
using Age.Engine.Model;
using Age.Engine.Sys4;
using Age.Engine.Vm;
using Xunit;
public class StringComparisonOpsTests
{
private const int Immediate = 0, InlineString = 2, GlobalInt = 3, GlobalString = 5,
GlobalStringPointer = 8, LocalInt = 9, LocalString = 11,
LocalStringPointer = 14;
[Fact]
public void StringEquals_HandlesLiteralGlobalLocalAndPointerOperands()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
int lookup = table.ByLabel("lookup-array")!.Value;
int move = table.ByLabel("mov")!.Value;
var script = ScriptAssembler.Assemble(table, "STRING_EQUALS", new List<(int, Operand[])>
{
(move, new[] { new Operand(LocalString, 0), new Operand(InlineString, 0) }),
(lookup, new[]
{
new Operand(LocalStringPointer, 0), new Operand(GlobalString, 0x300),
new Operand(Immediate, 2),
}),
(0x194, new[]
{
new Operand(LocalInt, 0), new Operand(InlineString, 0), new Operand(InlineString, 1),
}),
(0x194, new[]
{
new Operand(LocalInt, 1), new Operand(GlobalString, 0x301), new Operand(InlineString, 0),
}),
(0x194, new[]
{
new Operand(LocalInt, 2), new Operand(LocalString, 0), new Operand(InlineString, 0),
}),
(0x194, new[]
{
new Operand(LocalInt, 3), new Operand(LocalStringPointer, 0), new Operand(InlineString, 0),
}),
(0x194, new[]
{
new Operand(LocalInt, 4), new Operand(GlobalStringPointer, 0x400),
new Operand(InlineString, 0),
}),
(move, new[] { new Operand(GlobalInt, 0x500), new Operand(LocalInt, 0) }),
(move, new[] { new Operand(GlobalInt, 0x501), new Operand(LocalInt, 1) }),
(move, new[] { new Operand(GlobalInt, 0x502), new Operand(LocalInt, 2) }),
(move, new[] { new Operand(GlobalInt, 0x503), new Operand(LocalInt, 3) }),
(move, new[] { new Operand(GlobalInt, 0x504), new Operand(LocalInt, 4) }),
(0x2, Array.Empty<Operand>()),
}, new[] { "姫狩り", "姫狩り", "姫狩り違い" });
var vm = new VirtualMachine(script, table, new RecordingHost());
vm.GlobalStrings[0x301] = "姫狩り違い";
vm.GlobalStrings[0x302] = "姫狩り";
vm.Globals[0x400] = 0x302;
vm.Run();
Assert.Equal(new long[] { 1, 0, 1, 1, 1 }, new[]
{
vm.Globals[0x500], vm.Globals[0x501], vm.Globals[0x502],
vm.Globals[0x503], vm.Globals[0x504],
});
}
[Fact]
public void StringEquals_DrivesAGameStartShapedConditionalBranch()
{
var table = OpcodeTableJson.Load(Paths.OpcodesJson);
var script = ScriptAssembler.Assemble(table, "GAMESTART_STRING_BRANCH", new List<(int, Operand[])>
{
// These offsets mirror the release idiom: compare INPUTNAME with a literal, then jcc.
(0x194, new[]
{
new Operand(LocalInt, 0), new Operand(GlobalString, 0x52d), new Operand(InlineString, 0),
}),
(0xa0, new[]
{
new Operand(LocalInt, 0), new Operand(Immediate, 19),
new Operand(Immediate, unchecked((long)0xffffffff)),
}),
(0x55, new[] { new Operand(GlobalInt, 0x600), new Operand(Immediate, 99) }),
(0x2, Array.Empty<Operand>()),
}, new[] { "?" });
var vm = new VirtualMachine(script, table, new RecordingHost());
vm.GlobalStrings[0x52d] = "?";
vm.Globals[0x600] = 1;
vm.Run();
Assert.Equal(1, vm.Globals[0x600]);
}
}

View File

@@ -289,19 +289,28 @@ public sealed class VirtualMachine
T_LINT => VmAddress.LocalInteger((int)op.Value),
T_LFLOAT => VmAddress.LocalFloat((int)op.Value),
T_LSTR => VmAddress.LocalString((int)op.Value),
T_GPTR or T_GSTRPTR => VmAddress.Global((int)Gi(Globals, (int)op.Value)),
T_LPTR => Ga(_cur.Locals.P, (int)op.Value),
T_LSTRPTR => Ga(_cur.Locals.SP, (int)op.Value),
_ => VmAddress.Global((int)op.Value),
};
private bool TryStoreAddress(Operand destination, VmAddress address)
{
switch (destination.Type)
{
case T_LPTR: _cur.Locals.P[(int)destination.Value] = address; return true;
case T_LSTRPTR: _cur.Locals.SP[(int)destination.Value] = address; return true;
case T_GPTR: case T_GSTRPTR: Globals[(int)destination.Value] = address.Address; return true;
default: return false;
}
}
private void LookupStore(Operand dst, VmAddress addr)
{
if (TryStoreAddress(dst, addr)) return;
switch (dst.Type)
{
case T_LPTR: _cur.Locals.P[(int)dst.Value] = addr; break;
case T_LSTRPTR: _cur.Locals.SP[(int)dst.Value] = addr; break;
case T_GPTR: Globals[(int)dst.Value] = addr.Address; break;
case T_GSTRPTR: Globals[(int)dst.Value] = addr.Address; break;
default:
if (IsStr(dst)) WriteStr(dst, ReadStringCell(addr));
else Write(dst, ReadIntCell(addr));
@@ -572,6 +581,9 @@ public sealed class VirtualMachine
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 "string-equals":
Write(a[0], string.Equals(ReadStr(a[1]), ReadStr(a[2]), StringComparison.Ordinal) ? 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;
@@ -585,6 +597,13 @@ public sealed class VirtualMachine
LookupStore(a[0], BaseAddr(a[1]).Offset(Read(a[2]))); return pc + 1;
case "lookup-array-2d":
LookupStore(a[0], BaseAddr(a[1]).Offset(Read(a[2]) * Read(a[3]) + Read(a[4]))); return pc + 1;
case "take-address": // 0x63: pointer destination <- underlying address of operand 2
if (!TryStoreAddress(a[0], BaseAddr(a[1])))
{
HaltReason ??= $"take-address-destination-type:{a[0].Type}";
return HALT;
}
return pc + 1;
case "copy-inline-int-array": // 0x64: count dword followed by plain file values
{
int offset = checked((int)Read(a[1]));
@@ -605,6 +624,22 @@ public sealed class VirtualMachine
WriteConsecutive(a[0], i, unchecked((int)_cur.Script.BodyDwords[offset + 1 + i]));
return pc + 1;
}
case "copy-dwords": // 0x1b0: memcpy(count * 4) across resolved integer-cell spans
{
int count = checked((int)Read(a[2]));
if (count < 0)
{
HaltReason ??= $"copy-dwords-negative-count:{count}";
return HALT;
}
VmAddress source = BaseAddr(a[0]);
VmAddress destination = BaseAddr(a[1]);
var values = new long[count];
for (int i = 0; i < count; i++)
values[i] = unchecked((int)ReadIntCell(source.Offset(i)));
for (int i = 0; i < count; i++) WriteIntCell(destination.Offset(i), values[i]);
return pc + 1;
}
case "find-hit-rectangle": // 0x12e: inclusive rectangle intersection over addressed arrays
case "u0041E940":
{