using System.Text;
using Age.Engine.Model;
namespace Age.Engine.Sys4;
/// Assembles a SYS4 script from instructions + strings — the inverse of .
/// 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.
///
/// A string operand is written as new Operand(2, stringIndex); 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 ).
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 strings)
=> Sys4Loader.Parse(AssembleBytes(instrs, strings), table, name);
public static byte[] AssembleBytes(IReadOnlyList<(int Op, Operand[] Args)> instrs, IReadOnlyList strings)
{
int codeLen = 0;
foreach (var ins in instrs) codeLen += 1 + 2 * ins.Args.Length;
var messageOffsets = new List();
int instructionOffset = 0;
foreach (var ins in instrs)
{
if (ins.Op == 0x71) messageOffsets.Add((uint)instructionOffset);
instructionOffset += 1 + 2 * ins.Args.Length;
}
// Encode strings; record each string's starting dword offset (relative to body start).
var strDwords = new List();
var strOffset = new int[strings.Count];
for (int i = 0; i < strings.Count; i++)
{
strOffset[i] = codeLen + messageOffsets.Count + strDwords.Count;
strDwords.AddRange(EncodeString(strings[i]));
}
var body = new List(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(messageOffsets);
body.AddRange(strDwords);
var fields = new int[NumFields];
fields[7] = messageOffsets.Count; // F7 = T1/read-message boundary count
fields[8] = codeLen; // F8 = code end / T1 table start
fields[10] = codeLen + messageOffsets.Count;
fields[12] = codeLen + messageOffsets.Count;
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 EncodeString(string text)
{
var raw = new List(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;
}
}
}