Files
pokemon_pinball_wiki/scripts/dump_radium_opcode_tables.py
2026-07-07 00:25:38 -05:00

105 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""Dump Radium command opcode table entries from the native game binaries.
The command stream runtime uses a 16-byte table entry per opcode:
handler address, then payload byte length. Static Ghidra analysis of
FUN_007a46e0 confirms the preload scan indexes the second qword directly
(`table_base + 8`) to skip unknown/non-sound opcodes.
"""
from __future__ import annotations
import csv
import struct
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
OPCODE_COUNT = 0x13
TARGETS = [
{
"image": "main",
"binary": ROOT / "games/pokemon_pro/game",
"table_offset": 0x3079E70,
"histogram": ROOT / "analysis/radium-image/command_opcode_histogram.tsv",
"payload_summary": ROOT / "analysis/radium-image/command_opcode_payload_summary.tsv",
"out": ROOT / "analysis/radium-image/opcode_handler_table.tsv",
},
{
"image": "spike_menu",
"binary": ROOT / "games/pokemon_pro/spike3/spike_menu/game",
"table_offset": 0x410CE0,
"histogram": ROOT / "analysis/radium-spike-menu-image/command_opcode_histogram.tsv",
"payload_summary": ROOT / "analysis/radium-spike-menu-image/command_opcode_payload_summary.tsv",
"out": ROOT / "analysis/radium-spike-menu-image/opcode_handler_table.tsv",
},
]
def load_tsv_by_opcode(path: Path) -> dict[int, dict[str, str]]:
if not path.exists():
return {}
with path.open(newline="") as f:
rows = {}
for row in csv.DictReader(f, delimiter="\t"):
opcode = row.get("opcode", "")
if opcode == "":
continue
rows[int(opcode)] = row
return rows
def main() -> int:
fieldnames = [
"image",
"opcode",
"opcode_hex",
"table_offset",
"entry_offset",
"handler_address",
"payload_length",
"observed_command_count",
"observed_stream_count",
"observed_sound_ref_count",
"unique_payload_count",
"top_payload_hex",
]
for target in TARGETS:
data = target["binary"].read_bytes()
hist = load_tsv_by_opcode(target["histogram"])
summary = load_tsv_by_opcode(target["payload_summary"])
rows = []
for opcode in range(OPCODE_COUNT):
entry_offset = target["table_offset"] + opcode * 0x10
handler_address, payload_length = struct.unpack_from("<QQ", data, entry_offset)
hist_row = hist.get(opcode, {})
summary_row = summary.get(opcode, {})
rows.append(
{
"image": target["image"],
"opcode": opcode,
"opcode_hex": f"0x{opcode:02x}",
"table_offset": f"0x{target['table_offset']:x}",
"entry_offset": f"0x{entry_offset:x}",
"payload_length": payload_length,
"handler_address": f"0x{handler_address:x}" if handler_address else "",
"observed_command_count": hist_row.get("command_count", "0"),
"observed_stream_count": hist_row.get("stream_count", "0"),
"observed_sound_ref_count": hist_row.get("sound_ref_count", "0"),
"unique_payload_count": summary_row.get("unique_payload_count", "0"),
"top_payload_hex": summary_row.get("top_payload_hex", ""),
}
)
target["out"].parent.mkdir(parents=True, exist_ok=True)
with target["out"].open("w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames, delimiter="\t")
writer.writeheader()
writer.writerows(rows)
print(f"wrote {target['out']} ({len(rows)} rows)")
return 0
if __name__ == "__main__":
raise SystemExit(main())