#!/usr/bin/env python3 """Diagnostic SYS4INI scene-group correlation tool; NOT a native runtime resource resolver. SYS4INI's file list is organized into SECTIONS, one per scene -- each is a `SCxxxx.BIN` script entry followed by that scene's asset MANIFEST (all assets it references, across every archive and type: EV/BG/CS/AE event & sprite graphics, OGG/WAV audio, ...). `file_number` is usually the 0-based index within that grouping. This tool explores the correlation: resId -> files[ section_base(scene) + resId ] That relationship explained 97% of file ordering and 586/595 captured load/name pairs, but Ghidra native RE on 2026-07-21 proved bytecode operands are already universal packed SYS4INI/AAI ids. asset_open_indexed_entry applies no section base. Do not use this tool's scene-relative output in runtime code; see docs/asset-resolution-re.md. Usage: py -3.11 -X utf8 tools/resolve_asset.py --build # emit build/asset-sections.json py -3.11 -X utf8 tools/resolve_asset.py [resId] # inspect one correlation, or dump a group """ from __future__ import annotations import json import re import sys from pathlib import Path HERE = Path(__file__).resolve().parent sys.path.insert(0, str(HERE)) import paths def load_index(): return json.loads((paths.BUILD / "asset-index.json").read_text(encoding="utf-8"))["files"] def sections(files): """Split the SYS4INI file list into sections at each file_number reset (fn <= prev). Returns (section_start_per_position[list], sections[list of (start, end, scene_name|None]).""" base_of, secs, start, prev = [], [], 0, -1 for i, f in enumerate(files): if f["file_number"] <= prev: secs.append((start, i - 1)) start = i base_of.append(start) prev = f["file_number"] secs.append((start, len(files) - 1)) # attach the scene script (SCxxxx.BIN) that owns each section, if any out = [] for s, e in secs: scene = next((files[k]["name"] for k in range(s, e + 1) if re.match(r"SC\d+\.BIN$", files[k]["name"])), None) out.append({"start": s, "end": e, "scene": scene}) return base_of, out def scene_base(files, base_of, scene): key = scene.upper() if not key.endswith(".BIN"): key += ".BIN" pos = next((i for i, f in enumerate(files) if f["name"].upper() == key), None) if pos is None: raise SystemExit(f"scene not in SYS4INI: {scene}") return base_of[pos] def resolve(files, base, resid): p = base + resid return files[p] if 0 <= p < len(files) else None def main() -> int: files = load_index() base_of, secs = sections(files) if "--build" in sys.argv: scene_bases = {} for sec in secs: if sec["scene"]: scene_bases[sec["scene"].removesuffix(".BIN")] = sec["start"] out = paths.BUILD / "asset-sections.json" out.write_text(json.dumps( {"note": "SYS4INI sections; resolve resId -> files[section_base + resId]. " "See docs/asset-resolution-re.md.", "section_count": len(secs), "scene_base": scene_bases, "sections": secs}, ensure_ascii=False, indent=1), encoding="utf-8") print(f"{len(secs)} sections, {len(scene_bases)} scenes -> {out.relative_to(paths.REPO)}") return 0 args = [a for a in sys.argv[1:] if not a.startswith("-")] if not args: raise SystemExit(__doc__) scene = args[0] base = scene_base(files, base_of, scene) sec = next(s for s in secs if s["start"] == base) print(f"{scene}: section [{sec['start']}..{sec['end']}] base {base} " f"({sec['end']-sec['start']+1} entries)") if len(args) > 1: resid = int(args[1], 0) f = resolve(files, base, resid) print(f" resId {resid} -> {f['archive']} {f['name']} (offset {f['offset']}, size {f['size']})" if f else f" resId {resid} -> out of range") return 0 # dump the scene's manifest (graphics + audio), skipping the leading script entry print(" manifest (resId -> asset):") for p in range(base, sec["end"] + 1): resid = p - base f = files[p] print(f" {resid:>4} {f['archive'][:5]} {f['name']}") return 0 if __name__ == "__main__": sys.exit(main())