From 0cb83fcb62343061e8cf1064e448b11d28b84938 Mon Sep 17 00:00:00 2001 From: gamer147 Date: Mon, 6 Jul 2026 13:51:32 -0400 Subject: [PATCH] feat(a1): SYS4 container parser (header + code decode + data_array_end shrink) Co-Authored-By: Claude Opus 4.8 (1M context) --- engine/Age.Engine.Tests/Sys4LoaderTests.cs | 22 +++++++ engine/Age.Engine/Sys4/Sys4Loader.cs | 69 ++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 engine/Age.Engine.Tests/Sys4LoaderTests.cs create mode 100644 engine/Age.Engine/Sys4/Sys4Loader.cs diff --git a/engine/Age.Engine.Tests/Sys4LoaderTests.cs b/engine/Age.Engine.Tests/Sys4LoaderTests.cs new file mode 100644 index 0000000..d65bc02 --- /dev/null +++ b/engine/Age.Engine.Tests/Sys4LoaderTests.cs @@ -0,0 +1,22 @@ +using Age.Engine.Sys4; +using Xunit; + +public class Sys4LoaderTests +{ + [Fact] + public void ParsesMenuBinLikeSys4load() + { + var table = OpcodeTableJson.Load(Paths.OpcodesJson); + var s = Sys4Loader.Load(Path.Combine(Paths.Data1, "MENU.BIN"), table); + // header F0..F5 from `sys4load MENU.BIN --summary` + Assert.Equal(0x17, s.Header.LocalInt1); + Assert.Equal(1, s.Header.LocalFloats); + Assert.Equal(1, s.Header.LocalStrings1); + Assert.Equal(2, s.Header.LocalInt2); + Assert.Equal(1, s.Header.Unknown); + Assert.Equal(1, s.Header.LocalStrings2); + Assert.Equal(148, s.Instructions.Count); // sys4load: 148 instructions + Assert.Equal(3, s.Strings.Count); // sys4load: 3 inline strings + Assert.All(s.Instructions, ins => Assert.True(s.IndexByOffset.ContainsKey(ins.Offset))); + } +} diff --git a/engine/Age.Engine/Sys4/Sys4Loader.cs b/engine/Age.Engine/Sys4/Sys4Loader.cs new file mode 100644 index 0000000..ac41fb4 --- /dev/null +++ b/engine/Age.Engine/Sys4/Sys4Loader.cs @@ -0,0 +1,69 @@ +using Age.Engine.Model; +namespace Age.Engine.Sys4; +public static class Sys4Loader +{ + private const int HeaderSize = 0x3C, BodyOff = 0x3C, NumFields = 13, ArrayOpcode = 0x64; + + public static Script Load(string path, OpcodeTable table) + => Parse(File.ReadAllBytes(path), table, Path.GetFileName(path)); + + public static Script Parse(byte[] data, OpcodeTable table, string name = "") + { + if (data.Length < HeaderSize) throw new InvalidDataException($"{name}: too small"); + if (!(data[0] == (byte)'S' && data[1] == (byte)'Y' && data[2] == (byte)'S' && data[3] == (byte)'4')) + throw new InvalidDataException($"{name}: bad magic"); + if (data.Length % 4 != 0) throw new InvalidDataException($"{name}: not dword-aligned"); + + var fields = new int[NumFields]; + for (int k = 0; k < NumFields; k++) fields[k] = BitConverter.ToInt32(data, 8 + k * 4); + int nbody = (data.Length - BodyOff) / 4; + var dw = new uint[nbody]; + for (int k = 0; k < nbody; k++) dw[k] = BitConverter.ToUInt32(data, BodyOff + k * 4); + + var header = new ScriptHeader(fields[0], fields[1], fields[2], fields[3], fields[4], fields[5]); + var (instrs, idxByOff, strings) = DecodeCode(dw, fields, nbody, table); + return new Script { Header = header, Instructions = instrs, IndexByOffset = idxByOff, Strings = strings }; + } + + private static (List, Dictionary, Dictionary) + DecodeCode(uint[] dw, int[] fields, int nbody, OpcodeTable table) + { + int codeEnd = fields[8]; // F8; shrinks to first inline string/array offset + var instrs = new List(); + var idx = new Dictionary(); + var strings = new Dictionary(); + int i = 0; + while (i < codeEnd) + { + int op = (int)dw[i]; + int argc = table.Argc(op); + if (argc < 0) { idx[i] = instrs.Count; instrs.Add(new Instruction(i, op, Array.Empty())); break; } + int baseI = i + 1; + if (baseI + 2 * argc > codeEnd) { idx[i] = instrs.Count; instrs.Add(new Instruction(i, op, Array.Empty())); break; } + var args = new Operand[argc]; + for (int a = 0; a < argc; a++) + { + int atype = (int)dw[baseI + 2 * a]; + long aval = dw[baseI + 2 * a + 1]; + args[a] = new Operand(atype, aval); + if (atype == 2 && aval >= 0 && aval < nbody) + { + if (aval < codeEnd) codeEnd = (int)aval; + if (!strings.ContainsKey((int)aval)) + { + var (text, _) = Sys4StringCodec.Decode(dw, (int)aval); + if (text != null) strings[(int)aval] = text; + } + } + else if (op == ArrayOpcode && a == 1 && aval >= 0 && aval < nbody) + { + if (aval < codeEnd) codeEnd = (int)aval; + } + } + idx[i] = instrs.Count; + instrs.Add(new Instruction(i, op, args)); + i = baseI + 2 * argc; + } + return (instrs, idx, strings); + } +}