#!/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(' 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 ") 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()