105 lines
3.8 KiB
Python
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())
|