backend-infra-engineer: Post v0.3.9-hotfix7 snapshot (build cleanup)
This commit is contained in:
825
scripts/analyze_room.py
Normal file
825
scripts/analyze_room.py
Normal file
@@ -0,0 +1,825 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dungeon Room Object Analyzer for ALTTP ROM Hacking.
|
||||
|
||||
This script parses room data from a Link to the Past ROM to understand which
|
||||
objects are on each layer (BG1/BG2). Useful for debugging layer compositing
|
||||
and understanding room structure.
|
||||
|
||||
Usage:
|
||||
python analyze_room.py [OPTIONS] [ROOM_IDS...]
|
||||
|
||||
Examples:
|
||||
python analyze_room.py 1 # Analyze room 001
|
||||
python analyze_room.py 1 2 3 # Analyze rooms 001, 002, 003
|
||||
python analyze_room.py --range 0 10 # Analyze rooms 0-10
|
||||
python analyze_room.py --all # Analyze all 296 rooms (summary only)
|
||||
python analyze_room.py 1 --json # Output as JSON
|
||||
python analyze_room.py 1 --rom path/to.sfc # Use specific ROM file
|
||||
python analyze_room.py --list-bg2 # List all rooms with BG2 overlay objects
|
||||
|
||||
Collision Offset Features:
|
||||
python analyze_room.py 0x27 --collision # Show collision offsets
|
||||
python analyze_room.py 0x27 --collision --asm # Output ASM format
|
||||
python analyze_room.py 0x27 --collision --filter-id 0xD9 # Filter by object ID
|
||||
python analyze_room.py 0x27 --collision --area # Expand objects to full tile area
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
# ROM addresses from dungeon_rom_addresses.h
|
||||
ROOM_OBJECT_POINTER = 0x874C # Object data pointer table
|
||||
ROOM_HEADER_POINTER = 0xB5DD # Room header pointer
|
||||
NUMBER_OF_ROOMS = 296
|
||||
|
||||
# Default ROM path (relative to script location)
|
||||
DEFAULT_ROM_PATHS = [
|
||||
"roms/alttp_vanilla.sfc",
|
||||
"../roms/alttp_vanilla.sfc",
|
||||
"roms/vanilla.sfc",
|
||||
"../roms/vanilla.sfc",
|
||||
]
|
||||
|
||||
# Object descriptions - comprehensive list
|
||||
OBJECT_DESCRIPTIONS = {
|
||||
# Type 1 Objects (0x00-0xFF)
|
||||
0x00: "Ceiling (2x2)",
|
||||
0x01: "Wall horizontal (2x4)",
|
||||
0x02: "Wall horizontal (2x4, variant)",
|
||||
0x03: "Diagonal wall NW->SE",
|
||||
0x04: "Diagonal wall NE->SW",
|
||||
0x05: "Pit horizontal (4x2)",
|
||||
0x06: "Pit vertical (2x4)",
|
||||
0x07: "Floor pattern",
|
||||
0x08: "Water edge",
|
||||
0x09: "Water edge variant",
|
||||
0x0A: "Conveyor belt",
|
||||
0x0B: "Conveyor belt variant",
|
||||
0x0C: "Diagonal acute",
|
||||
0x0D: "Diagonal acute variant",
|
||||
0x0E: "Pushable block",
|
||||
0x0F: "Rail",
|
||||
0x10: "Diagonal grave",
|
||||
0x11: "Diagonal grave variant",
|
||||
0x12: "Wall top edge",
|
||||
0x13: "Wall bottom edge",
|
||||
0x14: "Diagonal acute 2",
|
||||
0x15: "Diagonal acute 2 variant",
|
||||
0x16: "Wall pattern",
|
||||
0x17: "Wall pattern variant",
|
||||
0x18: "Diagonal grave 2",
|
||||
0x19: "Diagonal grave 2 variant",
|
||||
0x1A: "Inner corner NW",
|
||||
0x1B: "Inner corner NE",
|
||||
0x1C: "Diagonal acute 3",
|
||||
0x1D: "Diagonal acute 3 variant",
|
||||
0x1E: "Diagonal grave 3",
|
||||
0x1F: "Diagonal grave 3 variant",
|
||||
0x20: "Diagonal acute 4",
|
||||
|
||||
0x21: "Floor edge 1x2",
|
||||
0x22: "Has edge 1x1",
|
||||
0x23: "Has edge 1x1 variant",
|
||||
0x24: "Has edge 1x1 variant 2",
|
||||
0x25: "Has edge 1x1 variant 3",
|
||||
0x26: "Has edge 1x1 variant 4",
|
||||
|
||||
0x30: "Bottom corners 1x2",
|
||||
0x31: "Nothing A",
|
||||
0x32: "Nothing A",
|
||||
0x33: "Floor 4x4",
|
||||
0x34: "Solid 1x1",
|
||||
0x35: "Door switcher",
|
||||
0x36: "Decor 4x4",
|
||||
0x37: "Decor 4x4 variant",
|
||||
0x38: "Statue 2x3",
|
||||
0x39: "Pillar 2x4",
|
||||
0x3A: "Decor 4x3",
|
||||
0x3B: "Decor 4x3 variant",
|
||||
0x3C: "Doubled 2x2",
|
||||
0x3D: "Pillar 2x4 variant",
|
||||
0x3E: "Decor 2x2",
|
||||
|
||||
0x47: "Waterfall",
|
||||
0x48: "Waterfall variant",
|
||||
0x49: "Floor tile 4x2",
|
||||
0x4A: "Floor tile 4x2 variant",
|
||||
0x4C: "Bar 4x3",
|
||||
0x4D: "Shelf 4x4",
|
||||
0x4E: "Shelf 4x4 variant",
|
||||
0x4F: "Shelf 4x4 variant 2",
|
||||
0x50: "Line 1x1",
|
||||
0x51: "Cannon hole 4x3",
|
||||
0x52: "Cannon hole 4x3 variant",
|
||||
|
||||
0x60: "Wall vertical (2x2)",
|
||||
0x61: "Wall vertical (4x2)",
|
||||
0x62: "Wall vertical (4x2, variant)",
|
||||
0x63: "Diagonal wall NW->SE (vert)",
|
||||
0x64: "Diagonal wall NE->SW (vert)",
|
||||
0x65: "Decor 4x2",
|
||||
0x66: "Decor 4x2 variant",
|
||||
0x67: "Floor 2x2",
|
||||
0x68: "Floor 2x2 variant",
|
||||
0x69: "Has edge 1x1 (vert)",
|
||||
0x6A: "Edge 1x1",
|
||||
0x6B: "Edge 1x1 variant",
|
||||
0x6C: "Left corners 2x1",
|
||||
0x6D: "Right corners 2x1",
|
||||
|
||||
0x70: "Floor 4x4 (vert)",
|
||||
0x71: "Solid 1x1 (vert)",
|
||||
0x72: "Nothing B",
|
||||
0x73: "Decor 4x4 (vert)",
|
||||
|
||||
0x85: "Cannon hole 3x4",
|
||||
0x86: "Cannon hole 3x4 variant",
|
||||
0x87: "Pillar 2x4 (vert)",
|
||||
0x88: "Big rail 3x1",
|
||||
0x89: "Block 2x2",
|
||||
|
||||
0xA0: "Diagonal ceiling TL",
|
||||
0xA1: "Diagonal ceiling BL",
|
||||
0xA2: "Diagonal ceiling TR",
|
||||
0xA3: "Diagonal ceiling BR",
|
||||
0xA4: "Big hole 4x4",
|
||||
0xA5: "Diagonal ceiling TL B",
|
||||
0xA6: "Diagonal ceiling BL B",
|
||||
0xA7: "Diagonal ceiling TR B",
|
||||
0xA8: "Diagonal ceiling BR B",
|
||||
|
||||
0xC0: "Chest",
|
||||
0xC1: "Chest variant",
|
||||
0xC2: "Big chest",
|
||||
0xC3: "Big chest variant",
|
||||
0xC4: "Interroom stairs",
|
||||
0xC5: "Torch",
|
||||
0xC6: "Torch (variant)",
|
||||
|
||||
0xE0: "Pot",
|
||||
0xE1: "Block",
|
||||
0xE2: "Pot variant",
|
||||
0xE3: "Block variant",
|
||||
0xE4: "Pot (skull)",
|
||||
0xE5: "Block (push any)",
|
||||
0xE6: "Skull pot",
|
||||
0xE7: "Big gray block",
|
||||
0xE8: "Spike block",
|
||||
0xE9: "Spike block variant",
|
||||
|
||||
# Type 2 objects (0x100+)
|
||||
0x100: "Corner NW (concave)",
|
||||
0x101: "Corner NE (concave)",
|
||||
0x102: "Corner SW (concave)",
|
||||
0x103: "Corner SE (concave)",
|
||||
0x104: "Corner NW (convex)",
|
||||
0x105: "Corner NE (convex)",
|
||||
0x106: "Corner SW (convex)",
|
||||
0x107: "Corner SE (convex)",
|
||||
0x108: "4x4 Corner NW",
|
||||
0x109: "4x4 Corner NE",
|
||||
0x10A: "4x4 Corner SW",
|
||||
0x10B: "4x4 Corner SE",
|
||||
0x10C: "Corner piece NW",
|
||||
0x10D: "Corner piece NE",
|
||||
0x10E: "Corner piece SW",
|
||||
0x10F: "Corner piece SE",
|
||||
0x110: "Weird corner bottom NW",
|
||||
0x111: "Weird corner bottom NE",
|
||||
0x112: "Weird corner bottom SW",
|
||||
0x113: "Weird corner bottom SE",
|
||||
0x114: "Weird corner top NW",
|
||||
0x115: "Weird corner top NE",
|
||||
0x116: "Platform / Floor overlay",
|
||||
0x117: "Platform variant",
|
||||
0x118: "Statue / Pillar",
|
||||
0x119: "Statue / Pillar variant",
|
||||
0x11A: "Star tile switch",
|
||||
0x11B: "Star tile switch variant",
|
||||
0x11C: "Rail platform",
|
||||
0x11D: "Rail platform variant",
|
||||
0x11E: "Somaria platform",
|
||||
0x11F: "Somaria platform variant",
|
||||
0x120: "Stairs up (north)",
|
||||
0x121: "Stairs down (south)",
|
||||
0x122: "Stairs left",
|
||||
0x123: "Stairs right",
|
||||
0x124: "Spiral stairs up",
|
||||
0x125: "Spiral stairs down",
|
||||
0x126: "Sanctuary entrance",
|
||||
0x127: "Sanctuary entrance variant",
|
||||
0x128: "Hole/pit",
|
||||
0x129: "Hole/pit variant",
|
||||
0x12A: "Warp tile",
|
||||
0x12B: "Warp tile variant",
|
||||
0x12C: "Layer switch NW",
|
||||
0x12D: "Layer switch NE",
|
||||
0x12E: "Layer switch SW",
|
||||
0x12F: "Layer switch SE",
|
||||
0x130: "Light cone",
|
||||
0x131: "Light cone variant",
|
||||
0x132: "Floor switch",
|
||||
0x133: "Floor switch (heavy)",
|
||||
0x134: "Bombable floor",
|
||||
0x135: "Bombable floor variant",
|
||||
0x136: "Cracked floor",
|
||||
0x137: "Cracked floor variant",
|
||||
0x138: "Stairs inter-room",
|
||||
0x139: "Stairs inter-room variant",
|
||||
0x13A: "Stairs straight",
|
||||
0x13B: "Stairs straight variant",
|
||||
0x13C: "Eye switch",
|
||||
0x13D: "Eye switch variant",
|
||||
0x13E: "Crystal switch",
|
||||
0x13F: "Crystal switch variant",
|
||||
}
|
||||
|
||||
# Draw routine names for detailed analysis
|
||||
DRAW_ROUTINES = {
|
||||
0x01: "RoomDraw_Rightwards2x4_1to15or26",
|
||||
0x02: "RoomDraw_Rightwards2x4_1to15or26",
|
||||
0x03: "RoomDraw_Rightwards2x4_1to16_BothBG",
|
||||
0x04: "RoomDraw_Rightwards2x4_1to16_BothBG",
|
||||
0x33: "RoomDraw_Rightwards4x4_1to16",
|
||||
0x34: "RoomDraw_Rightwards1x1Solid_1to16_plus3",
|
||||
0x38: "RoomDraw_RightwardsStatue2x3spaced2_1to16",
|
||||
0x61: "RoomDraw_Downwards4x2_1to15or26",
|
||||
0x62: "RoomDraw_Downwards4x2_1to15or26",
|
||||
0x63: "RoomDraw_Downwards4x2_1to16_BothBG",
|
||||
0x64: "RoomDraw_Downwards4x2_1to16_BothBG",
|
||||
0x71: "RoomDraw_Downwards1x1Solid_1to16_plus3",
|
||||
0xA4: "RoomDraw_BigHole4x4_1to16",
|
||||
0xC6: "RoomDraw_Torch",
|
||||
}
|
||||
|
||||
|
||||
def snes_to_pc(snes_addr: int) -> int:
|
||||
"""Convert SNES LoROM address to PC file offset."""
|
||||
bank = (snes_addr >> 16) & 0xFF
|
||||
addr = snes_addr & 0xFFFF
|
||||
|
||||
if bank >= 0x80:
|
||||
bank -= 0x80
|
||||
|
||||
if addr >= 0x8000:
|
||||
return (bank * 0x8000) + (addr - 0x8000)
|
||||
else:
|
||||
return snes_addr & 0x3FFFFF
|
||||
|
||||
|
||||
def read_long(rom_data: bytes, offset: int) -> int:
|
||||
"""Read a 24-bit little-endian long address."""
|
||||
return struct.unpack('<I', rom_data[offset:offset+3] + b'\x00')[0]
|
||||
|
||||
|
||||
def decode_object(b1: int, b2: int, b3: int, layer: int) -> Dict:
|
||||
"""Decode 3-byte object data into object properties."""
|
||||
obj = {
|
||||
'b1': b1, 'b2': b2, 'b3': b3,
|
||||
'layer': layer,
|
||||
'type': 1,
|
||||
'id': 0,
|
||||
'x': 0,
|
||||
'y': 0,
|
||||
'size': 0
|
||||
}
|
||||
|
||||
# Type 2: 111111xx xxxxyyyy yyiiiiii
|
||||
if b1 >= 0xFC:
|
||||
obj['type'] = 2
|
||||
obj['id'] = (b3 & 0x3F) | 0x100
|
||||
obj['x'] = ((b2 & 0xF0) >> 4) | ((b1 & 0x03) << 4)
|
||||
obj['y'] = ((b2 & 0x0F) << 2) | ((b3 & 0xC0) >> 6)
|
||||
obj['size'] = 0
|
||||
# Type 3: xxxxxxii yyyyyyii 11111iii
|
||||
elif b3 >= 0xF8:
|
||||
obj['type'] = 3
|
||||
obj['id'] = (b3 << 4) | 0x80 | ((b2 & 0x03) << 2) | (b1 & 0x03)
|
||||
obj['x'] = (b1 & 0xFC) >> 2
|
||||
obj['y'] = (b2 & 0xFC) >> 2
|
||||
obj['size'] = ((b1 & 0x03) << 2) | (b2 & 0x03)
|
||||
# Type 1: xxxxxxss yyyyyyss iiiiiiii
|
||||
else:
|
||||
obj['type'] = 1
|
||||
obj['id'] = b3
|
||||
obj['x'] = (b1 & 0xFC) >> 2
|
||||
obj['y'] = (b2 & 0xFC) >> 2
|
||||
obj['size'] = ((b1 & 0x03) << 2) | (b2 & 0x03)
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def get_object_description(obj_id: int) -> str:
|
||||
"""Return a human-readable description of an object ID."""
|
||||
return OBJECT_DESCRIPTIONS.get(obj_id, f"Object 0x{obj_id:03X}")
|
||||
|
||||
|
||||
def get_draw_routine(obj_id: int) -> str:
|
||||
"""Return the draw routine name for an object ID."""
|
||||
return DRAW_ROUTINES.get(obj_id, "")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Collision Offset Functions
|
||||
# =============================================================================
|
||||
|
||||
def calculate_collision_offset(x_tile: int, y_tile: int) -> int:
|
||||
"""Calculate offset into $7F2000 collision map.
|
||||
|
||||
Collision map is 64 bytes per row (64 tiles wide).
|
||||
Each position is 1 byte, but SNES uses 16-bit addressing.
|
||||
Formula: offset = (Y * 64) + X
|
||||
"""
|
||||
return (y_tile * 64) + x_tile
|
||||
|
||||
|
||||
def expand_object_area(obj: Dict) -> List[Tuple[int, int]]:
|
||||
"""Expand object to full tile coverage based on size.
|
||||
|
||||
Object 'size' field encodes dimensions differently per object type.
|
||||
Water/flood objects use size as horizontal span.
|
||||
Type 2 objects (0x100+) are typically fixed-size.
|
||||
"""
|
||||
tiles = []
|
||||
x, y, size = obj['x'], obj['y'], obj['size']
|
||||
obj_id = obj['id']
|
||||
|
||||
# Water/flood objects (0x0C9, 0x0D9, etc.) - horizontal span
|
||||
# Size encodes horizontal extent
|
||||
if obj_id in [0xC9, 0xD9, 0x0C9, 0x0D9]:
|
||||
# Size is the horizontal span (number of tiles - 1)
|
||||
for dx in range(size + 1):
|
||||
tiles.append((x + dx, y))
|
||||
|
||||
# Floor 4x4 objects (0x33, 0x70)
|
||||
elif obj_id in [0x33, 0x70]:
|
||||
# 4x4 block, size adds to dimensions
|
||||
width = 4 + (size & 0x03)
|
||||
height = 4 + ((size >> 2) & 0x03)
|
||||
for dy in range(height):
|
||||
for dx in range(width):
|
||||
tiles.append((x + dx, y + dy))
|
||||
|
||||
# Wall objects (size extends in one direction)
|
||||
elif obj_id in [0x01, 0x02, 0x03, 0x04]:
|
||||
# Horizontal walls
|
||||
for dx in range(size + 1):
|
||||
for dy in range(4): # 4 tiles tall
|
||||
tiles.append((x + dx, y + dy))
|
||||
|
||||
elif obj_id in [0x61, 0x62, 0x63, 0x64]:
|
||||
# Vertical walls
|
||||
for dx in range(4): # 4 tiles wide
|
||||
for dy in range(size + 1):
|
||||
tiles.append((x + dx, y + dy))
|
||||
|
||||
# Type 2 objects (0x100+) - fixed sizes, no expansion
|
||||
elif obj_id >= 0x100:
|
||||
tiles.append((x, y))
|
||||
|
||||
# Default: single tile or small area based on size
|
||||
else:
|
||||
# Generic expansion: size encodes width/height
|
||||
width = max(1, (size & 0x03) + 1)
|
||||
height = max(1, ((size >> 2) & 0x03) + 1)
|
||||
for dy in range(height):
|
||||
for dx in range(width):
|
||||
tiles.append((x + dx, y + dy))
|
||||
|
||||
return tiles
|
||||
|
||||
|
||||
def format_collision_asm(offsets: List[int], room_id: int, label: str = None,
|
||||
objects: List[Dict] = None) -> str:
|
||||
"""Generate ASM-ready collision data block."""
|
||||
lines = []
|
||||
label = label or f"Room{room_id:02X}_CollisionData"
|
||||
|
||||
lines.append(f"; Room 0x{room_id:02X} - Collision Offsets")
|
||||
lines.append(f"; Generated by analyze_room.py")
|
||||
|
||||
if objects:
|
||||
for obj in objects:
|
||||
lines.append(f"; Object 0x{obj['id']:03X} @ ({obj['x']},{obj['y']}) size={obj['size']}")
|
||||
|
||||
lines.append(f"{label}:")
|
||||
lines.append("{")
|
||||
lines.append(f" db {len(offsets)} ; Tile count")
|
||||
|
||||
# Group offsets by rows of 8 for readability
|
||||
for i in range(0, len(offsets), 8):
|
||||
row = offsets[i:i+8]
|
||||
hex_vals = ", ".join(f"${o:04X}" for o in sorted(row))
|
||||
lines.append(f" dw {hex_vals}")
|
||||
|
||||
lines.append("}")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def analyze_collision_offsets(result: Dict, filter_id: Optional[int] = None,
|
||||
expand_area: bool = False, asm_output: bool = False,
|
||||
verbose: bool = True) -> Dict:
|
||||
"""Analyze collision offsets for objects in a room."""
|
||||
analysis = {
|
||||
'room_id': result['room_id'],
|
||||
'objects': [],
|
||||
'offsets': [],
|
||||
'tiles': []
|
||||
}
|
||||
|
||||
# Collect all objects from all layers
|
||||
all_objects = []
|
||||
for layer_num in [0, 1, 2]:
|
||||
all_objects.extend(result['objects_by_layer'][layer_num])
|
||||
|
||||
# Filter by object ID if specified
|
||||
if filter_id is not None:
|
||||
all_objects = [obj for obj in all_objects if obj['id'] == filter_id]
|
||||
|
||||
analysis['objects'] = all_objects
|
||||
|
||||
# Calculate collision offsets
|
||||
all_tiles = []
|
||||
for obj in all_objects:
|
||||
if expand_area:
|
||||
tiles = expand_object_area(obj)
|
||||
else:
|
||||
tiles = [(obj['x'], obj['y'])]
|
||||
|
||||
for (tx, ty) in tiles:
|
||||
# Validate tile coordinates
|
||||
if 0 <= tx < 64 and 0 <= ty < 64:
|
||||
offset = calculate_collision_offset(tx, ty)
|
||||
all_tiles.append((tx, ty, offset, obj))
|
||||
|
||||
# Remove duplicates and sort
|
||||
seen_offsets = set()
|
||||
unique_tiles = []
|
||||
for (tx, ty, offset, obj) in all_tiles:
|
||||
if offset not in seen_offsets:
|
||||
seen_offsets.add(offset)
|
||||
unique_tiles.append((tx, ty, offset, obj))
|
||||
|
||||
analysis['tiles'] = unique_tiles
|
||||
analysis['offsets'] = sorted(list(seen_offsets))
|
||||
|
||||
# Output
|
||||
if asm_output:
|
||||
asm = format_collision_asm(analysis['offsets'], result['room_id'],
|
||||
objects=all_objects)
|
||||
print(asm)
|
||||
elif verbose:
|
||||
print(f"\n{'='*70}")
|
||||
print(f"COLLISION OFFSETS - Room 0x{result['room_id']:02X}")
|
||||
print(f"{'='*70}")
|
||||
|
||||
if filter_id is not None:
|
||||
print(f"Filtered by object ID: 0x{filter_id:03X}")
|
||||
|
||||
print(f"\nObjects analyzed: {len(all_objects)}")
|
||||
for obj in all_objects:
|
||||
desc = get_object_description(obj['id'])
|
||||
print(f" ID=0x{obj['id']:03X} @ ({obj['x']},{obj['y']}) size={obj['size']} - {desc}")
|
||||
|
||||
print(f"\nTile coverage: {len(unique_tiles)} tiles")
|
||||
if expand_area:
|
||||
print("(Area expansion enabled)")
|
||||
|
||||
print(f"\nCollision offsets (for $7F2000):")
|
||||
for i, (tx, ty, offset, obj) in enumerate(sorted(unique_tiles, key=lambda t: t[2])):
|
||||
print(f" ({tx:2d},{ty:2d}) -> ${offset:04X}")
|
||||
if i > 20 and len(unique_tiles) > 25:
|
||||
print(f" ... and {len(unique_tiles) - i - 1} more")
|
||||
break
|
||||
|
||||
return analysis
|
||||
|
||||
|
||||
def parse_room_objects(rom_data: bytes, room_id: int, verbose: bool = True) -> Dict:
|
||||
"""Parse all objects for a given room."""
|
||||
result = {
|
||||
'room_id': room_id,
|
||||
'floor1': 0,
|
||||
'floor2': 0,
|
||||
'layout': 0,
|
||||
'objects_by_layer': {0: [], 1: [], 2: []},
|
||||
'doors': [],
|
||||
'data_address': 0,
|
||||
}
|
||||
|
||||
# Get room object data pointer
|
||||
object_ptr_table = read_long(rom_data, ROOM_OBJECT_POINTER)
|
||||
object_ptr_table_pc = snes_to_pc(object_ptr_table)
|
||||
|
||||
# Read room-specific pointer (3 bytes per room)
|
||||
room_ptr_addr = object_ptr_table_pc + (room_id * 3)
|
||||
room_data_snes = read_long(rom_data, room_ptr_addr)
|
||||
room_data_pc = snes_to_pc(room_data_snes)
|
||||
|
||||
result['data_address'] = room_data_pc
|
||||
|
||||
if verbose:
|
||||
print(f"\n{'='*70}")
|
||||
print(f"ROOM {room_id:03d} (0x{room_id:03X}) OBJECT ANALYSIS")
|
||||
print(f"{'='*70}")
|
||||
print(f"Room data at PC: 0x{room_data_pc:05X} (SNES: 0x{room_data_snes:06X})")
|
||||
|
||||
# First 2 bytes: floor graphics and layout
|
||||
floor_byte = rom_data[room_data_pc]
|
||||
layout_byte = rom_data[room_data_pc + 1]
|
||||
|
||||
result['floor1'] = floor_byte & 0x0F
|
||||
result['floor2'] = (floor_byte >> 4) & 0x0F
|
||||
result['layout'] = (layout_byte >> 2) & 0x07
|
||||
|
||||
if verbose:
|
||||
print(f"Floor: BG1={result['floor1']}, BG2={result['floor2']}, Layout={result['layout']}")
|
||||
|
||||
# Parse objects starting at offset 2
|
||||
pos = room_data_pc + 2
|
||||
layer = 0
|
||||
|
||||
if verbose:
|
||||
print(f"\n{'='*70}")
|
||||
print("OBJECTS (Layer 0=BG1 main, Layer 1=BG2 overlay, Layer 2=BG1 priority)")
|
||||
print(f"{'='*70}")
|
||||
|
||||
while pos + 2 < len(rom_data):
|
||||
b1 = rom_data[pos]
|
||||
b2 = rom_data[pos + 1]
|
||||
|
||||
# Check for layer terminator (0xFFFF)
|
||||
if b1 == 0xFF and b2 == 0xFF:
|
||||
if verbose:
|
||||
print(f"\n--- Layer {layer} END ---")
|
||||
pos += 2
|
||||
layer += 1
|
||||
if layer >= 3:
|
||||
break
|
||||
if verbose:
|
||||
print(f"\n--- Layer {layer} START ---")
|
||||
continue
|
||||
|
||||
# Check for door section marker (0xF0FF)
|
||||
if b1 == 0xF0 and b2 == 0xFF:
|
||||
if verbose:
|
||||
print(f"\n--- Doors ---")
|
||||
pos += 2
|
||||
while pos + 1 < len(rom_data):
|
||||
d1 = rom_data[pos]
|
||||
d2 = rom_data[pos + 1]
|
||||
if d1 == 0xFF and d2 == 0xFF:
|
||||
break
|
||||
door = {
|
||||
'position': (d1 >> 4) & 0x0F,
|
||||
'direction': d1 & 0x03,
|
||||
'type': d2
|
||||
}
|
||||
result['doors'].append(door)
|
||||
if verbose:
|
||||
print(f" Door: pos={door['position']}, dir={door['direction']}, type=0x{door['type']:02X}")
|
||||
pos += 2
|
||||
continue
|
||||
|
||||
# Read 3rd byte for object
|
||||
b3 = rom_data[pos + 2]
|
||||
pos += 3
|
||||
|
||||
obj = decode_object(b1, b2, b3, layer)
|
||||
result['objects_by_layer'][layer].append(obj)
|
||||
|
||||
if verbose:
|
||||
desc = get_object_description(obj['id'])
|
||||
routine = get_draw_routine(obj['id'])
|
||||
layer_names = ["BG1_Main", "BG2_Overlay", "BG1_Priority"]
|
||||
routine_str = f" [{routine}]" if routine else ""
|
||||
print(f" L{layer} ({layer_names[layer]}): [{b1:02X} {b2:02X} {b3:02X}] -> "
|
||||
f"T{obj['type']} ID=0x{obj['id']:03X} @ ({obj['x']:2d},{obj['y']:2d}) "
|
||||
f"sz={obj['size']:2d} - {desc}{routine_str}")
|
||||
|
||||
# Summary
|
||||
if verbose:
|
||||
print(f"\n{'='*70}")
|
||||
print("SUMMARY")
|
||||
print(f"{'='*70}")
|
||||
for layer_num, layer_name in [(0, "BG1 Main"), (1, "BG2 Overlay"), (2, "BG1 Priority")]:
|
||||
objs = result['objects_by_layer'][layer_num]
|
||||
print(f"Layer {layer_num} ({layer_name}): {len(objs)} objects")
|
||||
if objs:
|
||||
id_counts = {}
|
||||
for obj in objs:
|
||||
id_counts[obj['id']] = id_counts.get(obj['id'], 0) + 1
|
||||
for obj_id, count in sorted(id_counts.items()):
|
||||
desc = get_object_description(obj_id)
|
||||
print(f" 0x{obj_id:03X}: {count}x - {desc}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def analyze_layer_compositing(result: Dict, verbose: bool = True) -> Dict:
|
||||
"""Analyze layer compositing issues for a room."""
|
||||
analysis = {
|
||||
'has_bg2_objects': len(result['objects_by_layer'][1]) > 0,
|
||||
'bg2_object_count': len(result['objects_by_layer'][1]),
|
||||
'bg2_objects': result['objects_by_layer'][1],
|
||||
'same_floor_graphics': result['floor1'] == result['floor2'],
|
||||
'potential_issues': []
|
||||
}
|
||||
|
||||
if analysis['has_bg2_objects'] and analysis['same_floor_graphics']:
|
||||
analysis['potential_issues'].append(
|
||||
"BG2 overlay objects with same floor graphics - may have compositing issues"
|
||||
)
|
||||
|
||||
if verbose and analysis['has_bg2_objects']:
|
||||
print(f"\n{'='*70}")
|
||||
print("LAYER COMPOSITING ANALYSIS")
|
||||
print(f"{'='*70}")
|
||||
print(f"\nBG2 Overlay objects ({analysis['bg2_object_count']}):")
|
||||
for obj in analysis['bg2_objects']:
|
||||
desc = get_object_description(obj['id'])
|
||||
print(f" ID=0x{obj['id']:03X} @ ({obj['x']},{obj['y']}) size={obj['size']} - {desc}")
|
||||
|
||||
if analysis['potential_issues']:
|
||||
print("\nPotential Issues:")
|
||||
for issue in analysis['potential_issues']:
|
||||
print(f" - {issue}")
|
||||
|
||||
return analysis
|
||||
|
||||
|
||||
def find_rom_file(specified_path: Optional[str] = None) -> Optional[str]:
|
||||
"""Find a valid ROM file."""
|
||||
if specified_path:
|
||||
if os.path.isfile(specified_path):
|
||||
return specified_path
|
||||
print(f"Error: ROM file not found: {specified_path}")
|
||||
return None
|
||||
|
||||
# Try default paths relative to script location
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(script_dir)
|
||||
|
||||
for rel_path in DEFAULT_ROM_PATHS:
|
||||
full_path = os.path.join(project_root, rel_path)
|
||||
if os.path.isfile(full_path):
|
||||
return full_path
|
||||
|
||||
print("Error: Could not find ROM file. Please specify with --rom")
|
||||
print("Tried paths:")
|
||||
for rel_path in DEFAULT_ROM_PATHS:
|
||||
print(f" {os.path.join(project_root, rel_path)}")
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Analyze dungeon room objects from ALTTP ROM",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
%(prog)s 1 # Analyze room 001
|
||||
%(prog)s 1 2 3 # Analyze rooms 001, 002, 003
|
||||
%(prog)s --range 0 10 # Analyze rooms 0-10
|
||||
%(prog)s --all # Analyze all rooms (summary only)
|
||||
%(prog)s --list-bg2 # List rooms with BG2 overlay objects
|
||||
%(prog)s 1 --json # Output as JSON
|
||||
%(prog)s 1 --compositing # Include layer compositing analysis
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument('rooms', nargs='*', type=int, help='Room ID(s) to analyze')
|
||||
parser.add_argument('--rom', '-r', type=str, help='Path to ROM file')
|
||||
parser.add_argument('--range', nargs=2, type=int, metavar=('START', 'END'),
|
||||
help='Analyze range of rooms (inclusive)')
|
||||
parser.add_argument('--all', action='store_true', help='Analyze all rooms (summary only)')
|
||||
parser.add_argument('--json', '-j', action='store_true', help='Output as JSON')
|
||||
parser.add_argument('--quiet', '-q', action='store_true', help='Minimal output')
|
||||
parser.add_argument('--compositing', '-c', action='store_true',
|
||||
help='Include layer compositing analysis')
|
||||
parser.add_argument('--list-bg2', action='store_true',
|
||||
help='List all rooms with BG2 overlay objects')
|
||||
parser.add_argument('--summary', '-s', action='store_true',
|
||||
help='Show summary only (object counts)')
|
||||
|
||||
# Collision offset features
|
||||
parser.add_argument('--collision', action='store_true',
|
||||
help='Calculate collision map offsets for objects')
|
||||
parser.add_argument('--filter-id', type=lambda x: int(x, 0), metavar='ID',
|
||||
help='Filter objects by ID (e.g., 0xD9 or 217)')
|
||||
parser.add_argument('--asm', action='store_true',
|
||||
help='Output collision offsets in ASM format')
|
||||
parser.add_argument('--area', action='store_true',
|
||||
help='Expand objects to full tile area (not just origin)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Find ROM file
|
||||
rom_path = find_rom_file(args.rom)
|
||||
if not rom_path:
|
||||
sys.exit(1)
|
||||
|
||||
# Load ROM
|
||||
if not args.quiet:
|
||||
print(f"Loading ROM: {rom_path}")
|
||||
with open(rom_path, 'rb') as f:
|
||||
rom_data = f.read()
|
||||
if not args.quiet:
|
||||
print(f"ROM size: {len(rom_data)} bytes")
|
||||
|
||||
# Determine rooms to analyze
|
||||
room_ids = []
|
||||
if args.all or args.list_bg2:
|
||||
room_ids = list(range(NUMBER_OF_ROOMS))
|
||||
elif args.range:
|
||||
room_ids = list(range(args.range[0], args.range[1] + 1))
|
||||
elif args.rooms:
|
||||
room_ids = args.rooms
|
||||
else:
|
||||
# Default to room 1 if nothing specified
|
||||
room_ids = [1]
|
||||
|
||||
# Validate room IDs
|
||||
room_ids = [r for r in room_ids if 0 <= r < NUMBER_OF_ROOMS]
|
||||
|
||||
if not room_ids:
|
||||
print("Error: No valid room IDs specified")
|
||||
sys.exit(1)
|
||||
|
||||
# Analyze rooms
|
||||
all_results = []
|
||||
verbose = not (args.quiet or args.json or args.list_bg2 or args.all or args.asm)
|
||||
|
||||
for room_id in room_ids:
|
||||
try:
|
||||
result = parse_room_objects(rom_data, room_id, verbose=verbose)
|
||||
|
||||
if args.compositing:
|
||||
result['compositing'] = analyze_layer_compositing(result, verbose=verbose)
|
||||
|
||||
if args.collision:
|
||||
collision_verbose = not (args.asm or args.quiet)
|
||||
result['collision'] = analyze_collision_offsets(
|
||||
result,
|
||||
filter_id=args.filter_id,
|
||||
expand_area=args.area,
|
||||
asm_output=args.asm,
|
||||
verbose=collision_verbose
|
||||
)
|
||||
|
||||
all_results.append(result)
|
||||
|
||||
except Exception as e:
|
||||
if not args.quiet:
|
||||
print(f"Error analyzing room {room_id}: {e}")
|
||||
|
||||
# Output results
|
||||
if args.collision and args.asm:
|
||||
# Already output by analyze_collision_offsets
|
||||
pass
|
||||
|
||||
elif args.json:
|
||||
# Convert to JSON-serializable format
|
||||
for result in all_results:
|
||||
result['objects_by_layer'] = {
|
||||
str(k): v for k, v in result['objects_by_layer'].items()
|
||||
}
|
||||
print(json.dumps(all_results, indent=2))
|
||||
|
||||
elif args.list_bg2:
|
||||
print(f"\n{'='*70}")
|
||||
print("ROOMS WITH BG2 OVERLAY OBJECTS")
|
||||
print(f"{'='*70}")
|
||||
rooms_with_bg2 = []
|
||||
for result in all_results:
|
||||
bg2_count = len(result['objects_by_layer'][1])
|
||||
if bg2_count > 0:
|
||||
rooms_with_bg2.append((result['room_id'], bg2_count))
|
||||
|
||||
print(f"\nFound {len(rooms_with_bg2)} rooms with BG2 overlay objects:")
|
||||
for room_id, count in sorted(rooms_with_bg2):
|
||||
print(f" Room {room_id:03d} (0x{room_id:03X}): {count} BG2 objects")
|
||||
|
||||
elif args.all or args.summary:
|
||||
print(f"\n{'='*70}")
|
||||
print("ROOM SUMMARY")
|
||||
print(f"{'='*70}")
|
||||
print(f"{'Room':>6} {'L0':>4} {'L1':>4} {'L2':>4} {'Doors':>5} {'Floor':>8}")
|
||||
print("-" * 40)
|
||||
for result in all_results:
|
||||
l0 = len(result['objects_by_layer'][0])
|
||||
l1 = len(result['objects_by_layer'][1])
|
||||
l2 = len(result['objects_by_layer'][2])
|
||||
doors = len(result['doors'])
|
||||
floor = f"{result['floor1']}/{result['floor2']}"
|
||||
print(f"{result['room_id']:>6} {l0:>4} {l1:>4} {l2:>4} {doors:>5} {floor:>8}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user