Files
OpenMaidEngine/tools/probe_types.py
gamer147 aa3da00bfa 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>
2026-07-06 12:58:30 -04:00

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)