#!/usr/bin/env python3 """ Comprehensive Binary Model Parser for GoldenEye 007 Parses N64 binary model files (.bin) and generates C source code (Model.c). Binary Format: Offset 0x00: Switches array (if numSwitches > 0) Offset 0x??: Texture table (ModelFileTextures[numtextures]) Offset 0x??: RootNode and complete ModelNode tree with embedded data All pointers in the binary are stored as offsets from base address 0x05000000. The game's load_object_fill_header() converts these to RAM pointers at runtime. Usage: python3 scripts/generate_prop_model_c.py [--dry-run] [--force] [prop ...] """ import struct import sys import re from pathlib import Path from typing import Dict, List, Tuple, Optional, Any from dataclasses import dataclass, field # Constants BASE_ADDRESS = 0x05000000 VERTEX_SIZE = 16 # sizeof(Vertex) GFX_SIZE = 8 # sizeof(Gfx) # ModelNode Opcodes OPCODES = { 0: "NULL", 1: "HEADER", 2: "GROUP", 3: "OP03", 4: "DL", 5: "OP05", 6: "OP06", 7: "OP07", 8: "LOD", 9: "BSP", 10: "BBOX", 11: "OP11", 12: "GUNFIRE", 13: "SHADOW", 14: "OP14", 15: "INTERLINK", 16: "OP16", 17: "OP17", 18: "SWITCH", 19: "OP19", 20: "OP20", 21: "GROUPSIMPLE", 22: "DLPRIMARY", 23: "HEAD", 24: "DLCOLLISION", } # Binary reader utilities def read_u32(data, offset): return struct.unpack('>I', data[offset:offset+4])[0] def read_s32(data, offset): return struct.unpack('>i', data[offset:offset+4])[0] def read_u16(data, offset): return struct.unpack('>H', data[offset:offset+2])[0] def read_s16(data, offset): return struct.unpack('>h', data[offset:offset+2])[0] def read_u8(data, offset): return data[offset] def read_s8(data, offset): return struct.unpack('>b', bytes([data[offset]]))[0] def read_float(data, offset): return struct.unpack('>f', data[offset:offset+4])[0] def float_to_c(f: float) -> str: """Convert float to C literal (IDO doesn't support hex float literals)""" if f == 0.0: return "0.0f" elif f == 1.0: return "1.0f" elif f == -1.0: return "-1.0f" else: # Use sufficient precision for exact binary representation return f"{f}f" @dataclass class ModelTexture: texture_id: int width: int height: int mipmaptiles: int type: int renderdepth: int sflags: int tflags: int @dataclass class Vertex: x: int y: int z: int flag: int s: int t: int r: int g: int b: int a: int @dataclass class ModelNode: offset: int # Offset in binary where this node is located opcode: int opcode_flags: int = 0 # High byte of the opcode field (UseAdditionalMatrices flag for chr models) data_offset: int = 0 parent_offset: int = 0 next_offset: int = 0 prev_offset: int = 0 child_offset: int = 0 data: Any = None # Parsed data structure based on opcode class BinaryModelParser: def __init__(self, binary_data: bytes, metadata: dict, image_map: dict = None): self.data = binary_data self.metadata = metadata self.nodes = {} # offset -> ModelNode self.referenced_data = {} # offset -> parsed data structure (no ModelNode) self.textures = [] self.switches = [] self.base_address = BASE_ADDRESS self.image_map = image_map or {} def to_file_offset(self, addr: int) -> int: """Convert base-relative address to file offset. Returns -1 for NULL (0x00000000)""" if addr == 0: return -1 # NULL pointer if addr < self.base_address: return addr # Already a file offset return addr - self.base_address def offset_to_ptr_name(self, offset: int) -> str: """Convert binary offset to C pointer/symbol name""" if offset == 0: return "NULL" # Check if it's a known node if offset in self.nodes: return f"&ModelNode_0x{offset:03x}" # It's some data structure return f"0x{offset:08x}" # Will need resolving later def parse(self): """Parse the complete binary model file""" num_switches = self.metadata.get('num_switches', 0) num_textures = self.metadata['num_textures'] offset = 0 # Parse switches array (array of u32 pointers) if num_switches > 0: for i in range(num_switches): switch_ptr = read_u32(self.data, offset) # Convert absolute address (0x05xxxxxx) to relative offset if switch_ptr > 0: switch_offset = switch_ptr - BASE_ADDRESS else: switch_offset = 0 self.switches.append(switch_offset) offset += 4 # Parse texture table (12 bytes per texture) for i in range(num_textures): tex = ModelTexture( texture_id=read_u32(self.data, offset), width=read_u8(self.data, offset + 4), height=read_u8(self.data, offset + 5), mipmaptiles=read_u8(self.data, offset + 6), type=read_u8(self.data, offset + 7), renderdepth=read_u8(self.data, offset + 8), sflags=read_u8(self.data, offset + 9), tflags=read_u8(self.data, offset + 10), ) self.textures.append(tex) offset += 12 # Parse ModelNode tree (starts after textures) # RootNode is at this offset root_offset = offset self.parse_node_tree(root_offset) # Second pass: parse structures referenced by pointers but not in ModelNode tree self.parse_referenced_structures() return { 'switches': self.switches, 'textures': self.textures, 'nodes': self.nodes, 'referenced_data': self.referenced_data, 'root_offset': root_offset } def parse_node_tree(self, node_offset: int): """Recursively parse ModelNode tree structure""" if node_offset in self.nodes: return # Parse ModelNode structure (24 bytes) # typedef struct ModelNode { # u8 UseAdditionalMatrices; /*0x00 - high byte of opcode field, used in chr models*/ # u8 Opcode; /*0x01 - low byte*/ # u16 padding; /*0x02*/ # union ModelRoData *Data; /*0x04*/ # struct ModelNode *Parent; /*0x08*/ # struct ModelNode *Next; /*0x0c*/ # struct ModelNode *Prev; /*0x10*/ # struct ModelNode *Child; /*0x14*/ # } ModelNode; # Note: Officially it's "u16 Opcode" but chr models use both bytes separately opcode_u16 = read_u16(self.data, node_offset) opcode_flags = (opcode_u16 >> 8) & 0xFF # High byte opcode = opcode_u16 & 0xFF # Low byte data_offset = self.to_file_offset(read_u32(self.data, node_offset + 4)) parent_offset = self.to_file_offset(read_u32(self.data, node_offset + 8)) next_offset = self.to_file_offset(read_u32(self.data, node_offset + 12)) prev_offset = self.to_file_offset(read_u32(self.data, node_offset + 16)) child_offset = self.to_file_offset(read_u32(self.data, node_offset + 20)) # Create node node = ModelNode( offset=node_offset, opcode=opcode, opcode_flags=opcode_flags, data_offset=data_offset, parent_offset=parent_offset, next_offset=next_offset, prev_offset=prev_offset, child_offset=child_offset ) self.nodes[node_offset] = node # Parse the data structure for this opcode if data_offset > 0: node.data = self.parse_node_data(node.opcode, data_offset) # Recursively parse linked nodes (but not parent to avoid cycles) if child_offset > 0: self.parse_node_tree(child_offset) if next_offset > 0: self.parse_node_tree(next_offset) def parse_referenced_structures(self): """Parse structures referenced by pointers that aren't in ModelNode tree""" # Collect all referenced offsets referenced_offsets = set() for node_offset, node in self.nodes.items(): if not node.data: continue dtype = node.data.get('_type') # HeaderRecord.FirstGroup actually points to a ModelNode (not directly to GroupRecord) # The ModelNode is already in the tree, so no need to parse as referenced structure # LODRecord.Children point to ModelNodes with data if dtype == 'LODRecord': for i in range(3): child_offset = node.data.get(f'child{i}_offset', 0) if child_offset > 0 and child_offset in self.nodes: child_node = self.nodes[child_offset] if child_node.data_offset > 0: # Determine opcode from child node ref_opcode = child_node.opcode referenced_offsets.add((child_node.data_offset, ref_opcode)) # Parse any referenced structures that don't already have a ModelNode for ref_offset, ref_opcode in referenced_offsets: # Check if this offset already has a node pointing to it already_exists = any( node.data_offset == ref_offset for node in self.nodes.values() ) if not already_exists and ref_offset not in self.referenced_data: # Parse and store as a standalone data structure (not a ModelNode) self.referenced_data[ref_offset] = self.parse_node_data(ref_opcode, ref_offset) def parse_node_data(self, opcode: int, data_offset: int) -> Dict: """Parse the data structure for a specific opcode""" if opcode == 2: # GROUP return self.parse_group_record(data_offset) elif opcode == 9: # BSP return self.parse_bsp_record(data_offset) elif opcode == 10: # BBOX return self.parse_bbox_record(data_offset) elif opcode == 18: # SWITCH return self.parse_switch_record(data_offset) elif opcode == 21: # GROUPSIMPLE return self.parse_groupsimple_record(data_offset) elif opcode == 1: # HEADER return self.parse_header_record(data_offset) elif opcode == 12: # GUNFIRE return self.parse_gunfire_record(data_offset) elif opcode == 22: # DLPRIMARY return self.parse_dlprimary_record(data_offset) elif opcode == 24: # DLCOLLISION return self.parse_dlcollision_record(data_offset) elif opcode == 4: # DL return self.parse_dl_record(data_offset) elif opcode == 8: # LOD return self.parse_lod_record(data_offset) elif opcode == 23: # HEAD return self.parse_head_placeholder_record(data_offset) elif opcode == 13: # SHADOW return self.parse_shadow_record(data_offset) elif opcode == 15: # INTERLINK return self.parse_interlink_record(data_offset) # Add more as needed else: return {'_raw_offset': data_offset} def parse_group_record(self, offset: int) -> Dict: """Parse ModelRoData_GroupRecord (28 bytes)""" return { '_type': 'GroupRecord', 'origin_x': read_float(self.data, offset), 'origin_y': read_float(self.data, offset + 4), 'origin_z': read_float(self.data, offset + 8), 'joint_id': read_u16(self.data, offset + 12), 'matrix_id0': read_s16(self.data, offset + 14), 'matrix_id1': read_s16(self.data, offset + 16), 'matrix_id2': read_s16(self.data, offset + 18), 'child_group_offset': self.to_file_offset(read_u32(self.data, offset + 20)), 'bounding_volume_radius': read_float(self.data, offset + 24), } def parse_bbox_record(self, offset: int) -> Dict: """Parse ModelRoData_BoundingBoxRecord (28 bytes)""" return { '_type': 'BoundingBoxRecord', 'model_number': read_u32(self.data, offset), 'xmin': read_float(self.data, offset + 4), 'xmax': read_float(self.data, offset + 8), 'ymin': read_float(self.data, offset + 12), 'ymax': read_float(self.data, offset + 16), 'zmin': read_float(self.data, offset + 20), 'zmax': read_float(self.data, offset + 24), } def parse_lod_record(self, offset: int) -> Dict: """Parse ModelRoData_LODRecord (16 bytes) Structure (from bondtypes.h): f32 MinDistance; // 0x0 f32 MaxDistance; // 0x4 ModelNode *Affects; // 0x8 (runtime pointer, set to NULL in binary) u16 RwDataIndex; // 0xC u16 reserved; // 0xE Total: 16 bytes """ return { '_type': 'LODRecord', 'min_distance': read_float(self.data, offset + 0x0), 'max_distance': read_float(self.data, offset + 0x4), 'child_node_ptr': read_u32(self.data, offset + 0x8), # Runtime pointer 'rw_data_index': read_u16(self.data, offset + 0xC), 'reserved': read_u16(self.data, offset + 0xE), '_binary_size': 16, # Actual size in binary } def parse_dlcollision_record(self, offset: int) -> Dict: """Parse ModelRoData_DisplayList_CollisionRecord (32 bytes) Structure (from bondtypes.h): Gfx *Primary; // 0x00 - Primary display list Gfx *Secondary; // 0x04 - Secondary display list (optional) Vertex *Vertices; // 0x08 - Render vertices s16 numVertices; // 0x0C - Number of render vertices s16 numCollisionVertices; // 0x0E - Number of collision vertices Vertex *CollisionVertices; // 0x10 - Collision vertices s16 *PointUsage; // 0x14 - Point usage array s16 ModelType; // 0x18 - Model type flags u16 RwDataIndex; // 0x1A - Runtime data index void *BaseAddr; // 0x1C - Runtime base address Total: 32 bytes """ primary_offset = self.to_file_offset(read_u32(self.data, offset + 0x0)) secondary_offset = self.to_file_offset(read_u32(self.data, offset + 0x4)) vertices_offset = self.to_file_offset(read_u32(self.data, offset + 0x8)) num_vertices = read_s16(self.data, offset + 0xC) num_collision_vertices = read_s16(self.data, offset + 0xE) collision_vertices_offset = self.to_file_offset(read_u32(self.data, offset + 0x10)) point_usage_offset = self.to_file_offset(read_u32(self.data, offset + 0x14)) model_type = read_s16(self.data, offset + 0x18) rw_data_index = read_u16(self.data, offset + 0x1A) # base_addr at 0x1C is runtime pointer, not needed # Parse vertex arrays vertices = self.parse_vertex_array(vertices_offset, num_vertices) if vertices_offset > 0 else [] collision_vertices = self.parse_vertex_array(collision_vertices_offset, num_collision_vertices) if collision_vertices_offset > 0 else [] point_usage = self.parse_point_usage(point_usage_offset, num_vertices) if point_usage_offset > 0 else [] # Parse GDL commands primary_gfx = self.parse_gfx_list(primary_offset, vertices_offset) if primary_offset > 0 else [] secondary_gfx = self.parse_gfx_list(secondary_offset, vertices_offset) if secondary_offset > 0 else [] return { '_type': 'DisplayListCollisionRecord', 'primary_offset': primary_offset, 'secondary_offset': secondary_offset, 'vertices_offset': vertices_offset, 'collision_vertices_offset': collision_vertices_offset, 'point_usage_offset': point_usage_offset, 'vertices': vertices, 'collision_vertices': collision_vertices, 'point_usage': point_usage, 'model_type': model_type, 'rw_data_index': rw_data_index, 'primary_gfx': primary_gfx, 'secondary_gfx': secondary_gfx, } def parse_dl_record(self, offset: int) -> Dict: """Parse ModelRoData_DisplayListRecord (19 bytes logical, 20 with padding)""" return { '_type': 'DisplayListRecord', 'primary_offset': self.to_file_offset(read_u32(self.data, offset)), 'secondary_offset': self.to_file_offset(read_u32(self.data, offset + 4)), # More fields... } def parse_bsp_record(self, offset: int) -> Dict: """Parse ModelRoData_BSPRecord (36 bytes total)""" return { '_type': 'BSPRecord', 'point_x': read_float(self.data, offset), 'point_y': read_float(self.data, offset + 4), 'point_z': read_float(self.data, offset + 8), 'vector_x': read_float(self.data, offset + 12), 'vector_y': read_float(self.data, offset + 16), 'vector_z': read_float(self.data, offset + 20), 'left_child_offset': self.to_file_offset(read_u32(self.data, offset + 24)), 'right_child_offset': self.to_file_offset(read_u32(self.data, offset + 28)), 'reserved': read_s16(self.data, offset + 32), 'rw_data_index': read_u16(self.data, offset + 34), } def parse_switch_record(self, offset: int) -> Dict: """Parse ModelRoData_SwitchRecord (8 bytes)""" return { '_type': 'SwitchRecord', 'controls_offset': self.to_file_offset(read_u32(self.data, offset)), 'rw_data_index': read_u16(self.data, offset + 4), 'reserved': read_u16(self.data, offset + 6), } def parse_interlink_record(self, offset: int) -> Dict: """Parse ModelRoData_InterlinkageRecord (28 bytes) Structure (from bondtypes.h): coord3d pos; // 0x0 - position (3 floats) u32 unknown1; // 0xC u32 unknown2; // 0x10 u32 unknown3; // 0x14 f32 Scale; // 0x18 Total: 28 bytes (0x1C) """ return { '_type': 'InterlinkageRecord', 'pos_x': read_float(self.data, offset + 0x0), 'pos_y': read_float(self.data, offset + 0x4), 'pos_z': read_float(self.data, offset + 0x8), 'unknown1': read_u32(self.data, offset + 0xC), 'unknown2': read_u32(self.data, offset + 0x10), 'unknown3': read_u32(self.data, offset + 0x14), 'scale': read_float(self.data, offset + 0x18), } def parse_groupsimple_record(self, offset: int) -> Dict: """Parse ModelRoData_GroupSimpleRecord (20 bytes)""" return { '_type': 'GroupSimpleRecord', 'origin_x': read_float(self.data, offset), 'origin_y': read_float(self.data, offset + 4), 'origin_z': read_float(self.data, offset + 8), 'group1': read_s16(self.data, offset + 12), 'group2': read_u16(self.data, offset + 14), 'bounding_volume_radius': read_float(self.data, offset + 16), } def parse_header_record(self, offset: int) -> Dict: """Parse ModelRoData_HeaderRecord (16 bytes)""" return { '_type': 'HeaderRecord', 'model_type': read_u32(self.data, offset), 'first_group_offset': self.to_file_offset(read_u32(self.data, offset + 4)), 'group1': read_u16(self.data, offset + 8), 'group2': read_u16(self.data, offset + 10), 'rw_data_index': read_u16(self.data, offset + 12), 'reserved': read_u16(self.data, offset + 14), } def parse_head_placeholder_record(self, offset: int) -> Dict: """Parse ModelRoData_HeadPlaceholderRecord (4 bytes with padding) Structure (from bondtypes.h): u16 RwDataIndex; // 0x0 u16 padding; // 0x2 (implicit padding to 4 bytes) Total: 4 bytes """ return { '_type': 'HeadPlaceholderRecord', 'rw_data_index': read_u16(self.data, offset), 'padding': read_u16(self.data, offset + 2), } def parse_shadow_record(self, offset: int) -> Dict: """Parse ModelRoData_ShadowRecord (32 bytes) Structure (from bondtypes.h): coord2d pos; // 0x0 (2 floats) coord2d size; // 0x8 (2 floats) void *image; // 0x10 ModelRoData_HeaderRecord *Header; // 0x14 (pointer to header node) f32 Scale; // 0x18 void *BaseAddr; // 0x1C Total: 32 bytes (0x20) """ image_ptr = read_u32(self.data, offset + 0x10) image_offset = self.to_file_offset(image_ptr) if image_ptr != 0 else 0 header_ptr = read_u32(self.data, offset + 0x14) header_offset = self.to_file_offset(header_ptr) if header_ptr != 0 else 0 return { '_type': 'ShadowRecord', 'pos_x': read_float(self.data, offset + 0x0), 'pos_y': read_float(self.data, offset + 0x4), 'size_x': read_float(self.data, offset + 0x8), 'size_y': read_float(self.data, offset + 0xC), 'image_offset': image_offset, 'header_offset': header_offset, 'scale': read_float(self.data, offset + 0x18), 'base_addr': read_u32(self.data, offset + 0x1C), } def parse_gunfire_record(self, offset: int) -> Dict: """Parse ModelRoData_GunfireRecord (40 bytes) Structure (from bondtypes.h): coord3d Offset; // 0x0 (3 floats) coord3d Size; // 0xC (3 floats) void *Image; // 0x18 (pointer to texture) f32 Scale; // 0x1C u16 RwDataIndex; // 0x20 u16 reserved; // 0x22 u32 BaseAddr; // 0x24 Total: 40 bytes (0x28) """ image_ptr = read_u32(self.data, offset + 0x18) image_offset = self.to_file_offset(image_ptr) if image_ptr != 0 else 0 return { '_type': 'GunfireRecord', 'offset_x': read_float(self.data, offset + 0x0), 'offset_y': read_float(self.data, offset + 0x4), 'offset_z': read_float(self.data, offset + 0x8), 'size_x': read_float(self.data, offset + 0xC), 'size_y': read_float(self.data, offset + 0x10), 'size_z': read_float(self.data, offset + 0x14), 'image_offset': image_offset, 'scale': read_float(self.data, offset + 0x1C), 'rw_data_index': read_u16(self.data, offset + 0x20), 'reserved': read_u16(self.data, offset + 0x22), 'base_addr': read_u32(self.data, offset + 0x24), '_binary_size': 40, } def parse_dlprimary_record(self, offset: int) -> Dict: """Parse ModelRoData_DisplayListPrimaryRecord (16 bytes) Structure (from bondtypes.h): s32 numVertices; // 0x0 Vertex *Vertices; // 0x4 Gfx *Primary; // 0x8 void *BaseAddr; // 0xC Total: 16 bytes """ num_vertices = read_s32(self.data, offset) vertices_ptr = read_u32(self.data, offset + 4) primary_ptr = read_u32(self.data, offset + 8) base_addr = read_u32(self.data, offset + 12) vertices_offset = self.to_file_offset(vertices_ptr) if vertices_ptr != 0 else 0 primary_offset = self.to_file_offset(primary_ptr) if primary_ptr != 0 else 0 # Parse vertex array if present vertices = [] if vertices_offset > 0 and num_vertices > 0: for i in range(num_vertices): v_offset = vertices_offset + i * 16 vertices.append(Vertex( x=read_s16(self.data, v_offset + 0), y=read_s16(self.data, v_offset + 2), z=read_s16(self.data, v_offset + 4), flag=read_u16(self.data, v_offset + 6), s=read_s16(self.data, v_offset + 8), t=read_s16(self.data, v_offset + 10), r=read_u8(self.data, v_offset + 12), g=read_u8(self.data, v_offset + 13), b=read_u8(self.data, v_offset + 14), a=read_u8(self.data, v_offset + 15) )) # Parse Gfx array primary_gfx, _ = parse_gfx_array(self.data, primary_offset, self.base_address) if primary_offset > 0 else ([], 0) return { '_type': 'DisplayListPrimaryRecord', 'num_vertices': num_vertices, 'vertices_offset': vertices_offset, 'primary_offset': primary_offset, 'base_addr': base_addr, 'vertices': vertices, 'primary_gfx': primary_gfx, '_binary_size': 16, } def parse_dl_record(self, offset: int) -> Dict: """Parse ModelRoData_DisplayListRecord (19 bytes)""" primary_offset = self.to_file_offset(read_u32(self.data, offset)) secondary_offset = self.to_file_offset(read_u32(self.data, offset + 4)) vertices_offset = self.to_file_offset(read_u32(self.data, offset + 12)) num_vertices = read_u16(self.data, offset + 16) # Extract vertex array if present vertices = [] if vertices_offset > 0 and num_vertices > 0: vertices = self.parse_vertex_array(vertices_offset, num_vertices) # Extract Gfx display lists if present primary_gfx = [] if primary_offset > 0: primary_gfx = self.parse_gfx_list(primary_offset, vertices_offset) secondary_gfx = [] if secondary_offset > 0: secondary_gfx = self.parse_gfx_list(secondary_offset, vertices_offset) return { '_type': 'DisplayListRecord', 'primary_offset': primary_offset, 'secondary_offset': secondary_offset, 'base_addr': read_u32(self.data, offset + 8), 'vertices_offset': vertices_offset, 'num_vertices': num_vertices, 'model_type': read_s8(self.data, offset + 18), 'vertices': vertices, 'primary_gfx': primary_gfx, 'secondary_gfx': secondary_gfx, } def parse_vertex_array(self, offset: int, count: int) -> List[Vertex]: """Parse Vertex array (16 bytes per vertex)""" vertices = [] for i in range(count): v_off = offset + i * 16 v = Vertex( x=read_s16(self.data, v_off), y=read_s16(self.data, v_off + 2), z=read_s16(self.data, v_off + 4), flag=read_u16(self.data, v_off + 6), s=read_s16(self.data, v_off + 8), t=read_s16(self.data, v_off + 10), r=read_u8(self.data, v_off + 12), g=read_u8(self.data, v_off + 13), b=read_u8(self.data, v_off + 14), a=read_u8(self.data, v_off + 15), ) vertices.append(v) return vertices def parse_point_usage(self, offset: int, count: int) -> List[int]: """Parse point usage array (s16[])""" return [read_s16(self.data, offset + i * 2) for i in range(count)] def parse_gfx_list(self, offset: int, vertices_offset: int = None) -> List[str]: """Parse Gfx display list commands (8 bytes each) until final end marker Returns decoded command strings ready for C output vertices_offset: File offset where vertex array starts (for resolving vertex addresses) """ if offset <= 0 or offset >= len(self.data): return [] # Prepare vertex array name for address resolution vtx_array_name = f"Vertex_0x{vertices_offset:03x}" if vertices_offset else None gfx_cmds = [] while offset + 8 <= len(self.data): w0 = read_u32(self.data, offset) w1 = read_u32(self.data, offset + 4) # Decode command to macro format with image_map for texture lookup decoded = decode_gfx_command(w0, w1, vtx_array_name, vertices_offset, self.image_map) gfx_cmds.append(decoded) opcode = (w0 >> 24) & 0xFF # Stop at B8 (final end marker), but continue past E7 (initial marker) if opcode == 0xB8: break offset += 8 # Safety limit to prevent infinite loops if len(gfx_cmds) > 10000: break return gfx_cmds def load_image_map(): """Load IMAGE_* to ID mapping from images.def""" image_map = {} images_file = Path("assets/images.def") if images_file.exists(): with open(images_file, 'r') as f: for idx, line in enumerate(f): line = line.strip() if line.startswith('IMAGE('): # Extract the name from IMAGE(NAME, ...) match = re.match(r'IMAGE\(([^,]+),', line) if match: name = match.group(1).strip() # Map index to IMAGE_NAME format (no _ID suffix) image_map[idx] = f"IMAGE_{name}" return image_map def parse_metadata_files(prop_name: str) -> Optional[Dict]: """Parse ModelFileHeader.inc.c and propFileRecord.inc.c""" prop_dir = Path("assets/obseg/prop") / prop_name header_file = prop_dir / "ModelFileHeader.inc.c" record_file = prop_dir / "propFileRecord.inc.c" if not header_file.exists() or not record_file.exists(): return None metadata = {} # Parse ModelFileHeader.inc.c with open(header_file, 'r') as f: content = f.read() # Allow hex or decimal for all numeric fields match = re.search(r'MODELFILEHEADER\s*\(\s*(\w+)\s*,\s*(0x[0-9A-Fa-f]+|\d+)\s*,\s*&SKELETON\((\w+)\)\s*,\s*(0x[0-9A-Fa-f]+|\d+)\s*,\s*(0x[0-9A-Fa-f]+|\d+)\s*,\s*(0x[0-9A-Fa-f]+|\d+)\s*,\s*([\d.]+)\s*,\s*(0x[0-9A-Fa-f]+|\d+)\s*,\s*(0x[0-9A-Fa-f]+|\d+)\s*\)', content) if match: metadata['name'] = match.group(1) metadata['skeleton'] = match.group(3) metadata['num_switches'] = int(match.group(5), 0) metadata['bounding_radius'] = float(match.group(7)) metadata['num_textures'] = int(match.group(9), 0) # 0 base auto-detects hex/decimal # Parse propFileRecord.inc.c with open(record_file, 'r') as f: content = f.read() match = re.search(r'PROPFILERECORD\s*\(\s*\w+\s*,\s*([\d.]+)\s*\)', content) if match: metadata['scale'] = float(match.group(1)) return metadata if 'num_textures' in metadata else None def decode_gfx_command(w0: int, w1: int, vertex_array_name: str = None, vertex_array_offset: int = None, image_map: dict = None) -> str: """ Decode a single Gfx command (w0, w1) to its GBI macro representation. Returns the macro call as a string (e.g., "gsDPPipeSync()"). Falls back to raw hex format if command is unknown. image_map: dict mapping texture_id -> IMAGE_NAME for G_SETTEX Reference: include/PR/gbi.h vertex_array_name: Name of the vertex array (e.g., "Vertex_0x098") vertex_array_offset: File offset where vertex array starts """ opcode = (w0 >> 24) & 0xFF # Helper to resolve segment addresses to symbol names def resolve_address(addr): # All segment addresses (0xXX000000 format) are runtime-resolved # Leave them as raw hex for the game engine to resolve # The segments (3, 4, 5, etc.) are set up at runtime return f"0x{addr:08X}" # Helper to extract bit fields using _SHIFTR logic def extract_bits(value, shift, width): return (value >> shift) & ((1 << width) - 1) # G_IM_FMT_ constants from gbi.h FMT_NAMES = { 0: "G_IM_FMT_RGBA", 1: "G_IM_FMT_YUV", 2: "G_IM_FMT_CI", 3: "G_IM_FMT_IA", 4: "G_IM_FMT_I", } # G_IM_SIZ_ constants from gbi.h SIZ_NAMES = { 0: "G_IM_SIZ_4b", 1: "G_IM_SIZ_8b", 2: "G_IM_SIZ_16b", 3: "G_IM_SIZ_32b", 5: "G_IM_SIZ_DD", } # RDP Sync commands (no parameters) if opcode == 0xE7: return "gsDPPipeSync()" elif opcode == 0xE6: return "gsDPLoadSync()" elif opcode == 0xE8: return "gsDPTileSync()" elif opcode == 0xE9: return "gsDPFullSync()" # G_SETTEX (0xC0) - GoldenEye custom command for texture setup # gsSPUseTexture(cms, cmt, tile, shifts, shiftt, type, minlevel, detail_id, texture_id) elif opcode == 0xC0: cms = extract_bits(w0, 22, 2) cmt = extract_bits(w0, 20, 2) tile = extract_bits(w0, 18, 2) shifts = extract_bits(w0, 14, 4) shiftt = extract_bits(w0, 10, 4) type_val = extract_bits(w0, 0, 3) minlevel = extract_bits(w1, 24, 8) detail_id = extract_bits(w1, 12, 12) texture_id = extract_bits(w1, 0, 12) # Map type value to TextureTypes enum type_names = [ "TEXTURETYPE_LOD", "TEXTURETYPE_DETAIL", "TEXTURETYPE_MIPMAP", "TEXTURETYPE_TILE", "TEXTURETYPE_TILE_PRESWAPPED" ] type_str = type_names[type_val] if type_val < len(type_names) else str(type_val) # Look up IMAGE enum from texture_id if image_map and texture_id in image_map: image_name = image_map[texture_id] return f"gsSPUseTexture({cms}, {cmt}, {tile}, {shifts}, {shiftt}, {type_str}, {minlevel}, {detail_id}, {image_name})" else: # Fallback to raw texture_id if not found in map return f"gsSPUseTexture({cms}, {cmt}, {tile}, {shifts}, {shiftt}, {type_str}, {minlevel}, {detail_id}, 0x{texture_id:03X})" elif opcode == 0x00: return "gsDPNoOp()" # gsSPEndDisplayList (0xB8 or 0xDF depending on mode) elif opcode == 0xB8 or opcode == 0xDF: return "gsSPEndDisplayList()" # gsSPMatrixGE (0x01 G_MTX) - GoldenEye matrix command # Format: w0 = cmd(24-31) | params(16-23) | sizeof(Mtx)(0-15), w1 = address # params bits: projection(0) | load(1) | push(2) elif opcode == 0x01: params = extract_bits(w0, 16, 8) size = extract_bits(w0, 0, 16) addr = w1 addr_str = resolve_address(addr) # Decode matrix parameters param_str = "" if params & 0x01: param_str = "G_MTX_PROJECTION" else: param_str = "G_MTX_MODELVIEW" if params & 0x02: param_str += " | G_MTX_LOAD" else: param_str += " | G_MTX_MUL" if params & 0x04: param_str += " | G_MTX_PUSH" else: param_str += " | G_MTX_NOPUSH" # Output raw bytes with decoded comment return f"gsSPMatrixGE({addr_str}, {param_str})" elif opcode == 0x04: # RARE vertex format: w0 = cmd(24-31) | encoded(16-23) | sizeof(Vtx)*n(0-15) # encoded byte format: ((v0+n) << 1) | flag # Different from standard F3DEX which uses v0*2 and ((n)<<10)|(sizeof(Vtx)*(n)-1) encoded_value = extract_bits(w0, 16, 8) length = extract_bits(w0, 0, 16) flag = encoded_value & 1 v0_plus_n = encoded_value >> 1 n = length // 16 # sizeof(Vtx) = 16 v0 = v0_plus_n - n addr = w1 addr_str = resolve_address(addr) return f"gsSPVertexGE({addr_str}, {n}, {v0}, {flag})" v00 = extract_bits(w0, 16, 8) // 2 v01 = extract_bits(w0, 8, 8) // 2 v02 = extract_bits(w0, 0, 8) // 2 flag0 = 0 # Simplified - actual flag extraction is complex v10 = extract_bits(w1, 16, 8) // 2 v11 = extract_bits(w1, 8, 8) // 2 v12 = extract_bits(w1, 0, 8) // 2 flag1 = 0 return f"gsSP2Triangles({v00}, {v01}, {v02}, {flag0}, {v10}, {v11}, {v12}, {flag1})" # G_TRI4 (0xB1) - GoldenEye extension packing 4 triangles with 4-bit vertex indices # gsSP4Triangles(x1, y1, z1, x2, y2, z2, x3, y3, z3, x4, y4, z4) elif opcode == 0xB1: # Extract from w0: z4(12-15) | z3(8-11) | z2(4-7) | z1(0-3) z1 = extract_bits(w0, 0, 4) z2 = extract_bits(w0, 4, 4) z3 = extract_bits(w0, 8, 4) z4 = extract_bits(w0, 12, 4) # Extract from w1: y4(28-31) | x4(24-27) | y3(20-23) | x3(16-19) | y2(12-15) | x2(8-11) | y1(4-7) | x1(0-3) x1 = extract_bits(w1, 0, 4) y1 = extract_bits(w1, 4, 4) x2 = extract_bits(w1, 8, 4) y2 = extract_bits(w1, 12, 4) x3 = extract_bits(w1, 16, 4) y3 = extract_bits(w1, 20, 4) x4 = extract_bits(w1, 24, 4) y4 = extract_bits(w1, 28, 4) return f"gsSP4Triangles({x1}, {y1}, {z1}, {x2}, {y2}, {z2}, {x3}, {y3}, {z3}, {x4}, {y4}, {z4})" # gsSP1Triangle (0x05 in F3DEX_GBI_2, 0xBF in F3DEX/classic) elif opcode == 0x05: v0 = extract_bits(w1, 16, 8) // 2 v1 = extract_bits(w1, 8, 8) // 2 v2 = extract_bits(w1, 0, 8) // 2 flag = 0 return f"gsSP1Triangle({v0}, {v1}, {v2}, {flag})" # 0xBF - F3DEX gsSP1Triangle (GoldenEye version without flag rotation) elif opcode == 0xBF: v0 = extract_bits(w1, 16, 8) // 2 v1 = extract_bits(w1, 8, 8) // 2 v2 = extract_bits(w1, 0, 8) // 2 flag = 0 return f"gsSP1TriangleGE({v0}, {v1}, {v2}, {flag})" # gsSPTexture (0xD7 in F3DEX_GBI_2, 0xBB in classic) elif opcode == 0xD7 or opcode == 0xBB: # w0 = cmd(24-31) | bowtie(16-23) | level(11-13) | tile(8-10) | on(0-7) # w1 = s(16-31) | t(0-15) bowtie = extract_bits(w0, 16, 8) level = extract_bits(w0, 11, 3) tile = extract_bits(w0, 8, 3) on = extract_bits(w0, 0, 8) s = extract_bits(w1, 16, 16) t = extract_bits(w1, 0, 16) # Use gsSPTextureL if bowtie != 0, otherwise gsSPTexture if bowtie != 0: return f"gsSPTextureL({s}, {t}, {level}, 0x{bowtie:02X}, {tile}, {on})" else: return f"gsSPTexture({s}, {t}, {level}, {tile}, {on})" # gsDPSetCombineMode / gsDPSetCombineLERP (0xFC) elif opcode == 0xFC: # Complex color combiner - show as raw for now muxs0 = w0 & 0xFFFFFF muxs1 = w1 return f"gsDPSetCombine(0x{muxs0:06X}, 0x{muxs1:08X})" # gsSPSetOtherMode_H (0xE3 in F3DEX_GBI_2, 0xBA in classic) elif opcode == 0xE3 or opcode == 0xBA: sft = extract_bits(w0, 8, 8) length = extract_bits(w0, 0, 8) data = w1 # Decode common high-level macros if sft == 16 and length == 1: # gsDPSetTextureLOD if data == 0x00000000: return "gsDPSetTextureLOD(G_TL_TILE)" elif data == 0x00010000: return "gsDPSetTextureLOD(G_TL_LOD)" elif sft == 17 and length == 2: # gsDPSetTextureDetail if data == 0x00000000: return "gsDPSetTextureDetail(G_TD_CLAMP)" elif data == 0x00020000: return "gsDPSetTextureDetail(G_TD_SHARPEN)" elif data == 0x00040000: return "gsDPSetTextureDetail(G_TD_DETAIL)" elif sft == 12 and length == 2: # gsDPSetTextureFilter if data == 0x00000000: return "gsDPSetTextureFilter(G_TF_POINT)" elif data == 0x00002000: return "gsDPSetTextureFilter(G_TF_BILERP)" elif data == 0x00003000: return "gsDPSetTextureFilter(G_TF_AVERAGE)" # Fallback to generic macro return f"gsSPSetOtherMode(G_SETOTHERMODE_H, {sft}, {length}, 0x{data:08X})" # gsSPSetOtherMode_L (0xE2 in F3DEX_GBI_2, 0xB9 in classic) elif opcode == 0xE2 or opcode == 0xB9: sft = extract_bits(w0, 8, 8) length = extract_bits(w0, 0, 8) data = w1 return f"gsSPSetOtherMode(G_SETOTHERMODE_L, {sft}, {length}, 0x{data:08X})" # gsSPGeometryMode (0xD9 in F3DEX_GBI_2) elif opcode == 0xD9: clearbits = (~w0) & 0xFFFFFF setbits = w1 return f"gsSPGeometryMode(0x{clearbits:08X}, 0x{setbits:08X})" # gsSPSetGeometryMode (0xB7 classic, 0xD9 F3DEX2 variant) elif opcode == 0xB7: mode = w1 return f"gsSPSetGeometryMode(0x{mode:08X})" # gsSPClearGeometryMode (0xB6) elif opcode == 0xB6: mode = w1 return f"gsSPClearGeometryMode(0x{mode:08X})" # gsDPSetPrimColor (0xFA) elif opcode == 0xFA: m = extract_bits(w0, 8, 8) l = extract_bits(w0, 0, 8) r = extract_bits(w1, 24, 8) g = extract_bits(w1, 16, 8) b = extract_bits(w1, 8, 8) a = extract_bits(w1, 0, 8) return f"gsDPSetPrimColor({m}, {l}, {r}, {g}, {b}, {a})" # gsDPSetEnvColor (0xFB) elif opcode == 0xFB: r = extract_bits(w1, 24, 8) g = extract_bits(w1, 16, 8) b = extract_bits(w1, 8, 8) a = extract_bits(w1, 0, 8) return f"gsDPSetEnvColor({r}, {g}, {b}, {a})" # gsDPSetTextureImage (0xFD) elif opcode == 0xFD: fmt = extract_bits(w0, 21, 3) siz = extract_bits(w0, 19, 2) width = extract_bits(w0, 0, 12) + 1 addr = w1 fmt_name = FMT_NAMES.get(fmt, f"0x{fmt:X}") siz_name = SIZ_NAMES.get(siz, f"0x{siz:X}") return f"gsDPSetTextureImage({fmt_name}, {siz_name}, {width}, 0x{addr:08X})" # gsDPSetTile (0xF5) - output as raw bytes due to complex parameter encoding elif opcode == 0xF5: fmt = extract_bits(w0, 21, 3) siz = extract_bits(w0, 19, 2) line = extract_bits(w0, 9, 9) tmem = extract_bits(w0, 0, 9) tile = extract_bits(w1, 24, 3) palette = extract_bits(w1, 20, 4) ct = extract_bits(w1, 18, 2) mt = extract_bits(w1, 8, 2) maskt = extract_bits(w1, 14, 4) shiftt = extract_bits(w1, 10, 4) cs = extract_bits(w1, 2, 2) ms = extract_bits(w1, 8, 2) masks = extract_bits(w1, 4, 4) shifts = extract_bits(w1, 0, 4) fmt_name = FMT_NAMES.get(fmt, f"0x{fmt:X}") siz_name = SIZ_NAMES.get(siz, f"0x{siz:X}") return f"{{{{ 0x{w0:08X}, 0x{w1:08X} }}}} /* gsDPSetTile({fmt_name}, {siz_name}, {line}, {tmem}, {tile}, {palette}, {ct}, {maskt}, {shiftt}, {cs}, {masks}, {shifts}) */" # gsDPLoadBlock (0xF3) elif opcode == 0xF3: uls = extract_bits(w0, 12, 12) ult = extract_bits(w0, 0, 12) tile = extract_bits(w1, 24, 3) lrs = extract_bits(w1, 12, 12) dxt = extract_bits(w1, 0, 12) return f"gsDPLoadBlock({tile}, {uls}, {ult}, {lrs}, {dxt})" # gsDPSetTileSize (0xF2) - output as raw bytes due to precision issues elif opcode == 0xF2: uls = extract_bits(w0, 12, 12) ult = extract_bits(w0, 0, 12) tile = extract_bits(w1, 24, 3) lrs = extract_bits(w1, 12, 12) lrt = extract_bits(w1, 0, 12) return f"{{{{ 0x{w0:08X}, 0x{w1:08X} }}}} /* gsDPSetTileSize({tile}, {uls}, {ult}, {lrs}, {lrt}) */" # gsSPDisplayList (0xDE in F3DEX_GBI_2, 0x06 in classic) elif opcode == 0xDE or opcode == 0x06: addr = w1 return f"gsSPDisplayList(0x{addr:08X})" # gsSPBranchList (0xDE with different flags) # Distinguishing from DisplayList requires looking at push/nopush flag # For now, treat as DisplayList # gsSPMatrix (0xDA in F3DEX_GBI_2, 0x01 in classic) elif opcode == 0xDA or opcode == 0x01: params = extract_bits(w0, 0, 8) addr = w1 return f"gsSPMatrix(0x{addr:08X}, {params})" # gsSPPopMatrix (0xD8 in F3DEX_GBI_2, 0xBD in classic) elif opcode == 0xD8 or opcode == 0xBD: n = w1 return f"gsSPPopMatrix(G_MTX_MODELVIEW, {n})" # gsDPLoadTLUT (0xF0) elif opcode == 0xF0: tile = extract_bits(w1, 24, 3) count = extract_bits(w1, 14, 10) return f"gsDPLoadTLUT({tile}, {count})" # gsDPSetScissor (0xED) elif opcode == 0xED: mode = extract_bits(w0, 0, 2) ulx = extract_bits(w0, 12, 12) uly = extract_bits(w0, 0, 12) lrx = extract_bits(w1, 12, 12) lry = extract_bits(w1, 0, 12) return f"gsDPSetScissor(G_SC_NON_INTERLACE, {ulx}, {uly}, {lrx}, {lry})" # Unknown command - output as raw hex with comment else: return "{{0x%08X, 0x%08X}} /* unknown opcode 0x%02X */" % (w0, w1, opcode) def parse_gfx_array(data: bytes, offset: int, base_addr: int) -> Tuple[List[str], int]: """Parse Gfx display list commands until end marker (0xB8 or 0xDF)""" if offset == 0: return [], 0 commands = [] pos = offset max_commands = 1000 # Safety limit to prevent infinite loops for _ in range(max_commands): if pos + 8 > len(data): break # Read Gfx command (8 bytes) w0 = read_u32(data, pos) w1 = read_u32(data, pos + 4) opcode = (w0 >> 24) & 0xFF # Decode command to macro format decoded = decode_gfx_command(w0, w1) commands.append(decoded) pos += 8 # Stop at B8 (gsSPEndDisplayList) or DF end markers if opcode == 0xB8 or opcode == 0xDF: break return commands, pos - offset def generate_model_c(prop_name: str, parsed_model: Dict, metadata: Dict, image_map: Dict, binary_data: bytes) -> str: """Generate complete Model.c source code from parsed model data""" switches = parsed_model['switches'] textures = parsed_model['textures'] nodes = parsed_model['nodes'] referenced_data = parsed_model.get('referenced_data', {}) root_offset = parsed_model['root_offset'] lines = [] lines.append('#include "bondtypes.h"') lines.append('#include "bondconstants.h"') lines.append('#include "gbi_extension.h"') lines.append("") lines.append(f"#define TEXTURECOUNT {len(textures)}") lines.append("") # Calculate vertex counts from DisplayList nodes to generate #define statements vertex_counts = {} collision_vertex_counts = {} vertex_array_index = 0 collision_array_index = 0 for node_offset in sorted(nodes.keys()): node = nodes[node_offset] # DisplayListCollisionRecord (opcode 24) if node.opcode == 24 and node.data and node.data.get('_type') == 'DisplayListCollisionRecord': vertex_counts[vertex_array_index] = len(node.data['vertices']) collision_vertex_counts[collision_array_index] = len(node.data['collision_vertices']) vertex_array_index += 1 collision_array_index += 1 # DisplayListPrimaryRecord (opcode 22) elif node.opcode == 22 and node.data and node.data.get('_type') == 'DisplayListPrimaryRecord': if node.data.get('vertices'): vertex_counts[vertex_array_index] = len(node.data['vertices']) vertex_array_index += 1 # DisplayListRecord (opcode 4) elif node.opcode == 4 and node.data and node.data.get('_type') == 'DisplayListRecord': if node.data.get('vertices'): vertex_counts[vertex_array_index] = len(node.data['vertices']) vertex_array_index += 1 # Generate vertex count defines for idx, count in vertex_counts.items(): lines.append(f"#define VERTEXGROUPCOUNT{idx} {count}") for idx, count in collision_vertex_counts.items(): lines.append(f"#define COLLISIONVERTEXCOUNT{idx} {count}") lines.append("") # Forward declarations (will be populated after structure collection) lines.append("// Forward declarations") # First, declare all ModelNodes for node_offset in sorted(nodes.keys()): lines.append(f"extern ModelNode ModelNode_0x{node_offset:03x};") lines.append("") # Placeholder for data structure forward declarations (will be filled in later) forward_decl_placeholder_index = len(lines) # For props with switches: generate switch array at start of file # Game code expects: filedata[0..numSwitches*4-1] = switches, then textures, then nodes if len(switches) > 0: lines.append(f"// Switch array ({len(switches)} switches) - game loads this as part of binary") lines.append(f"u32 SwitchNodes[{len(switches)}] = ") lines.append("{") for switch_offset in switches: if switch_offset > 0: # Use label reference instead of absolute address lines.append(f" (u32)&ModelNode_0x{switch_offset:03x}, // ModelNode at offset 0x{switch_offset:03X}") else: lines.append(f" 0x00000000, // NULL") lines.append("};") lines.append("") # Generate texture table lines.append("//base address is 0x05000000") if len(textures) > 0: lines.append(f"ModelFileTextures proptextures[TEXTURECOUNT] = ") lines.append("{") for tex in textures: img_name = image_map.get(tex.texture_id, f"0x{tex.texture_id:X}") lines.append(f" {{{img_name}, {tex.width}, {tex.height}, 0x{tex.mipmaptiles:02X}, 0x{tex.type:X}, 0x{tex.renderdepth:02X}, 0x{tex.sflags:X}, 0x{tex.tflags:X}}},") lines.append("};") else: # Zero-sized arrays are invalid in C, so don't generate them lines.append("// No textures for this prop") lines.append("") # Generate ModelNode tree lines.append("// ModelNode tree") for node_offset in sorted(nodes.keys()): node = nodes[node_offset] # Format the opcode field - need to include both flag byte and opcode byte # In the binary it's stored as u16 big-endian: (flags << 8) | opcode # We output it as a compound literal to ensure correct byte layout if node.opcode_flags != 0: # Need both bytes: output as hex u16 to ensure exact byte layout opcode_value = f"0x{(node.opcode_flags << 8) | node.opcode:04X}" else: # Just the opcode for prop models (flags=0) opcode_name = f"MODELNODE_OPCODE_{OPCODES.get(node.opcode, 'UNKNOWN')}" opcode_value = opcode_name # Format pointers data_ptr = f"&{get_data_symbol_name(node)}" if node.data else "NULL" parent_ptr = f"&ModelNode_0x{node.parent_offset:03x}" if node.parent_offset is not None and node.parent_offset >= 0 else "NULL" next_ptr = f"&ModelNode_0x{node.next_offset:03x}" if node.next_offset is not None and node.next_offset >= 0 else "NULL" prev_ptr = f"&ModelNode_0x{node.prev_offset:03x}" if node.prev_offset is not None and node.prev_offset >= 0 else "NULL" child_ptr = f"&ModelNode_0x{node.child_offset:03x}" if node.child_offset is not None and node.child_offset >= 0 else "NULL" lines.append(f"ModelNode ModelNode_0x{node_offset:03x} = {{{opcode_value}, {data_ptr}, {parent_ptr}, {next_ptr}, {prev_ptr}, {child_ptr}}};") lines.append("") # CRITICAL: IDO compiler places structures in .data section in exact C file declaration order # We MUST output all structures sorted by their original binary offset to match the layout # Step 1: Collect all structures with their (file_offset, code_lines, dependencies) all_structures = [] # Map node offset to symbolic names for each data array node_to_vtx_name = {} node_to_colvtx_name = {} node_to_ptusage_name = {} node_to_gdl_prim_name = {} node_to_gdl_sec_name = {} # Create mapping from node_offset to vertex/collision array indices # This must match the indices generated in the first loop node_to_vtx_index = {} node_to_colvtx_index = {} vtx_counter = 0 colvtx_counter = 0 for node_offset in sorted(nodes.keys()): node = nodes[node_offset] if node.opcode == 24 and node.data and node.data.get('_type') == 'DisplayListCollisionRecord': node_to_vtx_index[node_offset] = vtx_counter node_to_colvtx_index[node_offset] = colvtx_counter vtx_counter += 1 colvtx_counter += 1 elif node.opcode == 22 and node.data and node.data.get('_type') == 'DisplayListPrimaryRecord': if node.data.get('vertices'): node_to_vtx_index[node_offset] = vtx_counter vtx_counter += 1 elif node.opcode == 4 and node.data and node.data.get('_type') == 'DisplayListRecord': if node.data.get('vertices'): node_to_vtx_index[node_offset] = vtx_counter vtx_counter += 1 # Counters for generating unique names ptusage_idx = 0 gdl_prim_idx = 0 gdl_sec_idx = 0 # Step 2: Process nodes to collect all structures with their offsets for node_offset in sorted(nodes.keys()): node = nodes[node_offset] if not node.data: continue dtype = node.data.get('_type') # GroupRecord structures if dtype == 'GroupRecord': struct_lines = [] struct_lines.append(f"ModelRoData_GroupRecord GroupRecord_0x{node.data_offset:03x} = ") struct_lines.append("{") ox, oy, oz = node.data['origin_x'], node.data['origin_y'], node.data['origin_z'] struct_lines.append(f" {{{ox}, {oy}, {oz}}}, //origin {{x,y,z}}") struct_lines.append(f" 0x{node.data['joint_id']:X}, //JointID") mid0 = node.data['matrix_id0'] mid1 = node.data['matrix_id1'] mid2 = node.data['matrix_id2'] struct_lines.append(f" 0x{mid0 & 0xFFFF:X}, //MatrixID0") struct_lines.append(f" 0x{mid1 & 0xFFFF:X}, //MatrixID1") struct_lines.append(f" 0x{mid2 & 0xFFFF:X}, //MatrixID2") # ChildGroup pointer (points to ModelNode, not directly to GroupRecord) child_offset = node.data.get('child_group_offset', -1) if child_offset > 0: struct_lines.append(f" (struct ModelRoData_GroupRecord *)&ModelNode_0x{child_offset:03x}, //ChildGroup") else: struct_lines.append(f" NULL, //ChildGroup") struct_lines.append(f" {node.data['bounding_volume_radius']} //BoundingVolumeRadius") struct_lines.append("};") all_structures.append((node.data_offset, '\n'.join(struct_lines))) # BoundingBoxRecord structures elif dtype == 'BoundingBoxRecord': struct_lines = [] struct_lines.append(f"ModelRoData_BoundingBoxRecord BoundingBoxRecord_0x{node.data_offset:03x} = ") struct_lines.append("{") struct_lines.append(f" 0x{node.data['model_number']:X},") xmin, xmax = node.data['xmin'], node.data['xmax'] ymin, ymax = node.data['ymin'], node.data['ymax'] zmin, zmax = node.data['zmin'], node.data['zmax'] struct_lines.append(f" {{{xmin}, {xmax}, {ymin}, {ymax}, {zmin}, {zmax}}}") struct_lines.append("};") all_structures.append((node.data_offset, '\n'.join(struct_lines))) # Note: Padding is handled by gap detection, not added here # LODRecord structures elif dtype == 'LODRecord': struct_lines = [] struct_lines.append(f"ModelRoData_LODRecord LODRecord_0x{node.data_offset:03x} = ") struct_lines.append("{") struct_lines.append(f" {node.data['min_distance']}f, //MinDistance") struct_lines.append(f" {node.data['max_distance']}f, //MaxDistance") # The Affects field points to the child1 node child1_ptr = f"&ModelNode_0x{node.child_offset:03x}" if node.child_offset > 0 else "NULL" struct_lines.append(f" {child1_ptr}, //Affects (child node)") rw_idx = node.data.get('rw_data_index', 0) res = node.data.get('reserved', 0) struct_lines.append(f" 0x{rw_idx:X}, 0x{res:X} //RwDataIndex, reserved") struct_lines.append("};") all_structures.append((node.data_offset, '\n'.join(struct_lines))) # Note: Padding is detected and added separately # BSPRecord structures elif dtype == 'BSPRecord': struct_lines = [] struct_lines.append(f"ModelRoData_BSPRecord BSPRecord_0x{node.data_offset:03x} = ") struct_lines.append("{") px, py, pz = node.data['point_x'], node.data['point_y'], node.data['point_z'] vx, vy, vz = node.data['vector_x'], node.data['vector_y'], node.data['vector_z'] struct_lines.append(f" {{{px}, {py}, {pz}}}, //Point") struct_lines.append(f" {{{vx}, {vy}, {vz}}}, //Vector") # Left and right children point to ModelNodes left_offset = node.data.get('left_child_offset', -1) right_offset = node.data.get('right_child_offset', -1) left_ptr = f"&ModelNode_0x{left_offset:03x}" if left_offset > 0 else "NULL" right_ptr = f"&ModelNode_0x{right_offset:03x}" if right_offset > 0 else "NULL" struct_lines.append(f" {left_ptr}, //leftChild") struct_lines.append(f" {right_ptr}, //rightChild") struct_lines.append(f" 0x{node.data['reserved']:X}, 0x{node.data['rw_data_index']:X} //reserved, RwDataIndex") struct_lines.append("};") all_structures.append((node.data_offset, '\n'.join(struct_lines))) # SwitchRecord structures elif dtype == 'SwitchRecord': struct_lines = [] struct_lines.append(f"ModelRoData_SwitchRecord SwitchRecord_0x{node.data_offset:03x} = ") struct_lines.append("{") controls_offset = node.data.get('controls_offset', -1) controls_ptr = f"&ModelNode_0x{controls_offset:03x}" if controls_offset > 0 else "NULL" struct_lines.append(f" {controls_ptr}, //Controls") struct_lines.append(f" 0x{node.data['rw_data_index']:X}, 0x{node.data['reserved']:X} //RwDataIndex, reserved") struct_lines.append("};") all_structures.append((node.data_offset, '\n'.join(struct_lines))) # InterlinkageRecord structures (28 bytes) elif dtype == 'InterlinkageRecord': struct_lines = [] struct_lines.append(f"ModelRoData_InterlinkageRecord InterlinkageRecord_0x{node.data_offset:03x} = ") struct_lines.append("{") px, py, pz = node.data['pos_x'], node.data['pos_y'], node.data['pos_z'] struct_lines.append(f" {{{px}, {py}, {pz}}}, //pos") struct_lines.append(f" 0x{node.data['unknown1']:08X}, //unknown1") struct_lines.append(f" 0x{node.data['unknown2']:08X}, //unknown2") struct_lines.append(f" 0x{node.data['unknown3']:08X}, //unknown3") struct_lines.append(f" {node.data['scale']} //Scale") struct_lines.append("};") all_structures.append((node.data_offset, '\n'.join(struct_lines))) # GroupSimpleRecord structures (20 bytes) elif dtype == 'GroupSimpleRecord': struct_lines = [] struct_lines.append(f"ModelRoData_GroupSimpleRecord GroupSimpleRecord_0x{node.data_offset:03x} = ") struct_lines.append("{") ox, oy, oz = node.data['origin_x'], node.data['origin_y'], node.data['origin_z'] struct_lines.append(f" {{{ox}, {oy}, {oz}}}, //Origin") struct_lines.append(f" 0x{node.data['group1']:X}, 0x{node.data['group2']:X}, //Group1, Group2") struct_lines.append(f" {node.data['bounding_volume_radius']} //BoundingVolumeRadius") struct_lines.append("};") all_structures.append((node.data_offset, '\n'.join(struct_lines))) # HeaderRecord structures (16 bytes) elif dtype == 'HeaderRecord': struct_lines = [] struct_lines.append(f"ModelRoData_HeaderRecord HeaderRecord_0x{node.data_offset:03x} = ") struct_lines.append("{") struct_lines.append(f" 0x{node.data['model_type']:X}, //ModelType") # FirstGroup pointer (points to ModelNode, despite the type name in bondtypes.h) first_group_offset = node.data.get('first_group_offset', 0) group_ptr = f"(struct ModelRoData_GroupRecord *)&ModelNode_0x{first_group_offset:03x}" if first_group_offset > 0 else "NULL" struct_lines.append(f" {group_ptr}, //FirstGroup") struct_lines.append(f" 0x{node.data['group1']:X}, 0x{node.data['group2']:X}, //Group1, Group2") struct_lines.append(f" 0x{node.data['rw_data_index']:X} //RwDataIndex") struct_lines.append("};") all_structures.append((node.data_offset, '\n'.join(struct_lines))) # HeadPlaceholderRecord structures (4 bytes) elif dtype == 'HeadPlaceholderRecord': struct_lines = [] struct_lines.append(f"ModelRoData_HeadPlaceholderRecord HeadPlaceholderRecord_0x{node.data_offset:03x} = ") struct_lines.append("{") struct_lines.append(f" 0x{node.data['rw_data_index']:X} //RwDataIndex") struct_lines.append("};") all_structures.append((node.data_offset, '\n'.join(struct_lines))) # ShadowRecord structures (32 bytes) elif dtype == 'ShadowRecord': data = node.data struct_lines = [] struct_lines.append(f"ModelRoData_ShadowRecord ShadowRecord_0x{node.data_offset:03x} = ") struct_lines.append("{") struct_lines.append(f" {{{float_to_c(data['pos_x'])}, {float_to_c(data['pos_y'])}}}, //pos") struct_lines.append(f" {{{float_to_c(data['size_x'])}, {float_to_c(data['size_y'])}}}, //size") # Image pointer if data['image_offset'] > 0: struct_lines.append(f" (void *)(0x05000000 + 0x{data['image_offset']:03x}), //image") else: struct_lines.append(" NULL, //image") # Header pointer - points to a ModelNode with HEADER opcode usually if data['header_offset'] > 0: struct_lines.append(f" (struct ModelRoData_HeaderRecord *)(0x05000000 + 0x{data['header_offset']:03x}), //Header") else: struct_lines.append(" NULL, //Header") struct_lines.append(f" {float_to_c(data['scale'])}, //Scale") struct_lines.append(f" (void *)0x{data['base_addr']:X} //BaseAddr") struct_lines.append("};") all_structures.append((node.data_offset, '\n'.join(struct_lines))) # GunfireRecord structures (40 bytes) elif dtype == 'GunfireRecord': data = node.data struct_lines = [] struct_lines.append(f"ModelRoData_GunfireRecord GunfireRecord_0x{node.data_offset:03x} = ") struct_lines.append("{") # Offset coord3d struct_lines.append(" {") struct_lines.append(f" {float_to_c(data['offset_x'])}, //{data['offset_x']:.6f}") struct_lines.append(f" {float_to_c(data['offset_y'])}, //{data['offset_y']:.6f}") struct_lines.append(f" {float_to_c(data['offset_z'])}, //{data['offset_z']:.6f}") struct_lines.append(" },") # Size coord3d struct_lines.append(" {") struct_lines.append(f" {float_to_c(data['size_x'])}, //{data['size_x']:.6f}") struct_lines.append(f" {float_to_c(data['size_y'])}, //{data['size_y']:.6f}") struct_lines.append(f" {float_to_c(data['size_z'])}, //{data['size_z']:.6f}") struct_lines.append(" },") # Image pointer (NULL for now, or reference texture) if data.get('image_offset', 0) > 0: struct_lines.append(f" (void *)(0x05000000 + 0x{data['image_offset']:03x}), //Image") else: struct_lines.append(" NULL, //Image") struct_lines.append(f" {float_to_c(data['scale'])}, //{data['scale']:.6f}") struct_lines.append(f" 0x{data['rw_data_index']:X}, //RwDataIndex") struct_lines.append(f" 0x{data['reserved']:X}, //reserved") struct_lines.append(f" 0x{data['base_addr']:08X} //BaseAddr") struct_lines.append("};") all_structures.append((node.data_offset, '\n'.join(struct_lines))) # DisplayListPrimaryRecord structures (16 bytes) with sub-arrays elif dtype == 'DisplayListPrimaryRecord': data = node.data dl_offset = node.data_offset # Extract file offsets vertices_offset = data.get('vertices_offset', 0) primary_offset = data['primary_offset'] vtx_name = None prim_name = None # Vertex array if data.get('vertices') and vertices_offset > 0: vtx_name = f"Vertex_0x{vertices_offset:03x}" vtx_index = node_to_vtx_index.get(node_offset, 0) node_to_vtx_name[node_offset] = (vtx_name, vtx_index) struct_lines = [] struct_lines.append(f"Vertex {vtx_name}[VERTEXGROUPCOUNT{vtx_index}] = ") struct_lines.append("{ //{ { x, y, z}, index, s, t, r, g, b, a }") for v in data['vertices']: s_str = f"0x{v.s:X}" if v.s >= 0 else f"{v.s}" t_str = f"0x{v.t:X}" if v.t >= 0 else f"{v.t}" struct_lines.append(f" {{ {{ {v.x:4d}, {v.y:4d}, {v.z:4d}}}, 0x{v.flag:X}, {s_str:>6}, {t_str:>6}, 0x{v.r:02X}, 0x{v.g:02X}, 0x{v.b:02X}, 0x{v.a:02X} }},") struct_lines.append("};") all_structures.append((vertices_offset, '\n'.join(struct_lines))) # Primary Gfx array if data['primary_gfx'] and primary_offset > 0: prim_name = f"GDL_0x{primary_offset:03x}" node_to_gdl_prim_name[node_offset] = prim_name struct_lines = [] struct_lines.append(f"Gfx {prim_name}[] = ") struct_lines.append("{") for cmd in data['primary_gfx']: struct_lines.append(f" {cmd},") struct_lines.append("};") all_structures.append((primary_offset, '\n'.join(struct_lines))) # DLPrimary record itself struct_lines = [] struct_lines.append(f"ModelRoData_DisplayListPrimaryRecord DLPrimaryRecord_0x{dl_offset:03x} = ") struct_lines.append("{") struct_lines.append(f" {data['num_vertices']}, //numVertices") if vtx_name: struct_lines.append(f" {vtx_name}, //Vertices") else: struct_lines.append(f" NULL, //Vertices") if prim_name: struct_lines.append(f" {prim_name}, //Primary") else: struct_lines.append(f" NULL, //Primary") struct_lines.append(f" (void *)0x{data['base_addr']:08X} //BaseAddr") struct_lines.append("};") all_structures.append((dl_offset, '\n'.join(struct_lines))) # DisplayListRecord structures (19 bytes) with sub-arrays elif dtype == 'DisplayListRecord': data = node.data dl_offset = node.data_offset # Extract file offsets for all sub-structures vertices_offset = data.get('vertices_offset', 0) primary_offset = data['primary_offset'] secondary_offset = data['secondary_offset'] # Assign unique symbolic names vtx_name = None prim_name = None sec_name = None # Vertex array if data.get('vertices') and vertices_offset > 0: vtx_name = f"Vertex_0x{vertices_offset:03x}" vtx_index = node_to_vtx_index.get(node_offset, 0) node_to_vtx_name[node_offset] = (vtx_name, vtx_index) struct_lines = [] struct_lines.append(f"Vertex {vtx_name}[VERTEXGROUPCOUNT{vtx_index}] = ") struct_lines.append("{ //{ { x, y, z}, index, s, t, r, g, b, a }") for v in data['vertices']: s_str = f"0x{v.s:X}" if v.s >= 0 else f"{v.s}" t_str = f"0x{v.t:X}" if v.t >= 0 else f"{v.t}" struct_lines.append(f" {{ {{ {v.x:4d}, {v.y:4d}, {v.z:4d}}}, 0x{v.flag:X}, {s_str:>6}, {t_str:>6}, 0x{v.r:02X}, 0x{v.g:02X}, 0x{v.b:02X}, 0x{v.a:02X} }},") struct_lines.append("};") all_structures.append((vertices_offset, '\n'.join(struct_lines))) # Primary GFX array if data.get('primary_gfx') and primary_offset > 0: prim_name = f"GFX_PRIMARY_0x{primary_offset:03x}" node_to_gdl_prim_name[node_offset] = prim_name gdl_prim_idx += 1 struct_lines = [] struct_lines.append(f"Gfx {prim_name}[] = ") struct_lines.append("{") for cmd_str in data['primary_gfx']: struct_lines.append(f" {cmd_str},") struct_lines.append("};") all_structures.append((primary_offset, '\n'.join(struct_lines))) # Secondary GFX array if data.get('secondary_gfx') and secondary_offset > 0: sec_name = f"GFX_SECONDARY_0x{secondary_offset:03x}" node_to_gdl_sec_name[node_offset] = sec_name gdl_sec_idx += 1 struct_lines = [] struct_lines.append(f"Gfx {sec_name}[] = ") struct_lines.append("{") for cmd_str in data['secondary_gfx']: struct_lines.append(f" {cmd_str},") struct_lines.append("};") all_structures.append((secondary_offset, '\n'.join(struct_lines))) # Now the DisplayListRecord itself struct_lines = [] struct_lines.append(f"ModelRoData_DisplayListRecord DisplayListRecord_0x{dl_offset:03x} = ") struct_lines.append("{") primary_ref = f"&{node_to_gdl_prim_name[node_offset]}" if node_offset in node_to_gdl_prim_name else "NULL" secondary_ref = f"&{node_to_gdl_sec_name[node_offset]}" if node_offset in node_to_gdl_sec_name else "NULL" struct_lines.append(f" {primary_ref}, //PrimaryDisplayList") struct_lines.append(f" {secondary_ref}, //SecondaryDisplayList") # BaseAddr pointer (raw value from binary) base_addr = data.get('base_addr', 0) struct_lines.append(f" (void *)0x{base_addr:08X}, //BaseAddr") # Vertices pointer if node_offset in node_to_vtx_name: vtx_ref = f"&{node_to_vtx_name[node_offset][0]}" elif data.get('vertices_offset', -1) >= 0: # Vertices pointer exists but no vertices were parsed (num_vertices=0) vtx_ref = f"(Vtx *)0x{BASE_ADDRESS + data['vertices_offset']:08X}" else: vtx_ref = "NULL" struct_lines.append(f" {vtx_ref}, //Vertices") struct_lines.append(f" 0x{data['num_vertices']:X}, //NumVertices") struct_lines.append(f" {data['model_type']} //ModelType") struct_lines.append("};") all_structures.append((dl_offset, '\n'.join(struct_lines))) # DisplayListCollisionRecord and all its sub-structures elif dtype == 'DisplayListCollisionRecord': data = node.data # DLCollisionRecord itself (32 bytes at node.data_offset) dlcoll_offset = node.data_offset # Extract file offsets for all sub-structures vertices_offset = data.get('vertices_offset', 0) collision_vertices_offset = data.get('collision_vertices_offset', 0) point_usage_offset = data.get('point_usage_offset', 0) primary_offset = data['primary_offset'] secondary_offset = data['secondary_offset'] # Assign unique symbolic names for this node's arrays vtx_name = None colvtx_name = None ptusage_name = None prim_name = None sec_name = None # Vertex array if data.get('vertices') and vertices_offset > 0: vtx_name = f"Vertex_0x{vertices_offset:03x}" vtx_index = node_to_vtx_index.get(node_offset, 0) node_to_vtx_name[node_offset] = (vtx_name, vtx_index) struct_lines = [] struct_lines.append(f"Vertex {vtx_name}[VERTEXGROUPCOUNT{vtx_index}] = ") struct_lines.append("{ //{ { x, y, z}, index, s, t, r, g, b, a }") for v in data['vertices']: s_str = f"0x{v.s:X}" if v.s >= 0 else f"{v.s}" t_str = f"0x{v.t:X}" if v.t >= 0 else f"{v.t}" struct_lines.append(f" {{ {{ {v.x:4d}, {v.y:4d}, {v.z:4d}}}, 0x{v.flag:X}, {s_str:>6}, {t_str:>6}, 0x{v.r:02X}, 0x{v.g:02X}, 0x{v.b:02X}, 0x{v.a:02X} }},") struct_lines.append("};") all_structures.append((vertices_offset, '\n'.join(struct_lines))) # Collision vertex array if data['collision_vertices'] and collision_vertices_offset > 0: colvtx_name = f"Collision_Vertex_0x{collision_vertices_offset:03x}" colvtx_index = node_to_colvtx_index.get(node_offset, 0) node_to_colvtx_name[node_offset] = (colvtx_name, colvtx_index) struct_lines = [] struct_lines.append(f"Vertex {colvtx_name}[COLLISIONVERTEXCOUNT{colvtx_index}] = ") struct_lines.append("{ //{ { x, y, z}, index, s, t, r, g, b, a }") for v in data['collision_vertices']: s_str = f"0x{v.s:X}" if v.s >= 0 else f"{v.s}" t_str = f"0x{v.t:X}" if v.t >= 0 else f"{v.t}" struct_lines.append(f" {{ {{ {v.x:4d}, {v.y:4d}, {v.z:4d}}}, 0x{v.flag:X}, {s_str:>6}, {t_str:>6}, 0x{v.r:02X}, 0x{v.g:02X}, 0x{v.b:02X}, 0x{v.a:02X} }},") struct_lines.append("};") all_structures.append((collision_vertices_offset, '\n'.join(struct_lines))) # Point usage array if data['point_usage'] and point_usage_offset > 0: ptusage_name = f"POINT_USAGE_0x{point_usage_offset:03x}" node_to_ptusage_name[node_offset] = (ptusage_name, ptusage_idx) ptusage_idx += 1 struct_lines = [] struct_lines.append(f"s16 {ptusage_name}[VERTEXGROUPCOUNT{node_to_vtx_name[node_offset][1]}] = ") struct_lines.append("{") for i in range(0, len(data['point_usage']), 4): chunk = data['point_usage'][i:i+4] formatted = ', '.join(f"0x{v:04X}" if v >= 0 else f"{v}" for v in chunk) struct_lines.append(f" {formatted},") struct_lines.append("};") all_structures.append((point_usage_offset, '\n'.join(struct_lines))) # Note: Padding is detected via gap analysis, not added explicitly here # Primary GFX array if data.get('primary_gfx') and primary_offset > 0: prim_name = f"GFX_PRIMARY_0x{primary_offset:03x}" node_to_gdl_prim_name[node_offset] = prim_name gdl_prim_idx += 1 struct_lines = [] struct_lines.append(f"Gfx {prim_name}[] = ") struct_lines.append("{") for cmd_str in data['primary_gfx']: struct_lines.append(f" {cmd_str},") struct_lines.append("};") all_structures.append((primary_offset, '\n'.join(struct_lines))) # Secondary GFX array if data.get('secondary_gfx') and secondary_offset > 0: sec_name = f"GFX_SECONDARY_0x{secondary_offset:03x}" node_to_gdl_sec_name[node_offset] = sec_name gdl_sec_idx += 1 struct_lines = [] struct_lines.append(f"Gfx {sec_name}[] = ") struct_lines.append("{") for cmd_str in data['secondary_gfx']: struct_lines.append(f" {cmd_str},") struct_lines.append("};") all_structures.append((secondary_offset, '\n'.join(struct_lines))) # Now the DLCollisionRecord itself, which references the arrays above struct_lines = [] struct_lines.append(f"ModelRoData_DisplayList_CollisionRecord DLCollisionRecord_0x{dlcoll_offset:03x} = ") struct_lines.append("{") primary_ref = node_to_gdl_prim_name.get(node_offset, "NULL") secondary_ref = node_to_gdl_sec_name.get(node_offset, "NULL") if node_offset in node_to_vtx_name: vtx_ref_name = node_to_vtx_name[node_offset][0] vtx_count_ref = f"VERTEXGROUPCOUNT{node_to_vtx_name[node_offset][1]}" else: vtx_ref_name = "NULL" vtx_count_ref = "0" if node_offset in node_to_colvtx_name: colvtx_ref_name = node_to_colvtx_name[node_offset][0] colvtx_count_ref = f"COLLISIONVERTEXCOUNT{node_to_colvtx_name[node_offset][1]}" else: colvtx_ref_name = "NULL" colvtx_count_ref = "0" ptusage_ref = node_to_ptusage_name.get(node_offset, ("NULL", -1))[0] struct_lines.append(f" {primary_ref}, //primary") struct_lines.append(f" {secondary_ref}, //secondary") struct_lines.append(f" {vtx_ref_name}, {vtx_count_ref}, //vertices,vcount") struct_lines.append(f" {colvtx_count_ref}, {colvtx_ref_name}, //ncolvtx,collision vertices") struct_lines.append(f" {ptusage_ref}, //point usage") struct_lines.append(f" 0x{data['model_type']:X}, 0x{data['rw_data_index']:X}, //type, index") struct_lines.append(f" 0x0 //baseaddr") struct_lines.append("};") all_structures.append((dlcoll_offset, '\n'.join(struct_lines))) # Step 2.1: Process referenced_data structures (not in ModelNode tree) for ref_offset in sorted(referenced_data.keys()): ref_data = referenced_data[ref_offset] dtype = ref_data.get('_type') # Only handle GroupRecord for now (main use case) if dtype == 'GroupRecord': struct_lines = [] struct_lines.append(f"ModelRoData_GroupRecord GroupRecord_0x{ref_offset:03x} = ") struct_lines.append("{") ox, oy, oz = ref_data['origin_x'], ref_data['origin_y'], ref_data['origin_z'] struct_lines.append(f" {{{ox}, {oy}, {oz}}}, //origin {{x,y,z}}") struct_lines.append(f" 0x{ref_data['joint_id']:X}, //JointID") mid0 = ref_data['matrix_id0'] mid1 = ref_data['matrix_id1'] mid2 = ref_data['matrix_id2'] struct_lines.append(f" 0x{mid0 & 0xFFFF:X}, //MatrixID0") struct_lines.append(f" 0x{mid1 & 0xFFFF:X}, //MatrixID1") struct_lines.append(f" 0x{mid2 & 0xFFFF:X}, //MatrixID2") # ChildGroup points to a ModelNode child_offset = ref_data.get('child_group_offset', -1) if child_offset > 0: struct_lines.append(f" (struct ModelRoData_GroupRecord *)&ModelNode_0x{child_offset:03x}, //ChildGroup") else: struct_lines.append(f" NULL, //ChildGroup") struct_lines.append(f" {ref_data['bounding_volume_radius']} //BoundingVolumeRadius") struct_lines.append("};") all_structures.append((ref_offset, '\n'.join(struct_lines))) # Step 2.5: Detect padding by finding gaps between sorted structures # Mark all structure bytes, find gaps if binary_data: binary_size = len(binary_data) byte_map = [False] * binary_size # Mark switches (handled separately in Switches.c, not padding) switch_size = len(switches) * 4 if switches else 0 for i in range(switch_size): byte_map[i] = True # Mark textures tex_end = switch_size + len(textures) * 12 for i in range(switch_size, tex_end): byte_map[i] = True # Mark nodes nodes_start = tex_end nodes_end = nodes_start + len(nodes) * 24 for i in range(nodes_start, nodes_end): byte_map[i] = True # Mark all structures by their offsets in all_structures list for offset, _ in all_structures: # Determine size based on what's at this offset for node_offset in sorted(nodes.keys()): node = nodes[node_offset] if not node.data: continue dtype = node.data.get('_type') data = node.data # GroupRecord or BoundingBoxRecord if offset == node.data_offset and dtype in ('GroupRecord', 'BoundingBoxRecord'): for i in range(offset, offset + 28): byte_map[i] = True # LODRecord (16 bytes) if offset == node.data_offset and dtype == 'LODRecord': for i in range(offset, offset + 16): byte_map[i] = True # BSPRecord (36 bytes) if offset == node.data_offset and dtype == 'BSPRecord': for i in range(offset, offset + 36): byte_map[i] = True # SwitchRecord (8 bytes) if offset == node.data_offset and dtype == 'SwitchRecord': for i in range(offset, offset + 8): byte_map[i] = True # GroupSimpleRecord (20 bytes) if offset == node.data_offset and dtype == 'GroupSimpleRecord': for i in range(offset, offset + 20): byte_map[i] = True # HeaderRecord (16 bytes) if offset == node.data_offset and dtype == 'HeaderRecord': for i in range(offset, offset + 16): byte_map[i] = True # DisplayListRecord (19 bytes logical, 20 bytes with compiler padding) if offset == node.data_offset and dtype == 'DisplayListRecord': for i in range(offset, offset + 20): byte_map[i] = True # GunfireRecord (40 bytes) if offset == node.data_offset and dtype == 'GunfireRecord': for i in range(offset, offset + 40): byte_map[i] = True # ShadowRecord (32 bytes) if offset == node.data_offset and dtype == 'ShadowRecord': for i in range(offset, offset + 32): byte_map[i] = True # HeadPlaceholderRecord (4 bytes) if offset == node.data_offset and dtype == 'HeadPlaceholderRecord': for i in range(offset, offset + 4): byte_map[i] = True # InterlinkageRecord (28 bytes) if offset == node.data_offset and dtype == 'InterlinkageRecord': for i in range(offset, offset + 28): byte_map[i] = True # DisplayListPrimaryRecord (16 bytes) if offset == node.data_offset and dtype == 'DisplayListPrimaryRecord': for i in range(offset, offset + 16): byte_map[i] = True # DLCollisionRecord (32 bytes) if offset == node.data_offset and dtype == 'DisplayListCollisionRecord': for i in range(offset, offset + 32): byte_map[i] = True # Vertices if data.get('vertices_offset') and offset == data['vertices_offset']: vsize = len(data['vertices']) * 16 for i in range(offset, offset + vsize): byte_map[i] = True # Collision vertices if data.get('collision_vertices_offset') and offset == data['collision_vertices_offset']: csize = len(data['collision_vertices']) * 16 for i in range(offset, offset + csize): byte_map[i] = True # Point usage if data.get('point_usage_offset') and offset == data['point_usage_offset']: psize = len(data['point_usage']) * 2 for i in range(offset, offset + psize): byte_map[i] = True # GFX arrays if data.get('primary_offset') and offset == data['primary_offset']: gsize = len(data['primary_gfx']) * 8 for i in range(offset, offset + gsize): byte_map[i] = True if data.get('secondary_offset') and offset == data['secondary_offset']: gsize = len(data['secondary_gfx']) * 8 for i in range(offset, offset + gsize): byte_map[i] = True # Find last real structure (handle empty case) if all_structures: last_offset = max(off for off, _ in all_structures) else: last_offset = binary_size - 1 # Calculate switch size to skip that region (switches handled separately in Switches.c) switch_size = len(switches) * 4 if switches else 0 # Find padding gaps (up to last structure only, SKIP switch region at start) # Don't scan past last_offset + 256 bytes to avoid trailing data scan_limit = min(last_offset + 256, binary_size) i = switch_size # Start AFTER switches while i < scan_limit: if not byte_map[i]: pad_start = i while i < binary_size and not byte_map[i]: i += 1 pad_end = i pad_size = pad_end - pad_start # ONLY add padding for 4+ byte gaps that are BETWEEN structures (not at the end) # 2-byte gaps are natural compiler alignment - don't add variables for them # Skip padding that extends to near the end of the file (trailing data) if pad_size >= 4 and pad_end < scan_limit - 32: # Read padding bytes pad_bytes = binary_data[pad_start:pad_end] # Generate padding as u32 array for proper alignment if pad_size == 4: val = struct.unpack('>I', pad_bytes)[0] all_structures.append((pad_start, f"u32 PADDING_0x{pad_start:03x} = 0x{val:08X};")) elif pad_size % 4 == 0: values = [struct.unpack('>I', pad_bytes[j:j+4])[0] for j in range(0, pad_size, 4)] vals_str = ', '.join(f"0x{v:08X}" for v in values) all_structures.append((pad_start, f"u32 PADDING_0x{pad_start:03x}[{pad_size // 4}] = {{{vals_str}}};")) else: # Odd size - use byte array vals_str = ', '.join(f"0x{b:02X}" for b in pad_bytes) all_structures.append((pad_start, f"u8 PADDING_0x{pad_start:03x}[{pad_size}] = {{{vals_str}}};")) else: i += 1 # Step 2.5.1: REMOVED - Trailing padding generation # Trailing padding after arrays is compiler-generated for alignment, not source data # The compiler automatically adds padding between structures as needed # DO NOT generate PADDING_TRAILING variables - they cause incorrect binary sizes # Step 2.6: Generate forward declarations with correct names forward_decl_lines = [] # Declare referenced data structures (not in ModelNode tree) for ref_offset in sorted(referenced_data.keys()): ref_data = referenced_data[ref_offset] dtype = ref_data.get('_type') if dtype == 'GroupRecord': forward_decl_lines.append(f"extern ModelRoData_GroupRecord GroupRecord_0x{ref_offset:03x};") # Declare all data structures for node_offset in sorted(nodes.keys()): node = nodes[node_offset] if node.data: dtype = node.data.get('_type') if dtype == 'GroupRecord': forward_decl_lines.append(f"extern ModelRoData_GroupRecord GroupRecord_0x{node.data_offset:03x};") elif dtype == 'BoundingBoxRecord': forward_decl_lines.append(f"extern ModelRoData_BoundingBoxRecord BoundingBoxRecord_0x{node.data_offset:03x};") elif dtype == 'LODRecord': forward_decl_lines.append(f"extern ModelRoData_LODRecord LODRecord_0x{node.data_offset:03x};") elif dtype == 'BSPRecord': forward_decl_lines.append(f"extern ModelRoData_BSPRecord BSPRecord_0x{node.data_offset:03x};") elif dtype == 'SwitchRecord': forward_decl_lines.append(f"extern ModelRoData_SwitchRecord SwitchRecord_0x{node.data_offset:03x};") elif dtype == 'InterlinkageRecord': forward_decl_lines.append(f"extern ModelRoData_InterlinkageRecord InterlinkageRecord_0x{node.data_offset:03x};") elif dtype == 'GroupSimpleRecord': forward_decl_lines.append(f"extern ModelRoData_GroupSimpleRecord GroupSimpleRecord_0x{node.data_offset:03x};") elif dtype == 'HeaderRecord': forward_decl_lines.append(f"extern ModelRoData_HeaderRecord HeaderRecord_0x{node.data_offset:03x};") elif dtype == 'HeadPlaceholderRecord': forward_decl_lines.append(f"extern ModelRoData_HeadPlaceholderRecord HeadPlaceholderRecord_0x{node.data_offset:03x};") elif dtype == 'ShadowRecord': forward_decl_lines.append(f"extern ModelRoData_ShadowRecord ShadowRecord_0x{node.data_offset:03x};") elif dtype == 'GunfireRecord': forward_decl_lines.append(f"extern ModelRoData_GunfireRecord GunfireRecord_0x{node.data_offset:03x};") elif dtype == 'DisplayListPrimaryRecord': forward_decl_lines.append(f"extern ModelRoData_DisplayListPrimaryRecord DLPrimaryRecord_0x{node.data_offset:03x};") # Declare arrays for DisplayListPrimaryRecord if node_offset in node_to_vtx_name: vtx_name, vtx_idx = node_to_vtx_name[node_offset] forward_decl_lines.append(f"extern Vertex {vtx_name}[VERTEXGROUPCOUNT{vtx_idx}];") if node_offset in node_to_gdl_prim_name: prim_name = node_to_gdl_prim_name[node_offset] forward_decl_lines.append(f"extern Gfx {prim_name}[];") elif dtype == 'DisplayListRecord': forward_decl_lines.append(f"extern ModelRoData_DisplayListRecord DisplayListRecord_0x{node.data_offset:03x};") # Declare arrays for DisplayListRecord if node_offset in node_to_vtx_name: vtx_name, vtx_idx = node_to_vtx_name[node_offset] forward_decl_lines.append(f"extern Vertex {vtx_name}[VERTEXGROUPCOUNT{vtx_idx}];") if node_offset in node_to_gdl_prim_name: prim_name = node_to_gdl_prim_name[node_offset] forward_decl_lines.append(f"extern Gfx {prim_name}[];") if node_offset in node_to_gdl_sec_name: sec_name = node_to_gdl_sec_name[node_offset] forward_decl_lines.append(f"extern Gfx {sec_name}[];") elif dtype == 'DisplayListCollisionRecord': forward_decl_lines.append(f"extern ModelRoData_DisplayList_CollisionRecord DLCollisionRecord_0x{node.data_offset:03x};") # Declare arrays using the SAME names we assigned during structure collection if node_offset in node_to_vtx_name: vtx_name, vtx_idx = node_to_vtx_name[node_offset] forward_decl_lines.append(f"extern Vertex {vtx_name}[VERTEXGROUPCOUNT{vtx_idx}];") if node_offset in node_to_colvtx_name: colvtx_name, colvtx_idx = node_to_colvtx_name[node_offset] forward_decl_lines.append(f"extern Vertex {colvtx_name}[COLLISIONVERTEXCOUNT{colvtx_idx}];") if node_offset in node_to_ptusage_name: ptusage_name, _ = node_to_ptusage_name[node_offset] vtx_idx = node_to_vtx_name[node_offset][1] forward_decl_lines.append(f"extern s16 {ptusage_name}[VERTEXGROUPCOUNT{vtx_idx}];") if node_offset in node_to_gdl_prim_name: prim_name = node_to_gdl_prim_name[node_offset] forward_decl_lines.append(f"extern Gfx {prim_name}[];") if node_offset in node_to_gdl_sec_name: sec_name = node_to_gdl_sec_name[node_offset] forward_decl_lines.append(f"extern Gfx {sec_name}[];") forward_decl_lines.append("") # Insert forward declarations at the placeholder position lines[forward_decl_placeholder_index:forward_decl_placeholder_index] = forward_decl_lines # Step 3: Sort ALL structures by their original binary file offset all_structures.sort(key=lambda x: x[0]) # Step 4: Output structures in sorted offset order for offset, code in all_structures: lines.append(code) lines.append("") return "\n".join(lines) def get_data_symbol_name(node: ModelNode) -> str: """Get the C symbol name for a node's data structure""" if not node.data: return "NULL" dtype = node.data.get('_type') if dtype == 'GroupRecord': return f"GroupRecord_0x{node.data_offset:03x}" elif dtype == 'BoundingBoxRecord': return f"BoundingBoxRecord_0x{node.data_offset:03x}" elif dtype == 'LODRecord': return f"LODRecord_0x{node.data_offset:03x}" elif dtype == 'BSPRecord': return f"BSPRecord_0x{node.data_offset:03x}" elif dtype == 'SwitchRecord': return f"SwitchRecord_0x{node.data_offset:03x}" elif dtype == 'InterlinkageRecord': return f"InterlinkageRecord_0x{node.data_offset:03x}" elif dtype == 'GroupSimpleRecord': return f"GroupSimpleRecord_0x{node.data_offset:03x}" elif dtype == 'HeaderRecord': return f"HeaderRecord_0x{node.data_offset:03x}" elif dtype == 'HeadPlaceholderRecord': return f"HeadPlaceholderRecord_0x{node.data_offset:03x}" elif dtype == 'ShadowRecord': return f"ShadowRecord_0x{node.data_offset:03x}" elif dtype == 'GunfireRecord': return f"GunfireRecord_0x{node.data_offset:03x}" elif dtype == 'DisplayListPrimaryRecord': return f"DLPrimaryRecord_0x{node.data_offset:03x}" elif dtype == 'DisplayListRecord': return f"DisplayListRecord_0x{node.data_offset:03x}" elif dtype == 'DisplayListCollisionRecord': return f"DLCollisionRecord_0x{node.data_offset:03x}" else: return f"Data_0x{node.data_offset:03x}" def main(): import argparse parser = argparse.ArgumentParser(description="Generate Model.c from binary prop files") parser.add_argument('props', nargs='*', help="Prop names to process (default: all)") parser.add_argument('--dry-run', action='store_true', help="Show what would be done") parser.add_argument('--force', action='store_true', help="Overwrite existing Model.c files") parser.add_argument('--cleanup', action='store_true', help="Delete source PnameZ.bin files after successful conversion") args = parser.parse_args() # Load image mapping image_map = load_image_map() print(f"Loaded {len(image_map)} image definitions") # Find all props prop_dir = Path("assets/obseg/prop") all_props = [] prop_name_map = {} # Map lowercase name to actual binary filename for bin_file in prop_dir.glob("P*Z.bin"): # Extract name between P and Z, preserve original case actual_name = bin_file.stem[1:-1] lower_name = actual_name.lower() all_props.append(lower_name) prop_name_map[lower_name] = actual_name props_to_process = args.props if args.props else all_props stats = {'processed': 0, 'skipped': 0, 'errors': 0} for prop_name_input in sorted(props_to_process): prop_name_lower = prop_name_input.lower() # Get the actual case from the binary file if prop_name_lower not in prop_name_map: print(f" ✗ {prop_name_input}: Binary file not found") stats['errors'] += 1 continue actual_prop_name = prop_name_map[prop_name_lower] bin_file = prop_dir / f"P{actual_prop_name}Z.bin" # Find the actual directory name (case-insensitive search) prop_subdirs = list(prop_dir.glob(f"{actual_prop_name}")) if not prop_subdirs: # Try case-insensitive prop_subdirs = [d for d in prop_dir.iterdir() if d.is_dir() and d.name.lower() == prop_name_lower] if not prop_subdirs: # Try fuzzy match for names with suffixes like _lg prop_subdirs = [d for d in prop_dir.iterdir() if d.is_dir() and d.name.lower().startswith(prop_name_lower)] if not prop_subdirs: print(f" ⊘ {actual_prop_name}: Missing metadata directory") stats['skipped'] += 1 continue actual_dir_name = prop_subdirs[0].name # Parse metadata using the actual directory name metadata = parse_metadata_files(actual_dir_name) if not metadata: print(f" ⊘ {actual_prop_name}: Missing metadata files") stats['skipped'] += 1 continue output_file = prop_dir / actual_dir_name / "Model.c" if output_file.exists() and not args.force: print(f" ⊘ {actual_prop_name}: Model.c already exists (use --force)") stats['skipped'] += 1 continue try: # Parse binary with open(bin_file, 'rb') as f: binary_data = f.read() parser = BinaryModelParser(binary_data, metadata, image_map) parsed_model = parser.parse() # Generate C code - use the directory name for output c_code = generate_model_c(actual_dir_name, parsed_model, metadata, image_map, binary_data) if args.dry_run: if len(parsed_model['switches']) > 0: print(f" ✓ {actual_prop_name}: Would generate Model.c ({len(parsed_model['nodes'])} nodes, {len(parsed_model['textures'])} textures, {len(parsed_model['switches'])} switches)") else: print(f" ✓ {actual_prop_name}: Would generate Model.c ({len(parsed_model['nodes'])} nodes, {len(parsed_model['textures'])} textures)") else: output_file.parent.mkdir(parents=True, exist_ok=True) with open(output_file, 'w') as f: f.write(c_code) # Report generation if len(parsed_model['switches']) > 0: print(f" ✓ {actual_prop_name}: Generated Model.c ({len(parsed_model['nodes'])} nodes, {len(parsed_model['textures'])} textures, {len(parsed_model['switches'])} switches)") else: print(f" ✓ {actual_prop_name}: Generated Model.c ({len(parsed_model['nodes'])} nodes, {len(parsed_model['textures'])} textures)") # Clean up source binary file after successful conversion if args.cleanup and bin_file.exists(): bin_file.unlink() print(f" Cleaned up {bin_file.name}") stats['processed'] += 1 except Exception as e: print(f" ✗ {actual_prop_name}: Error - {e}") stats['errors'] += 1 import traceback traceback.print_exc() print(f"\n=== Summary ===") print(f"Processed: {stats['processed']}") print(f"Skipped: {stats['skipped']}") print(f"Errors: {stats['errors']}") if __name__ == "__main__": main()