Test on synthesized scenes with full handling, not crippled real scenes
- ScriptAssembler (Age.Engine/Sys4): assemble code+strings -> Script (inverse of Sys4Loader; also Phase-D modding-assembler groundwork). - SyntheticSceneTests: deterministic show-text/wait/nested-call-script/shared-global scene run with call-script handling ON. - WaitForInputTests: reworked onto a synthesized two-page scene (was: SC0000 stub=186). - RecoverTests: full call-script handling via a no-op subroutine double (isolates RECOVER's ISA semantics from real subroutines' game-state deps). - Retire TraceDiffTests: it matched the C# VM to vm0.py's stubbed-call-script trace; vm0.py is retired from oracle duty, and we no longer gate handling to keep it matching. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
74
engine/Age.Engine/Sys4/ScriptAssembler.cs
Normal file
74
engine/Age.Engine/Sys4/ScriptAssembler.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using System.Text;
|
||||
using Age.Engine.Model;
|
||||
namespace Age.Engine.Sys4;
|
||||
|
||||
/// <summary>Assembles a SYS4 script from instructions + strings — the inverse of <see cref="Sys4Loader"/>.
|
||||
/// Used to synthesize deterministic test scenes (so regression tests exercise real op handling instead
|
||||
/// of running a real scene in a crippled mode) and as groundwork for the Phase-D modding assembler.
|
||||
///
|
||||
/// <para>A string operand is written as <c>new Operand(2, stringIndex)</c>; the assembler lays strings
|
||||
/// out immediately after the code and patches each such operand's value to the string's dword offset,
|
||||
/// exactly as the compiler does. Strings are cp932, null-terminated, stored XOR-0xFFFFFFFF per dword
|
||||
/// (matching <see cref="Sys4StringCodec"/>).</para></summary>
|
||||
public static class ScriptAssembler
|
||||
{
|
||||
private const int HeaderSize = 0x3C, NumFields = 13, StringArgType = 2;
|
||||
private static readonly Encoding Cp932;
|
||||
static ScriptAssembler()
|
||||
{
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
Cp932 = Encoding.GetEncoding(932);
|
||||
}
|
||||
|
||||
public static Script Assemble(OpcodeTable table, string name,
|
||||
IReadOnlyList<(int Op, Operand[] Args)> instrs, IReadOnlyList<string> strings)
|
||||
=> Sys4Loader.Parse(AssembleBytes(instrs, strings), table, name);
|
||||
|
||||
public static byte[] AssembleBytes(IReadOnlyList<(int Op, Operand[] Args)> instrs, IReadOnlyList<string> strings)
|
||||
{
|
||||
int codeLen = 0;
|
||||
foreach (var ins in instrs) codeLen += 1 + 2 * ins.Args.Length;
|
||||
|
||||
// Encode strings; record each string's starting dword offset (relative to body start).
|
||||
var strDwords = new List<uint>();
|
||||
var strOffset = new int[strings.Count];
|
||||
for (int i = 0; i < strings.Count; i++)
|
||||
{
|
||||
strOffset[i] = codeLen + strDwords.Count;
|
||||
strDwords.AddRange(EncodeString(strings[i]));
|
||||
}
|
||||
|
||||
var body = new List<uint>(codeLen + strDwords.Count);
|
||||
foreach (var ins in instrs)
|
||||
{
|
||||
body.Add((uint)ins.Op);
|
||||
foreach (var arg in ins.Args)
|
||||
{
|
||||
long val = arg.Type == StringArgType ? strOffset[(int)arg.Value] : arg.Value;
|
||||
body.Add((uint)arg.Type);
|
||||
body.Add((uint)val);
|
||||
}
|
||||
}
|
||||
body.AddRange(strDwords);
|
||||
|
||||
var fields = new int[NumFields];
|
||||
fields[8] = codeLen; // F8 = code end (strings begin here)
|
||||
|
||||
var bytes = new byte[HeaderSize + body.Count * 4];
|
||||
Encoding.ASCII.GetBytes("SYS4422 ").CopyTo(bytes, 0);
|
||||
for (int k = 0; k < NumFields; k++) BitConverter.GetBytes(fields[k]).CopyTo(bytes, 8 + k * 4);
|
||||
for (int i = 0; i < body.Count; i++) BitConverter.GetBytes(body[i]).CopyTo(bytes, HeaderSize + i * 4);
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static IEnumerable<uint> EncodeString(string text)
|
||||
{
|
||||
var raw = new List<byte>(Cp932.GetBytes(text)) { 0 }; // null terminator
|
||||
while (raw.Count % 4 != 0) raw.Add(0);
|
||||
for (int i = 0; i < raw.Count; i += 4)
|
||||
{
|
||||
uint le = (uint)(raw[i] | raw[i + 1] << 8 | raw[i + 2] << 16 | raw[i + 3] << 24);
|
||||
yield return le ^ 0xFFFFFFFFu;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user