chore: initialize age-reimpl repo

Reverse-engineering + open reimplementation workspace for Eushully's AGE/SYS4
engine (first target: Himegari). The repo root is age-reimpl/; the original game
install and the extracted ALF data are siblings outside the repo and are never
tracked. build/ (derived corpora) is gitignored and regenerated by the tools.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-06 12:58:30 -04:00
commit 463877773c
34 changed files with 8898 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Validate Kelebek1's AGE opcode table against Himegari SYS4 scripts.
Model (from Kelebek1 disassembler.cpp + age-shared.cpp):
code stream = sequence of instructions.
each instruction = <opcode:u32> then argument_count * <arg>, where each arg = <type:u32><value:u32>.
=> instruction length in dwords = 1 + 2*argument_count (uniform; type-2/0x64 args seek elsewhere, don't consume inline)
arg type 2 = inline string (value = dword offset into body). types: 0 imm,1 float,3 g-int,9 l-int, etc.
A clean decode consumes exactly code_len dwords with no unknown opcode and no arg overrun.
"""
import os, re, sys, collections
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import paths
import sys4load
CPP = paths.KELEBEK_CPP.read_text(encoding="utf-8")
# parse {0x1F4, "label", 0x0},
TABLE = {}
LABEL = {}
for m in re.finditer(r'\{\s*(0x[0-9A-Fa-f]+)\s*,\s*"([^"]*)"\s*,\s*(0x[0-9A-Fa-f]+)\s*\}', CPP):
op = int(m.group(1), 16); lbl = m.group(2); argc = int(m.group(3), 16)
TABLE[op] = argc; LABEL[op] = lbl
print(f"parsed {len(TABLE)} opcode defs from Kelebek1 table (max op 0x{max(TABLE):x})")
files = paths.scripts()
clean = dirty = parsefail = 0
unknown_ops = collections.Counter()
fail_examples = []
str_ok = str_bad = 0
opcode_use = collections.Counter()
for name, p in sorted(files.items()):
try:
scr = sys4load.load(p)
except Exception:
parsefail += 1; continue
dw = scr.dwords; cl = scr.code_len
i = 0; ok = True; reason = ""
while i < cl:
op = dw[i]
if op not in TABLE:
unknown_ops[op] += 1; ok = False; reason = f"unknown op 0x{op:x} @{i}"; break
opcode_use[op] += 1
argc = TABLE[op]
# check each arg's type; resolve strings
base = i + 1
if base + 2*argc > cl:
ok = False; reason = f"arg overrun op 0x{op:x} @{i} needs {argc} args"; break
for a in range(argc):
atype = dw[base + 2*a]; aval = dw[base + 2*a + 1]
if atype == 2: # inline string
if 0 <= aval < scr.nbody:
txt, nd = sys4load._decode_string(dw, aval)
if txt is None: str_bad += 1
else: str_ok += 1
else:
str_bad += 1
i = base + 2*argc
if ok and i == cl:
clean += 1
else:
dirty += 1
if len(fail_examples) < 15:
fail_examples.append(f" {name}: {reason} (stopped @{i}/{cl})")
print(f"\n== decode result over {len(files)} scripts ==")
print(f" clean (fully consumed, all opcodes known): {clean}")
print(f" dirty: {dirty}")
print(f" parse-fail (container): {parsefail}")
print(f" string args resolved ok / bad: {str_ok} / {str_bad}")
print(f"\ntop unknown opcodes (op: files affected):")
for op, c in unknown_ops.most_common(25):
print(f" 0x{op:x}: {c}")
print(f"\nsample dirty files:")
print("\n".join(fail_examples))
print(f"\ntop 25 opcodes actually used in Himegari (op label count):")
for op, c in opcode_use.most_common(25):
print(f" 0x{op:<4x} {LABEL.get(op,'?'):22} {c}")