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>
102 lines
3.5 KiB
Python
102 lines
3.5 KiB
Python
"""Corpus-wide: verify T1/T2/T3 target-dword types, test table completeness,
|
|
and do a stricter string scan.
|
|
"""
|
|
import struct
|
|
from pathlib import Path
|
|
from collections import Counter
|
|
|
|
ROOT = Path(__file__).resolve().parents[2] / "extracted" / "DATA1"
|
|
|
|
def parse(path):
|
|
data = path.read_bytes()
|
|
f = struct.unpack_from("<13I", data, 8)
|
|
body = data[0x3C:]
|
|
n = len(body) // 4
|
|
dws = struct.unpack_from(f"<{n}I", body, 0)
|
|
return f, dws, body
|
|
|
|
def main():
|
|
files = sorted(ROOT.glob("*.BIN"))
|
|
type_hist = {1: Counter(), 2: Counter(), 3: Counter()}
|
|
complete = {1: Counter(), 2: Counter(), 3: Counter()}
|
|
for p in files:
|
|
f, dws, body = parse(p)
|
|
code_end = f[8]
|
|
specs = {1: (f[7], f[8], 0x71), 2: (f[9], f[10], 0x03), 3: (f[11], f[12], 0x8F)}
|
|
for t, (cnt, off, expect) in specs.items():
|
|
targets = dws[off:off + cnt]
|
|
for e in targets:
|
|
type_hist[t][dws[e] if e < len(dws) else "OOB"] += 1
|
|
# naive count of (expect, 0, X) triples in code, at any alignment
|
|
naive = sum(1 for i in range(code_end - 2)
|
|
if dws[i] == expect and dws[i + 1] == 0)
|
|
if cnt == naive:
|
|
complete[t]["exact"] += 1
|
|
elif cnt < naive:
|
|
complete[t]["table<naive"] += 1
|
|
else:
|
|
complete[t]["table>naive"] += 1
|
|
for t in (1, 2, 3):
|
|
print(f"T{t} target-dword histogram: "
|
|
+ ", ".join(f"{v:#x}x{n}" for v, n in type_hist[t].most_common(6)))
|
|
print(f"T{t} completeness vs naive scan: {dict(complete[t])}")
|
|
|
|
# value ranges of the operands referenced by each table (sample corpus-wide)
|
|
for t, expect in ((1, 0x71), (2, 0x03), (3, 0x8F)):
|
|
lo, hi = None, None
|
|
vals = Counter()
|
|
for p in files:
|
|
f, dws, body = parse(p)
|
|
cnt, off = (f[7], f[8]) if t == 1 else (f[9], f[10]) if t == 2 else (f[11], f[12])
|
|
for e in dws[off:off + cnt]:
|
|
if e + 2 < len(dws):
|
|
v = dws[e + 2]
|
|
vals[v] += 1
|
|
lo = v if lo is None else min(lo, v)
|
|
hi = v if hi is None else max(hi, v)
|
|
print(f"T{t} operand values: min={lo and hex(lo)}, max={hi and hex(hi)}, "
|
|
f"top: {', '.join(f'{v:#x}x{n}' for v, n in vals.most_common(8))}")
|
|
|
|
def good_string(bs):
|
|
"""bs = decoded-candidate raw bytes (already XOR'd). Strict cp932 validity."""
|
|
i, chars = 0, 0
|
|
while i < len(bs):
|
|
b = bs[i]
|
|
if 0x20 <= b <= 0x7E:
|
|
i += 1; chars += 1
|
|
elif 0x81 <= b <= 0x9F or 0xE0 <= b <= 0xEA:
|
|
if i + 1 < len(bs) and (0x40 <= bs[i+1] <= 0xFC) and bs[i+1] != 0x7F:
|
|
i += 2; chars += 1
|
|
else:
|
|
return 0
|
|
else:
|
|
return 0
|
|
return chars
|
|
|
|
def scan(name, limit=25, min_chars=3):
|
|
f, dws, body = parse(ROOT / name)
|
|
n = len(body) // 4
|
|
print(f"\n=== strings in {name} (strict) ===")
|
|
found, i = 0, 0
|
|
while i < n and found < limit:
|
|
raw = bytearray()
|
|
j = i
|
|
while j < n:
|
|
chunk = bytes(b ^ 0xFF for b in body[j*4:(j+1)*4])
|
|
raw += chunk
|
|
j += 1
|
|
if 0 in chunk:
|
|
break
|
|
s = bytes(raw).split(b"\0")[0]
|
|
if len(s) >= 2 and good_string(s) >= min_chars:
|
|
print(f" [{i:#x}] {s.decode('cp932')!r}")
|
|
found += 1
|
|
i = j
|
|
else:
|
|
i += 1
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
scan("SC0030.BIN")
|
|
scan("MENU.BIN", limit=12)
|