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:
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user