Files
goldeneye_src/scripts/generate_prop_model_c.py

2254 lines
102 KiB
Python
Executable File

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