190 lines
6.2 KiB
Python
Executable File
190 lines
6.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Dump ALTTP Dungeon Object Handler Tables
|
|
|
|
This script reads the dungeon object handler tables from ROM and dumps:
|
|
1. Handler addresses for Type 1, 2, and 3 objects
|
|
2. First 20 Type 1 handler addresses
|
|
3. Handler routine analysis
|
|
|
|
Based on ALTTP ROM structure:
|
|
- Type 1 handler table: Bank $01, $8200 (objects 0x00-0xFF)
|
|
- Type 2 handler table: Bank $01, $8470 (objects 0x100-0x1FF)
|
|
- Type 3 handler table: Bank $01, $85F0 (objects 0x200-0x2FF)
|
|
|
|
Each entry is a 16-bit pointer (little-endian) to a handler routine in Bank $01.
|
|
"""
|
|
|
|
import sys
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
|
|
def read_rom(rom_path):
|
|
"""Read ROM file and return data, skipping SMC header if present."""
|
|
with open(rom_path, 'rb') as f:
|
|
data = f.read()
|
|
|
|
# Check for SMC header (512 bytes)
|
|
if len(data) % 0x400 == 0x200:
|
|
print(f"[INFO] SMC header detected, skipping 512 bytes")
|
|
return data[0x200:]
|
|
return data
|
|
|
|
|
|
def pc_to_snes(pc_addr):
|
|
"""Convert PC address to SNES $01:xxxx format."""
|
|
# For LoROM, PC address maps to SNES as:
|
|
# PC 0x00000-0x7FFF -> $00:8000-$00:FFFF
|
|
# PC 0x08000-0x0FFFF -> $01:8000-$01:FFFF
|
|
bank = (pc_addr >> 15) & 0xFF
|
|
offset = (pc_addr & 0x7FFF) | 0x8000
|
|
return f"${bank:02X}:{offset:04X}"
|
|
|
|
|
|
def snes_to_pc(bank, offset):
|
|
"""Convert SNES address to PC address (LoROM mapping)."""
|
|
# Bank $01, offset $8000-$FFFF -> PC 0x08000 + (offset - 0x8000)
|
|
if offset < 0x8000:
|
|
raise ValueError(f"Invalid offset ${offset:04X}, must be >= $8000")
|
|
return (bank * 0x8000) + (offset - 0x8000)
|
|
|
|
|
|
def dump_handler_table(rom_data, bank, start_offset, count, name):
|
|
"""
|
|
Dump handler table from ROM.
|
|
|
|
Args:
|
|
rom_data: ROM data bytes
|
|
bank: SNES bank number
|
|
start_offset: SNES offset in bank
|
|
count: Number of entries to read
|
|
name: Table name for display
|
|
|
|
Returns:
|
|
List of handler addresses (as integers)
|
|
"""
|
|
pc_addr = snes_to_pc(bank, start_offset)
|
|
print(f"\n{'='*70}")
|
|
print(f"{name}")
|
|
print(f"SNES Address: ${bank:02X}:{start_offset:04X}")
|
|
print(f"PC Address: 0x{pc_addr:06X}")
|
|
print(f"{'='*70}")
|
|
|
|
handlers = []
|
|
for i in range(count):
|
|
entry_pc = pc_addr + (i * 2)
|
|
if entry_pc + 1 >= len(rom_data):
|
|
print(f"[ERROR] PC address 0x{entry_pc:06X} out of bounds")
|
|
break
|
|
|
|
# Read 16-bit little-endian pointer
|
|
handler_offset = struct.unpack_from('<H', rom_data, entry_pc)[0]
|
|
handlers.append(handler_offset)
|
|
|
|
# Convert to full SNES address (same bank)
|
|
handler_snes = f"${bank:02X}:{handler_offset:04X}"
|
|
|
|
# Only print first 20 for Type 1
|
|
if i < 20 or name != "Type 1 Handler Table":
|
|
print(f" Object 0x{i:03X}: {handler_snes} (PC: 0x{snes_to_pc(bank, handler_offset):06X})")
|
|
|
|
if name == "Type 1 Handler Table" and count > 20:
|
|
print(f" ... ({count - 20} more entries)")
|
|
|
|
return handlers
|
|
|
|
|
|
def analyze_handler_uniqueness(handlers, name):
|
|
"""Analyze how many unique handlers exist."""
|
|
unique_handlers = set(handlers)
|
|
print(f"\n[ANALYSIS] {name}:")
|
|
print(f" Total objects: {len(handlers)}")
|
|
print(f" Unique handlers: {len(unique_handlers)}")
|
|
print(f" Shared handlers: {len(handlers) - len(unique_handlers)}")
|
|
|
|
# Find most common handlers
|
|
from collections import Counter
|
|
handler_counts = Counter(handlers)
|
|
most_common = handler_counts.most_common(5)
|
|
print(f" Most common handlers:")
|
|
for handler_offset, count in most_common:
|
|
print(f" ${handler_offset:04X}: used by {count} objects")
|
|
|
|
|
|
def dump_handler_bytes(rom_data, bank, handler_offset, byte_count=32):
|
|
"""Dump first N bytes of a handler routine."""
|
|
try:
|
|
pc_addr = snes_to_pc(bank, handler_offset)
|
|
if pc_addr + byte_count >= len(rom_data):
|
|
byte_count = len(rom_data) - pc_addr
|
|
|
|
handler_bytes = rom_data[pc_addr:pc_addr + byte_count]
|
|
print(f"\n[HANDLER DUMP] ${bank:02X}:{handler_offset:04X} (PC: 0x{pc_addr:06X})")
|
|
print(f" First {byte_count} bytes:")
|
|
|
|
# Print in hex rows of 16 bytes
|
|
for i in range(0, byte_count, 16):
|
|
row = handler_bytes[i:i+16]
|
|
hex_str = ' '.join(f'{b:02X}' for b in row)
|
|
ascii_str = ''.join(chr(b) if 32 <= b < 127 else '.' for b in row)
|
|
print(f" {i:04X}: {hex_str:<48} {ascii_str}")
|
|
except ValueError as e:
|
|
print(f"[ERROR] {e}")
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2:
|
|
print("Usage: python3 dump_object_handlers.py <rom_path>")
|
|
print("Example: python3 dump_object_handlers.py zelda3.sfc")
|
|
sys.exit(1)
|
|
|
|
rom_path = Path(sys.argv[1])
|
|
if not rom_path.exists():
|
|
print(f"[ERROR] ROM file not found: {rom_path}")
|
|
sys.exit(1)
|
|
|
|
print(f"[INFO] Reading ROM: {rom_path}")
|
|
rom_data = read_rom(rom_path)
|
|
print(f"[INFO] ROM size: {len(rom_data)} bytes ({len(rom_data) / 1024 / 1024:.2f} MB)")
|
|
|
|
# Dump handler tables
|
|
type1_handlers = dump_handler_table(rom_data, 0x01, 0x8200, 256, "Type 1 Handler Table")
|
|
type2_handlers = dump_handler_table(rom_data, 0x01, 0x8470, 64, "Type 2 Handler Table")
|
|
type3_handlers = dump_handler_table(rom_data, 0x01, 0x85F0, 128, "Type 3 Handler Table")
|
|
|
|
# Analyze handler distribution
|
|
analyze_handler_uniqueness(type1_handlers, "Type 1")
|
|
analyze_handler_uniqueness(type2_handlers, "Type 2")
|
|
analyze_handler_uniqueness(type3_handlers, "Type 3")
|
|
|
|
# Dump first handler (object 0x00)
|
|
if type1_handlers:
|
|
print(f"\n{'='*70}")
|
|
print(f"INVESTIGATING OBJECT 0x00 HANDLER")
|
|
print(f"{'='*70}")
|
|
dump_handler_bytes(rom_data, 0x01, type1_handlers[0], 64)
|
|
|
|
# Dump a few more common handlers
|
|
print(f"\n{'='*70}")
|
|
print(f"SAMPLE HANDLER DUMPS")
|
|
print(f"{'='*70}")
|
|
|
|
# Object 0x01 (common wall object)
|
|
if len(type1_handlers) > 1:
|
|
dump_handler_bytes(rom_data, 0x01, type1_handlers[1], 32)
|
|
|
|
# Type 2 first handler
|
|
if type2_handlers:
|
|
dump_handler_bytes(rom_data, 0x01, type2_handlers[0], 32)
|
|
|
|
print(f"\n{'='*70}")
|
|
print(f"SUMMARY")
|
|
print(f"{'='*70}")
|
|
print(f"Handler tables successfully read from ROM.")
|
|
print(f"See documentation at docs/internal/alttp-object-handlers.md")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|