Resolve call-script dispatch: id = raw SYS4INI file index

Native-RE (Ghidra) cracked call-script <id> (opcode 0x03): its handler
FUN_0041bc90 -> loader FUN_0040e980 -> resolver FUN_0044f390 indexes an
80-byte record table at base + id*0x50 == the SYS4INI record layout. So
`call-script <id>` is a direct RAW index into the SYS4INI global file
table (the asset index we already parse) -- there is no separate on-disk
id->code registry. This resolves name-resolution.md #1, statically, no
Frida.

Confirmed: all 297 distinct corpus call-script ids resolve to a .BIN
script with a semantically-exact name (0x1ab->ADDITEM, 0x2ae7->MES,
0x143->BUNKI, 0x329d->CALCREVISE), 0 out-of-range, 0 alternate-pack.
Companion op 0x8f `call` is an intra-script JSR (FUN_0041fba0), not
cross-script.

- parse_sys4ini.py: preserve `raw_index` per entry (= the engine file id;
  index the RAW records incl. '@' placeholders) + emit
  build/callscript-names.json (id->name).
- sys4load.py: annotate `call-script 0x1ab =ADDITEM.BIN`.
- opcodes.toml 0x03/0x8f refined (source=investigation, confidence high,
  handler VAs) + rebuilt opcode-reference.md.
- docs: engine-re.md (op 0x03 section + backlog re-aimed),
  name-resolution.md #1 (SOLVED), script-inventory.md (call graph +
  living-reference decision), tools-reference.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gamer147
2026-07-07 12:44:51 -04:00
parent 057a26a97a
commit 14ad23b8c7
8 changed files with 192 additions and 40 deletions

View File

@@ -115,13 +115,18 @@ def parse(path: Path) -> dict:
f"have {len(blob)} decompressed bytes)")
files = []
for _ in range(file_count):
for i in range(file_count):
name_b, arc_id, file_number, offset, size = struct.unpack_from(FILE_ENTRY_FMT, blob, p)
p += FILE_ENTRY_LEN
name = _cstr(name_b)
if name == "@" or not name:
continue
files.append({
# raw_index = the record's 0-based position in the SYS4INI table INCLUDING '@'
# placeholders. This is the engine's universal file id: `call-script <id>` and every
# script/asset load index by it (engine FUN_0044f390: record = base + id*0x50). It is
# NOT the same as this entry's position in `files` (which omits placeholders).
"raw_index": i,
"name": name,
"archive": archives[arc_id] if 0 <= arc_id < arc_count else None,
"arc_id": arc_id,
@@ -215,6 +220,15 @@ def main() -> int:
out.write_text(json.dumps(index, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"-> {out.relative_to(paths.REPO)}")
# call-script id -> name map (raw_index keyed). `call-script <id>` (opcode 0x03) is a direct
# raw index into the SYS4INI file table, so this IS the call-graph name registry that
# name-resolution.md #1 needed (confirmed via native-RE, see docs/engine-re.md). sys4load reads
# it to annotate `call-script 0x1ab =ADDITEM`.
cs = {str(f["raw_index"]): f["name"] for f in index["files"]}
cs_out = paths.BUILD / "callscript-names.json"
cs_out.write_text(json.dumps(cs, ensure_ascii=False, indent=0), encoding="utf-8")
print(f"-> {cs_out.relative_to(paths.REPO)} ({len(cs)} ids)")
if do_check:
print("--- validation ---")
return 1 if check(index) else 0

View File

@@ -80,6 +80,21 @@ def _load_global_labels() -> dict:
GLOBAL_LABELS = _load_global_labels()
def _load_callscript_names() -> dict:
"""id -> script name. `call-script <id>` (op 0x03) is a raw index into the SYS4INI file
table; build/callscript-names.json maps every id to its script name (see docs/engine-re.md)."""
try:
p = Path(__file__).resolve().parent.parent / "build" / "callscript-names.json"
data = json.loads(p.read_text(encoding="utf-8"))
except Exception:
return {}
return {int(k): v for k, v in data.items()}
CALLSCRIPT_NAMES = _load_callscript_names()
CALLSCRIPT_OP = 0x03
def display_label(op: int) -> str:
"""Rendered mnemonic: Kelebek name if it has one, else the inferred name, else u00…."""
lbl = OPCODES.get(op, (f"?{op:x}", 0))[0]
@@ -368,6 +383,8 @@ def _fmt_operand(op: int, arg_index: int, atype: int, aval: int, strings: dict)
tlabel = ARG_TYPES.get(atype)
if atype == 0 or tlabel is None: # immediate / unknown-tag: raw value
if atype == 0:
if op == CALLSCRIPT_OP and aval in CALLSCRIPT_NAMES:
return f"{aval:#x} ={CALLSCRIPT_NAMES[aval]}" # call-script target script name
return f"{aval:#x}"
return f"<t{atype:#x} {aval:#x}>"
if tlabel == "float":