116 lines
4.3 KiB
Python
116 lines
4.3 KiB
Python
#!/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 <SCENE> [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())
|