diff --git a/src/nutcracker/sputm/windex/compiler.py b/src/nutcracker/sputm/windex/compiler.py new file mode 100644 index 0000000..2efa572 --- /dev/null +++ b/src/nutcracker/sputm/windex/compiler.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import re +from pathlib import Path + +from nutcracker.sputm.script.opcodes_v5 import Variable + +BREAK_OPCODE = 0x80 +INCREMENT_OPCODE = 0x46 +DECREMENT_OPCODE = 0xC6 + +_VAR_RE = re.compile(r'([VLB])\.(\d+)') + + +def parse_variable(token: str) -> Variable: + match = _VAR_RE.fullmatch(token) + if not match: + raise ValueError(token) + prefix, num = match.groups() + base = { + 'V': 0, + 'L': 0x4000, + 'B': 0x8000, + }[prefix] + return Variable(base + int(num)) + + +def compile_line(line: str) -> bytes: + line = line.strip() + if not line or line.startswith((';', 'room')): + return b'' + if line.startswith('break-here'): + parts = line.split() + count = int(parts[1]) if len(parts) > 1 else 1 + return bytes([BREAK_OPCODE] * count) + inc = re.match(r'^\+\+(\S+)$', line) + if inc: + var = parse_variable(inc.group(1)) + return bytes([INCREMENT_OPCODE]) + var.to_bytes() + dec = re.match(r'^--(\S+)$', line) + if dec: + var = parse_variable(dec.group(1)) + return bytes([DECREMENT_OPCODE]) + var.to_bytes() + raise NotImplementedError(f'Unsupported statement: {line}') + + +def compile_scu(path: Path) -> bytes: + target = Path(path) + data = bytearray() + for line in target.read_text().splitlines(): + data += compile_line(line) + return bytes(data) diff --git a/src/nutcracker/sputm/windex/runner.py b/src/nutcracker/sputm/windex/runner.py index 2b59576..f548930 100644 --- a/src/nutcracker/sputm/windex/runner.py +++ b/src/nutcracker/sputm/windex/runner.py @@ -12,6 +12,7 @@ from ..strings import RAW_ENCODING from ..tree import narrow_schema, open_game_resource from .scu import dump_script_file +from .compiler import compile_scu app = typer.Typer() @@ -97,17 +98,28 @@ def decompile( for disk in root: for room in sputm.findall('LFLF', disk): - room_no = rnam.get(room.attribs['gid'], f"room_{room.attribs['gid']}") + room_no = rnam.get(room.attribs['gid'], f'room_{room.attribs["gid"]}') print( '==========================', room.attribs['path'], room_no, ) - fname = f"{script_dir}/{room.attribs['gid']:04d}_{room_no}.scu" + fname = f'{script_dir}/{room.attribs["gid"]:04d}_{room_no}.scu' with open(fname, 'w', **RAW_ENCODING) as script_file: dump_script_file(room_no, room, decompile, script_file) +@app.command('compile') +def compile( + script: Path = typer.Argument(..., help='Windex .scu script to compile'), + output: Path = typer.Option(None, '--out', '-o', help='Target binary file'), +) -> None: + """Compile a Windex .scu script to raw bytecode.""" + data = compile_scu(script) + out = output or script.with_suffix('.bin') + out.write_bytes(data) + + if __name__ == '__main__': app()