rakata_formats/mdl/
types.rs

1//! Node-specific data types for the MDL format.
2//!
3//! Each MDL node carries a base header (name, transform, controllers) plus
4//! type-specific data determined by the node's bitflags. This module defines
5//! the [`MdlNodeData`] enum that discriminates between node types, along with
6//! the concrete structs for each variant.
7//!
8//! ## Rust Representation
9//!
10//! The engine's C++ code uses an inheritance hierarchy (`MdlNodeSkin` extends
11//! `MdlNodeTriMesh` extends `MdlNode`). We represent this as a flat enum
12//! with composition -- mesh-derived variants contain an [`MdlMesh`] field
13//! rather than inheriting from a base class. This is idiomatic Rust: the
14//! enum discriminant replaces the C++ vtable, and shared mesh data is
15//! accessed through the composed struct.
16//!
17//! ```text
18//! MdlNodeData (enum)
19//!  |-- Base                        (no extra data)
20//!  |-- Light(MdlLight)             (92 extra bytes)
21//!  |-- Emitter(MdlEmitter)         (224 extra bytes)
22//!  |-- Camera(MdlCamera)           (no extra data)
23//!  |-- Reference(MdlReference)     (36 extra bytes)
24//!  |-- Mesh(MdlMesh)               (332 extra bytes)
25//!  |-- Skin(MdlSkin)               (332 + 100 extra bytes)
26//!  |     `-- contains MdlMesh      (composition, not inheritance)
27//!  |-- AnimMesh(MdlAnimMesh)       (332 + 56 extra bytes)
28//!  |     `-- contains MdlMesh
29//!  |-- Dangly(MdlDangly)           (332 + 28 extra bytes)
30//!  |     `-- contains MdlMesh
31//!  |-- Aabb(MdlAabb)               (332 + 4 extra bytes)
32//!  |     `-- contains MdlMesh
33//!  `-- Saber(MdlSaber)             (332 + 20 extra bytes)
34//!        `-- contains MdlMesh
35//! ```
36//!
37//! See [`super`] module docs for the binary format layout and
38//! `docs/notes/mdl_mdx.md` for Ghidra-verified allocation sizes.
39
40/// Type-specific payload for an MDL node.
41///
42/// Determined at parse time from the node's [`super::node_flags`] bitfield.
43/// Mesh-derived variants (Skin, AnimMesh, Dangly, Aabb, Saber) each contain
44/// an [`MdlMesh`] plus type-specific extension fields.
45#[derive(Debug, Clone, PartialEq)]
46pub enum MdlNodeData {
47    /// Pure hierarchy node with no type-specific data (flags = 0x0001).
48    Base,
49    /// Light node (flags & 0x0002).
50    Light(MdlLight),
51    /// Emitter node (flags & 0x0004).
52    Emitter(MdlEmitter),
53    /// Camera node (flags & 0x0008).
54    Camera(MdlCamera),
55    /// Reference to an external model (flags & 0x0010).
56    Reference(MdlReference),
57    /// Triangle mesh node (flags & 0x0020, no subtype flags).
58    Mesh(MdlMesh),
59    /// Skinned mesh with bone weights (flags & 0x0060).
60    Skin(MdlSkin),
61    /// Animated mesh with per-frame vertex sets (flags & 0x00A0).
62    AnimMesh(MdlAnimMesh),
63    /// Dangly mesh with physics constraints (flags & 0x0120).
64    Dangly(MdlDangly),
65    /// AABB walkmesh tree (flags & 0x0220).
66    Aabb(MdlAabb),
67    /// Lightsaber blade mesh (flags & 0x0820).
68    Saber(MdlSaber),
69}
70
71impl MdlNodeData {
72    /// Returns the binary flags for this node type.
73    ///
74    /// These are the on-disk bitflags written at node offset `0x00`.
75    /// Derived from the variant rather than stored independently, ensuring
76    /// the type system and binary representation stay in sync.
77    ///
78    /// See [`super::node_flags`] for the individual flag constants.
79    #[must_use = "flags returns a computed value and has no side effects"]
80    pub fn flags(&self) -> u32 {
81        use super::node_flags;
82        match self {
83            MdlNodeData::Base => node_flags::HEADER,
84            MdlNodeData::Light(_) => node_flags::HEADER | node_flags::LIGHT,
85            MdlNodeData::Emitter(_) => node_flags::HEADER | node_flags::EMITTER,
86            MdlNodeData::Camera(_) => node_flags::HEADER | node_flags::CAMERA,
87            MdlNodeData::Reference(_) => node_flags::HEADER | node_flags::REFERENCE,
88            MdlNodeData::Mesh(_) => node_flags::HEADER | node_flags::MESH,
89            MdlNodeData::Skin(_) => node_flags::HEADER | node_flags::MESH | node_flags::SKIN,
90            MdlNodeData::AnimMesh(_) => node_flags::HEADER | node_flags::MESH | node_flags::ANIM,
91            MdlNodeData::Dangly(_) => node_flags::HEADER | node_flags::MESH | node_flags::DANGLY,
92            MdlNodeData::Aabb(_) => node_flags::HEADER | node_flags::MESH | node_flags::AABB,
93            MdlNodeData::Saber(_) => node_flags::HEADER | node_flags::MESH | node_flags::SABER,
94        }
95    }
96
97    /// Returns a reference to the contained mesh, if this is a mesh-derived variant.
98    #[must_use = "mesh returns a reference and has no side effects"]
99    pub fn mesh(&self) -> Option<&MdlMesh> {
100        match self {
101            MdlNodeData::Mesh(m) => Some(m),
102            MdlNodeData::Skin(s) => Some(&s.mesh),
103            MdlNodeData::AnimMesh(a) => Some(&a.mesh),
104            MdlNodeData::Dangly(d) => Some(&d.mesh),
105            MdlNodeData::Aabb(a) => Some(&a.mesh),
106            MdlNodeData::Saber(s) => Some(&s.mesh),
107            _ => None,
108        }
109    }
110
111    /// Returns a mutable reference to the contained mesh, if this is a mesh-derived variant.
112    #[must_use = "mesh_mut returns a reference and has no side effects"]
113    pub fn mesh_mut(&mut self) -> Option<&mut MdlMesh> {
114        match self {
115            MdlNodeData::Mesh(m) => Some(m),
116            MdlNodeData::Skin(s) => Some(&mut s.mesh),
117            MdlNodeData::AnimMesh(a) => Some(&mut a.mesh),
118            MdlNodeData::Dangly(d) => Some(&mut d.mesh),
119            MdlNodeData::Aabb(a) => Some(&mut a.mesh),
120            MdlNodeData::Saber(s) => Some(&mut s.mesh),
121            _ => None,
122        }
123    }
124}
125
126/// A face in the binary MaxFace format (32 bytes on disk).
127///
128/// Contains geometric data for rendering and collision: a plane equation,
129/// surface material ID, face adjacency indices, and vertex indices.
130///
131/// Layout verified via Ghidra struct `MaxFace` (32 bytes, `/KotOR Types/Rendering`)
132/// and cross-checked with the `0x1A` vertex-index offset constant used in both
133/// `MdlNodeDanglyMesh::InternalGenVertices` and `MdlNodeAnimMesh::InternalGenVertices`.
134/// See `docs/notes/mdl_mdx.md` §MdlNodeTriMesh.
135#[derive(Debug, Clone, PartialEq, Default)]
136pub struct MdlFace {
137    /// Face plane normal vector (3 × f32). MaxFace offset +0x00.
138    pub plane_normal: [f32; 3],
139    /// Face plane distance from origin (f32). MaxFace offset +0x0C.
140    pub plane_distance: f32,
141    /// Surface material/type ID (u32). MaxFace offset +0x10.
142    pub surface_id: u32,
143    /// Indices of the three adjacent faces (3 × u16). MaxFace offset +0x14.
144    pub adjacent: [u16; 3],
145    /// Vertex indices into the mesh vertex array (3 × u16). MaxFace offset +0x1A.
146    pub vertex_indices: [u16; 3],
147}
148
149/// A triangle mesh contained within a node.
150///
151/// Vertex attribute data comes from the companion MDX file. Each attribute
152/// is stored in a separate array, matching the interleaved MDX vertex layout
153/// controlled by the `mdx_vertex_flags` bitfield and per-attribute byte offsets
154/// in the TriMesh header (+0x100..+0x120).
155///
156/// See `docs/notes/mdl_mdx.md` §MDX Vertex Layout for the full specification.
157#[derive(Debug, Clone, Default, PartialEq)]
158pub struct MdlMesh {
159    // --- Toolset function pointer stubs (extra +0x00, +0x04) ---
160    // These are stale code addresses from BioWare's build toolset binary,
161    // baked into the MDL file during serialization. The engine overwrites
162    // them at load time with its own function pointers. They have no
163    // runtime meaning, but serve as a toolset version fingerprint:
164    // all vanilla K1 files share the same pair per mesh subtype.
165    //
166    // See `docs/notes/mdl_mdx.md` §MdlNodeTriMesh Binary Layout.
167    /// `gen_vertices` function pointer stub (u32). Extra +0x00.
168    pub fn_ptr_gen_vertices: u32,
169    /// `remove_temporary_array` function pointer stub (u32). Extra +0x04.
170    pub fn_ptr_remove_temp_array: u32,
171
172    // --- Inline scalar/fixed-size fields (Ghidra-verified) ---
173    // See `docs/notes/mdl_mdx.md` §MdlNodeTriMesh Binary Layout.
174    /// Bounding box minimum corner (3×f32). Extra +0x14.
175    pub bounding_min: [f32; 3],
176    /// Bounding box maximum corner (3×f32). Extra +0x20.
177    pub bounding_max: [f32; 3],
178    /// Bounding sphere radius (f32). Extra +0x2C.
179    pub bsphere_radius: f32,
180    /// Bounding sphere center (3×f32). Extra +0x30.
181    pub bsphere_center: [f32; 3],
182    /// RGB diffuse color (3×f32). Extra +0x3C.
183    pub diffuse_color: [f32; 3],
184    /// RGB ambient color (3×f32). Extra +0x48.
185    pub ambient_color: [f32; 3],
186    /// Transparency hint (0=opaque, 1=transparent). Extra +0x54.
187    pub transparency_hint: i32,
188    /// Primary texture name (up to 32 chars). Extra +0x58.
189    pub texture_0: String,
190    /// Secondary/lightmap texture name (up to 32 chars). Extra +0x78.
191    pub texture_1: String,
192    /// UV animation enable flag. Extra +0xE8.
193    pub animate_uv: i32,
194    /// UV animation direction X. Extra +0xEC.
195    pub uv_direction_x: f32,
196    /// UV animation direction Y. Extra +0xF0.
197    pub uv_direction_y: f32,
198    /// UV jitter amount. Extra +0xF4.
199    pub uv_jitter: f32,
200    /// UV jitter speed. Extra +0xF8.
201    pub uv_jitter_speed: f32,
202    /// Number of UV texture channels. Extra +0x132.
203    pub texture_channel_count: u16,
204    /// Lightmapped flag. Extra +0x134.
205    pub light_mapped: bool,
206    /// Rotate texture flag. Extra +0x135.
207    pub rotate_texture: bool,
208    /// Background geometry flag. Extra +0x136.
209    pub is_background_geometry: bool,
210    /// Beaming flag. Extra +0x138.
211    pub beaming: bool,
212    /// Total surface area (computed by toolset). Extra +0x13C.
213    pub total_surface_area: f32,
214
215    // --- MDX vertex attribute arrays ---
216    // Each array has `vertex_count` elements when populated, or is empty if
217    // the attribute is not present (offset == -1 in the mesh header).
218    /// Vertex positions (3×f32: x, y, z). MDX flag 0x01.
219    pub positions: Vec<[f32; 3]>,
220    /// Vertex normals (3×f32: x, y, z). MDX flag 0x20.
221    pub normals: Vec<[f32; 3]>,
222    /// Vertex colors (4×u8: R, G, B, A). No flag bit - offset != -1 indicates presence.
223    pub vertex_colors: Vec<[u8; 4]>,
224    /// Primary UV coordinates (2×f32: u, v). MDX flag 0x02.
225    pub uv1: Vec<[f32; 2]>,
226    /// Secondary UV coordinates (2×f32: u, v). MDX flag 0x04.
227    pub uv2: Vec<[f32; 2]>,
228    /// Tertiary UV coordinates (2×f32: u, v). MDX flag 0x08.
229    pub uv3: Vec<[f32; 2]>,
230    /// Quaternary UV coordinates (2×f32: u, v). MDX flag 0x10.
231    pub uv4: Vec<[f32; 2]>,
232    /// Tangent space basis (3×3×f32: tangent, bitangent, cross). MDX flag 0x80.
233    ///
234    /// Each entry is 36 bytes: three vec3s forming the tangent-space basis
235    /// used for bump/normal mapping.
236    pub tangent_space: Vec<[[f32; 3]; 3]>,
237
238    /// Face data parsed from the MaxFace array (32 bytes per face on disk).
239    pub faces: Vec<MdlFace>,
240
241    // --- TriMesh internal CExoArrayList fields (+0x98..+0xC8) ---
242    // These 5 CExoArrayList slots form a GL index buffer submission system.
243    // Three are derivable (face_count*3, packed u16 face indices, content
244    // pointer); one is a dead field (always zeros in KotOR); one stores the
245    // "inverted counter" mesh sequence value.
246    //
247    // See `docs/notes/mesh_derived_fields.md` for full documentation.
248    /// Mesh sequence "inverted counter" value from `index_buffer_pools` (+0xC8).
249    ///
250    /// Preserved from binary on roundtrip. For newly constructed models, compute
251    /// via the inverted counter formula (see `mesh_derived_fields.md` §1.5).
252    /// The engine overwrites this at load time with a GL pool handle.
253    pub inverted_counter: u32,
254
255    /// Whether the source binary used the "embedded position" variant for the
256    /// `vertex_indices_count` payload (+0xB0).
257    ///
258    /// When true, the writer emits a 4-byte tag + `vertex_count * 12` bytes
259    /// of position data instead of a single u32 `face_count * 3`.
260    pub has_embedded_positions: bool,
261
262    /// TriMesh shared index offset scalar (+0xD4).
263    pub shared_index_offset: i32,
264    /// TriMesh shared index pool scalar (+0xD8).
265    pub shared_index_pool: i32,
266    /// TriMesh shared index size scalar (+0xDC).
267    pub shared_index_size: i32,
268    /// TriMesh indices-per-face scalar (+0xE0). Vanilla value is typically 3.
269    pub indices_per_face: u32,
270    /// Number of vertices declared in the mesh header.
271    pub vertex_count: u16,
272    /// Is this mesh renderable? (MdlNodeTriMesh +0x189).
273    pub render: bool,
274    /// Does this mesh cast shadows? (MdlNodeTriMesh +0x187).
275    pub shadow: bool,
276}
277
278impl MdlMesh {
279    /// Compute the axis-aligned bounding box from vertex positions.
280    ///
281    /// Returns `(bounding_min, bounding_max)`, or `None` if positions are empty.
282    #[must_use]
283    pub fn compute_bounding_box(&self) -> Option<([f32; 3], [f32; 3])> {
284        let mut iter = self.positions.iter();
285        let first = iter.next()?;
286        let mut min = *first;
287        let mut max = *first;
288        for pos in iter {
289            for i in 0..3 {
290                if pos[i] < min[i] {
291                    min[i] = pos[i];
292                }
293                if pos[i] > max[i] {
294                    max[i] = pos[i];
295                }
296            }
297        }
298        Some((min, max))
299    }
300
301    /// Compute the centroid-based bounding sphere from vertex positions.
302    ///
303    /// Uses the engine's algorithm from `PartTriMesh::GetMinimumSphere`
304    /// (`0x00443330`): center = centroid, radius = max distance to any vertex.
305    /// This is NOT the true minimum bounding sphere (Welzl), but matches
306    /// vanilla file values.
307    ///
308    /// Returns `(center, radius)`, or `None` if positions are empty.
309    #[must_use]
310    pub fn compute_bounding_sphere(&self) -> Option<([f32; 3], f32)> {
311        if self.positions.is_empty() {
312            return None;
313        }
314        // Precision loss impossible: KotOR vertex counts fit easily in f64's 53-bit mantissa.
315        #[allow(clippy::cast_precision_loss, clippy::as_conversions)]
316        let n = self.positions.len() as f64;
317        let mut cx = 0.0f64;
318        let mut cy = 0.0f64;
319        let mut cz = 0.0f64;
320        for pos in &self.positions {
321            cx += f64::from(pos[0]);
322            cy += f64::from(pos[1]);
323            cz += f64::from(pos[2]);
324        }
325        // Intentional f64->f32 narrowing: geometry averaging is computed in double
326        // precision but stored as f32 per the MDL format.
327        #[allow(clippy::cast_possible_truncation, clippy::as_conversions)]
328        let center = [(cx / n) as f32, (cy / n) as f32, (cz / n) as f32];
329        let max_dist_sq = self
330            .positions
331            .iter()
332            .map(|pos| {
333                let dx = pos[0] - center[0];
334                let dy = pos[1] - center[1];
335                let dz = pos[2] - center[2];
336                dx * dx + dy * dy + dz * dz
337            })
338            .fold(0.0f32, f32::max);
339        Some((center, max_dist_sq.sqrt()))
340    }
341
342    /// Compute the total surface area of all triangles in the mesh.
343    ///
344    /// Returns 0.0 if positions or faces are empty.
345    #[must_use]
346    pub fn compute_total_surface_area(&self) -> f32 {
347        let positions = &self.positions;
348        if positions.is_empty() {
349            return 0.0;
350        }
351        let total: f64 = self
352            .faces
353            .iter()
354            .filter_map(|face| {
355                let v0 = usize::from(face.vertex_indices[0]);
356                let v1 = usize::from(face.vertex_indices[1]);
357                let v2 = usize::from(face.vertex_indices[2]);
358                if v0 >= positions.len() || v1 >= positions.len() || v2 >= positions.len() {
359                    return None;
360                }
361                let p0 = positions[v0];
362                let p1 = positions[v1];
363                let p2 = positions[v2];
364                let e1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
365                let e2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
366                let [cx, cy, cz] = cross_f32_as_f64(e1, e2);
367                Some((cx * cx + cy * cy + cz * cz).sqrt() * 0.5)
368            })
369            .sum();
370        // Intentional f64->f32 narrowing: cross-product magnitude sums are computed
371        // in double precision but the result is stored as f32.
372        #[allow(clippy::cast_possible_truncation, clippy::as_conversions)]
373        let result = total as f32;
374        result
375    }
376
377    /// Recompute `plane_normal` and `plane_distance` for all faces from vertex positions.
378    ///
379    /// Uses the standard cross-product formula:
380    /// ```text
381    /// normal = normalize(cross(v1 - v0, v2 - v0))
382    /// distance = -dot(normal, v0)
383    /// ```
384    /// Degenerate triangles (zero-area) get a zero normal and zero distance.
385    pub fn recompute_face_planes(&mut self) {
386        let positions = &self.positions;
387        for face in &mut self.faces {
388            let v0 = usize::from(face.vertex_indices[0]);
389            let v1 = usize::from(face.vertex_indices[1]);
390            let v2 = usize::from(face.vertex_indices[2]);
391            if v0 >= positions.len() || v1 >= positions.len() || v2 >= positions.len() {
392                face.plane_normal = [0.0; 3];
393                face.plane_distance = 0.0;
394                continue;
395            }
396            let p0 = positions[v0];
397            let p1 = positions[v1];
398            let p2 = positions[v2];
399            let e1 = [p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]];
400            let e2 = [p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]];
401            let [nx, ny, nz] = cross_f32(e1, e2);
402            let len = (nx * nx + ny * ny + nz * nz).sqrt();
403            if len > 0.0 {
404                face.plane_normal = [nx / len, ny / len, nz / len];
405                face.plane_distance = -(face.plane_normal[0] * p0[0]
406                    + face.plane_normal[1] * p0[1]
407                    + face.plane_normal[2] * p0[2]);
408            } else {
409                face.plane_normal = [0.0; 3];
410                face.plane_distance = 0.0;
411            }
412        }
413    }
414
415    /// Compute face adjacency from vertex positions using position-based edge matching.
416    ///
417    /// For each edge of each face, finds the adjacent face sharing that edge.
418    /// Uses `0xFFFF` as the no-neighbor sentinel. Position matching uses `{:.4e}`
419    /// formatting for floating-point key comparison (handles duplicate vertices
420    /// at the same position with different normals/UVs).
421    ///
422    /// For non-manifold edges (more than 2 faces sharing an edge), the smallest
423    /// face index is used (deterministic, matches PyKotor behavior).
424    pub fn compute_face_adjacency(&mut self) {
425        use std::collections::HashMap;
426
427        let positions = &self.positions;
428        if positions.is_empty() || self.faces.is_empty() {
429            return;
430        }
431
432        // Step 1: Build position key for each vertex.
433        let pos_key = |idx: usize| -> String {
434            if idx >= positions.len() {
435                return String::new();
436            }
437            let p = positions[idx];
438            format!("{:.4e},{:.4e},{:.4e}", p[0], p[1], p[2])
439        };
440
441        // Step 2: Group vertices by position.
442        let mut vertex_group: HashMap<String, Vec<usize>> = HashMap::new();
443        for i in 0..positions.len() {
444            vertex_group.entry(pos_key(i)).or_default().push(i);
445        }
446
447        // Step 3: Build vertex-to-faces map.
448        let mut vertex_to_faces: HashMap<usize, Vec<usize>> = HashMap::new();
449        for (fi, face) in self.faces.iter().enumerate() {
450            for &vi in &face.vertex_indices {
451                vertex_to_faces.entry(usize::from(vi)).or_default().push(fi);
452            }
453        }
454
455        // Step 4: For each vertex, collect all faces touching any vertex at the
456        // same position (handles UV seam / hard edge duplicates).
457        let face_set_for_vertex = |vi: usize| -> Vec<usize> {
458            let key = pos_key(vi);
459            let mut faces = Vec::new();
460            if let Some(group) = vertex_group.get(&key) {
461                for &gv in group {
462                    if let Some(flist) = vertex_to_faces.get(&gv) {
463                        faces.extend_from_slice(flist);
464                    }
465                }
466            }
467            faces.sort_unstable();
468            faces.dedup();
469            faces
470        };
471
472        // Step 5: For each face and each edge, find the adjacent face.
473        let face_count = self.faces.len();
474        let mut adjacency = vec![[0xFFFFu16; 3]; face_count];
475
476        for (fi, face) in self.faces.iter().enumerate() {
477            let vis = face.vertex_indices;
478            let edges = [
479                (usize::from(vis[0]), usize::from(vis[1])),
480                (usize::from(vis[1]), usize::from(vis[2])),
481                (usize::from(vis[2]), usize::from(vis[0])),
482            ];
483            for (ei, &(va, vb)) in edges.iter().enumerate() {
484                let faces_a = face_set_for_vertex(va);
485                let faces_b = face_set_for_vertex(vb);
486
487                // Intersect face sets, excluding self.
488                let mut best = None;
489                let (mut ia, mut ib) = (0, 0);
490                while ia < faces_a.len() && ib < faces_b.len() {
491                    match faces_a[ia].cmp(&faces_b[ib]) {
492                        std::cmp::Ordering::Less => ia += 1,
493                        std::cmp::Ordering::Greater => ib += 1,
494                        std::cmp::Ordering::Equal => {
495                            let candidate = faces_a[ia];
496                            if candidate != fi {
497                                // Use min (smallest index) for deterministic non-manifold handling.
498                                if best.is_none() {
499                                    best = Some(candidate);
500                                }
501                            }
502                            ia += 1;
503                            ib += 1;
504                        }
505                    }
506                }
507
508                if let Some(adj) = best {
509                    if adj <= 0xFFFE {
510                        adjacency[fi][ei] =
511                            u16::try_from(adj).expect("adj <= 0xFFFE guarantees it fits u16");
512                    }
513                }
514            }
515        }
516
517        // Apply computed adjacency.
518        for (fi, adj) in adjacency.into_iter().enumerate() {
519            self.faces[fi].adjacent = adj;
520        }
521    }
522
523    /// Recompute all derivable geometric fields at once.
524    ///
525    /// Updates: bounding box, bounding sphere, total surface area,
526    /// face plane normals/distances, and face adjacency.
527    pub fn recompute_derived_fields(&mut self) {
528        if let Some((bmin, bmax)) = self.compute_bounding_box() {
529            self.bounding_min = bmin;
530            self.bounding_max = bmax;
531        }
532        if let Some((center, radius)) = self.compute_bounding_sphere() {
533            self.bsphere_center = center;
534            self.bsphere_radius = radius;
535        }
536        self.total_surface_area = self.compute_total_surface_area();
537        self.recompute_face_planes();
538        self.compute_face_adjacency();
539    }
540}
541
542/// Light node data.
543///
544/// Binary layout: 92 extra bytes after the base node header.
545/// Contains scalar properties and flare data arrays.
546///
547/// Verified via `MdlNodeLight::MdlNodeLight` constructor (`0x0044a3f0`),
548/// `InputBinary::ResetLight` (`0x004a05e0`), and Ghidra struct
549/// `MdlNodeLight` (172 bytes total = 80 base + 92 extra).
550/// See `docs/notes/mdl_mdx.md` §Non-Mesh Node Type Structs.
551#[derive(Debug, Clone, PartialEq)]
552pub struct MdlLight {
553    /// Flare radius (f32, default 0.0). Extra offset +0x00.
554    pub flare_radius: f32,
555    /// Texture SafePointers (runtime-only). Extra +0x04.
556    ///
557    /// Three u32 values populated at runtime by `AurTextureGetReference`
558    /// after resolving texture names. In vanilla binaries these contain
559    /// stale addresses from the build toolset. Preserved for lossless
560    /// roundtrip and toolset fingerprinting (same rationale as the
561    /// mesh function pointer stubs).
562    pub texture_safe_ptrs: [u32; 3],
563    /// Flare sizes (one f32 per flare). Extra +0x10 CExoArrayList pointer.
564    pub flare_sizes: Vec<f32>,
565    /// Flare positions (one f32 per flare). Extra +0x1C CExoArrayList pointer.
566    pub flare_positions: Vec<f32>,
567    /// Flare color shifts (one vec3 per flare). Extra +0x28 CExoArrayList pointer.
568    pub flare_color_shifts: Vec<[f32; 3]>,
569    /// Flare texture names. Extra +0x34 CExoArrayList pointer.
570    ///
571    /// Stored as a CExoArrayList of char* pointers, where each pointer is
572    /// also relocated and points to a null-terminated string. Flattened to
573    /// a `Vec<String>` for ergonomic access.
574    pub flare_texture_names: Vec<String>,
575    /// Light priority (default 5). Extra offset +0x40.
576    pub priority: i32,
577    /// Dynamic type count (default 1). Extra offset +0x44.
578    pub num_dynamic_types: i32,
579    /// Affects dynamic objects (default 1). Extra offset +0x48.
580    pub affectdynamic: i32,
581    /// Casts shadow (default 1). Extra offset +0x4C.
582    pub shadow: i32,
583    /// Ambient-only light (default 0). Extra offset +0x50.
584    pub ambientonly: i32,
585    /// Generate flare effect (default 0). Extra offset +0x54.
586    pub generateflare: i32,
587    /// Fading light (default 1). Extra offset +0x58.
588    pub fading_light: i32,
589}
590
591impl Default for MdlLight {
592    /// Returns a light node with engine default values.
593    ///
594    /// Defaults match the `MdlNodeLight::MdlNodeLight` constructor (`0x0044a3f0`):
595    /// priority=5, num_dynamic_types=1, affectdynamic=1, shadow=1, fading_light=1.
596    fn default() -> Self {
597        MdlLight {
598            flare_radius: 0.0,
599            texture_safe_ptrs: [0u32; 3],
600            flare_sizes: vec![],
601            flare_positions: vec![],
602            flare_color_shifts: vec![],
603            flare_texture_names: vec![],
604            priority: 5,
605            num_dynamic_types: 1,
606            affectdynamic: 1,
607            shadow: 1,
608            ambientonly: 0,
609            generateflare: 0,
610            fading_light: 1,
611        }
612    }
613}
614
615/// Emitter node data.
616///
617/// Binary layout: 224 extra bytes (0xE0) after the 80-byte base node header.
618/// All fields are inline fixed-size data with no pointers - no relocation
619/// needed (only `ResetMdlNodeParts` is called, not a type-specific Reset).
620///
621/// The `update` field is architecturally significant: it determines which
622/// runtime emitter class is instantiated ("Fountain", "Explosion", "Single",
623/// or "Lightning"). Controller 502 ("detonate") is only valid for "Explosion"
624/// emitters.
625///
626/// Verified via `MdlNodeEmitter` constructor (`0x0044a300`, 304 bytes total),
627/// `MdlNodeEmitter::InternalParseField` (`0x00469700`, 2,632 bytes),
628/// and `MdlNodeEmitter::InternalCreateInstance` (`0x0049d5c0`).
629/// See `docs/notes/mdl_mdx.md` §Non-Mesh Node Type Structs.
630#[derive(Debug, Clone, PartialEq)]
631pub struct MdlEmitter {
632    /// Dead space / unused float (default 0.0). Extra offset +0x00.
633    pub deadspace: f32,
634    /// Blast radius (default 0.0). Extra offset +0x04.
635    pub blast_radius: f32,
636    /// Blast length (default 0.0). Extra offset +0x08.
637    pub blast_length: f32,
638    /// Number of branches (default 0). Extra offset +0x0C.
639    pub num_branches: i32,
640    /// Control point smoothing (default 0). Extra offset +0x10.
641    pub control_pt_smoothing: i32,
642    /// X grid size (default 0). Extra offset +0x14.
643    pub x_grid: i32,
644    /// Y grid size (default 0). Extra offset +0x18.
645    pub y_grid: i32,
646    /// Spawn type (default 0). Extra offset +0x1C.
647    pub spawn_type: i32,
648    /// Emitter update type string (up to 32 chars). Extra offset +0x20.
649    ///
650    /// Determines which runtime emitter class is instantiated:
651    /// - `"Fountain"` - steady particle effects (most common)
652    /// - `"Explosion"` - burst effects (supports controller 502 "detonate")
653    /// - `"Single"` - one-shot particle
654    /// - `"Lightning"` - lightning bolt effects
655    pub update: String,
656    /// Render type string (up to 32 chars). Extra offset +0x40.
657    pub render: String,
658    /// Blend type string (up to 32 chars). Extra offset +0x60.
659    pub blend: String,
660    /// Texture name (up to 32 chars). Extra offset +0x80.
661    pub texture: String,
662    /// Chunk name (up to 16 chars). Extra offset +0xA0.
663    pub chunk_name: String,
664    /// Two-sided texturing (default 0). Extra offset +0xB0.
665    pub two_sided_tex: i32,
666    /// Loop emitter (default 0). Extra offset +0xB4.
667    pub loop_emitter: i32,
668    /// Render order (default 0). Extra offset +0xB8.
669    pub render_order: u16,
670    /// Frame blending enabled (default false). Extra offset +0xBA.
671    pub frame_blending: bool,
672    /// Depth texture name (up to 16 chars). Extra offset +0xBB.
673    pub depth_texture_name: String,
674    /// Reserved/padding bytes at extra offset +0xCB..+0xE0 (21 bytes).
675    /// Preserved verbatim for roundtrip fidelity per the reserved field rule.
676    pub reserved: [u8; 21],
677}
678
679impl Default for MdlEmitter {
680    /// Returns an emitter with engine default values.
681    ///
682    /// All numeric fields default to zero, all strings default to empty.
683    fn default() -> Self {
684        MdlEmitter {
685            deadspace: 0.0,
686            blast_radius: 0.0,
687            blast_length: 0.0,
688            num_branches: 0,
689            control_pt_smoothing: 0,
690            x_grid: 0,
691            y_grid: 0,
692            spawn_type: 0,
693            update: String::new(),
694            render: String::new(),
695            blend: String::new(),
696            texture: String::new(),
697            chunk_name: String::new(),
698            two_sided_tex: 0,
699            loop_emitter: 0,
700            render_order: 0,
701            frame_blending: false,
702            depth_texture_name: String::new(),
703            reserved: [0u8; 21],
704        }
705    }
706}
707
708/// Camera node data.
709///
710/// Camera nodes have no type-specific binary data beyond the base 80-byte
711/// node header. Confirmed via `ParseFieldDispatch` dispatch table - camera
712/// (0x009) routes to the base `MdlNode::InternalParseField`, not a
713/// camera-specific parser.
714///
715/// See `docs/notes/mdl_mdx.md` Non-Mesh Node Type Structs.
716#[derive(Debug, Clone, PartialEq, Eq)]
717pub struct MdlCamera {
718    _private: (),
719}
720
721impl MdlCamera {
722    /// Creates a camera node (no type-specific data).
723    pub fn new() -> Self {
724        MdlCamera { _private: () }
725    }
726}
727
728impl Default for MdlCamera {
729    fn default() -> Self {
730        Self::new()
731    }
732}
733
734/// Reference node data.
735///
736/// References an external model by name, with an optional reattachment flag.
737/// Binary layout: 36 extra bytes after the base node header.
738///
739/// Verified via `MdlNodeReference::InternalParseField` (`0x00468070`) and
740/// Ghidra struct `MdlNodeReference` (116 bytes total = 80 base + 36 extra).
741/// See `docs/notes/mdl_mdx.md` §Non-Mesh Node Type Structs.
742#[derive(Debug, Clone, Default, PartialEq, Eq)]
743pub struct MdlReference {
744    /// Referenced model name (up to 32 chars). Extra offset +0x00.
745    pub ref_model: String,
746    /// Whether this reference can be reattached (default 0). Extra offset +0x20.
747    pub reattachable: i32,
748}
749
750/// Skinned mesh with bone weight data.
751///
752/// Extends [`MdlMesh`] with inverse bind pose data and bone index mappings
753/// used for skeletal animation. Each bone has an inverse bind rotation
754/// (quaternion) and translation (vector) that transform from bone space
755/// to model space.
756///
757/// Binary layout: 100 extra bytes (0x64) after the 332-byte TriMesh header.
758/// Verified via `InputBinary::ResetSkin` (`0x004a01b0`) and Ghidra struct
759/// `MdlNodeSkin` (512 bytes total = 412 TriMesh + 100 extra).
760/// See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
761#[derive(Debug, Clone, PartialEq)]
762pub struct MdlSkin {
763    /// The base triangle mesh.
764    pub mesh: MdlMesh,
765    /// MDX per-vertex bone weights byte offset (i32). Skin extra +0x0C.
766    ///
767    /// Byte offset within each MDX vertex stride to the 4-float bone weights.
768    /// Value -1 means not present.
769    pub mdx_bone_weights_offset: i32,
770    /// MDX per-vertex bone indices byte offset (i32). Skin extra +0x10.
771    ///
772    /// Byte offset within each MDX vertex stride to the 4-float bone indices.
773    /// Value -1 means not present.
774    pub mdx_bone_indices_offset: i32,
775    /// Per-vertex bone weights (4 x f32, one per influence slot).
776    ///
777    /// Each vertex has up to 4 bone influences. Unused slots are zero.
778    /// One entry per vertex when populated, empty when no skin weights are present.
779    pub bone_weights: Vec<[f32; 4]>,
780    /// Per-vertex bone indices (4 x f32, one per influence slot).
781    ///
782    /// Stored as f32 by engine convention (small non-negative integers).
783    /// Each index references a bone in the bonemap. Unused slots are zero.
784    /// One entry per vertex when populated, empty when no skin weights are present.
785    pub bone_indices: Vec<[f32; 4]>,
786    /// Bone-to-node index mapping from skin extra +0x14 pointer.
787    ///
788    /// Each u32 maps a local bone index to a global skeleton node index.
789    /// The engine relocates the pointer when the count (extra +0x18) > 0.
790    pub bonemap: Vec<u32>,
791    /// Inverse bind rotation per bone (quaternion: w, x, y, z).
792    ///
793    /// One entry per bone. Transforms from bone space to model space.
794    /// Data pointed to by CExoArrayList at skin extra +0x1C.
795    pub qbone_ref_inv: Vec<[f32; 4]>,
796    /// Inverse bind translation per bone (vector: x, y, z).
797    ///
798    /// One entry per bone. Transforms from bone space to model space.
799    /// Data pointed to by CExoArrayList at skin extra +0x28.
800    pub tbone_ref_inv: Vec<[f32; 3]>,
801    /// Bone constant indices mapping local bone index to global skeleton node index.
802    ///
803    /// Data pointed to by CExoArrayList at skin extra +0x34.
804    pub bone_constant_indices: Vec<i32>,
805    /// Fixed-size array of 16 bone node serial numbers (u16). Skin extra +0x40.
806    ///
807    /// Maps up to 16 local bone slots to node indices in the model hierarchy.
808    /// Unused slots are zero.
809    pub bone_node_numbers: [u16; 16],
810}
811
812impl Default for MdlSkin {
813    /// Defaults with MDX bone offsets set to -1 (not present).
814    fn default() -> Self {
815        Self {
816            mesh: MdlMesh::default(),
817            mdx_bone_weights_offset: -1,
818            mdx_bone_indices_offset: -1,
819            bone_weights: Vec::new(),
820            bone_indices: Vec::new(),
821            bonemap: Vec::new(),
822            qbone_ref_inv: Vec::new(),
823            tbone_ref_inv: Vec::new(),
824            bone_constant_indices: Vec::new(),
825            bone_node_numbers: [0u16; 16],
826        }
827    }
828}
829
830/// Animated mesh with per-frame vertex sets.
831///
832/// Extends [`MdlMesh`] with animated vertex positions and texture coordinates
833/// that vary over time. The `sample_period` controls the animation playback rate.
834///
835/// Binary layout: 56 extra bytes (0x38) after the 332-byte TriMesh header.
836/// Note: `ResetAnim` processes the extra data BEFORE calling `ResetTriMeshParts`
837/// (reversed order vs other subtypes).
838///
839/// Verified via `InputBinary::ResetAnim` (`0x004a0060`) and Ghidra struct
840/// `MdlNodeAnimMesh` (468 bytes total = 412 TriMesh + 56 extra).
841/// See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
842#[derive(Debug, Clone, Default, PartialEq)]
843pub struct MdlAnimMesh {
844    /// The base triangle mesh.
845    pub mesh: MdlMesh,
846    /// Animation sampling period (f32). AnimMesh extra +0x00.
847    pub sample_period: f32,
848    /// Animated vertex positions (Vector: x, y, z per entry).
849    ///
850    /// Data pointed to by CExoArrayList at anim extra +0x04.
851    pub anim_verts: Vec<[f32; 3]>,
852    /// Animated texture coordinates (Vector: u, v, w per entry).
853    ///
854    /// Data pointed to by CExoArrayList at anim extra +0x10.
855    pub anim_t_verts: Vec<[f32; 3]>,
856    // --- Runtime-only fields (always zero in authored files) ---
857    // These 6 fields have no ASCII parser names (confirmed via
858    // `MdlNodeAnimMesh::InternalParseField` at 0x0046a240).
859    // They are populated at runtime by `InternalGenVertices`.
860    // Preserved for roundtrip fidelity.
861    /// Runtime data pointer 1 (u32, relocated if `data_count_1 != 0`). Extra +0x1C.
862    pub data_ptr_1: u32,
863    /// Size guard for `data_ptr_1` (u32). Extra +0x20.
864    pub data_count_1: u32,
865    /// Padding (u32). Extra +0x24.
866    pub padding_24: u32,
867    /// Runtime animated vertices pointer (u32). Extra +0x28.
868    pub anim_vertices_ptr: u32,
869    /// Runtime animated texture vertices pointer (u32). Extra +0x2C.
870    pub anim_tex_vertices_ptr: u32,
871    /// Count for `anim_vertices_ptr` (u32). Extra +0x30.
872    pub anim_vertices_count: u32,
873    /// Count for `anim_tex_vertices_ptr` (u32). Extra +0x34.
874    pub anim_tex_vertices_count: u32,
875}
876
877/// Dangly mesh with physics constraints.
878///
879/// Extends [`MdlMesh`] with per-vertex constraint weights and physics parameters
880/// that control dangling geometry behavior (hair, cloaks, banners, etc.).
881///
882/// Binary layout: 28 extra bytes (0x1C) after the 332-byte TriMesh header.
883/// Verified via `InputBinary::ResetDangly` (`0x004a0100`) and Ghidra struct
884/// `MdlNodeDanglyMesh` (440 bytes total = 412 TriMesh + 28 extra).
885/// See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
886#[derive(Debug, Clone, Default, PartialEq)]
887pub struct MdlDangly {
888    /// The base triangle mesh.
889    pub mesh: MdlMesh,
890    /// Per-vertex constraint weights (float array).
891    ///
892    /// Each value constrains how much a vertex can move (0.0 = fully fixed,
893    /// higher = more freedom). Array length matches `mesh.vertex_count`.
894    /// Data pointed to by CExoArrayList at dangly extra +0x00.
895    pub constraints: Vec<f32>,
896    /// Maximum displacement distance (f32). Dangly extra +0x0C.
897    pub displacement: f32,
898    /// Spring tightness factor (f32). Dangly extra +0x10.
899    pub tightness: f32,
900    /// Oscillation period (f32). Dangly extra +0x14.
901    pub period: f32,
902    /// Per-vertex positions for the dangly constraint system (vec3 array).
903    ///
904    /// Each entry is a 3-component position `[x, y, z]` for one constraint
905    /// vertex. Array length matches `mesh.vertex_count`. Pointed to by the
906    /// conditional data pointer at dangly extra +0x18 (only present when
907    /// `vertex_count > 0`).
908    ///
909    /// At runtime, the engine copies this array into a GL vertex pool via
910    /// `PartDanglyMesh` constructor (`0x00447980`): allocates
911    /// `vertex_count * 12` bytes, copies `vertex_count` vec3s from this
912    /// pointer, then uploads to GL.
913    pub dangly_vertices: Vec<[f32; 3]>,
914}
915
916/// AABB walkmesh tree node.
917///
918/// Extends [`MdlMesh`] with a bounding-volume hierarchy used for
919/// walkmesh collision detection. The tree is stored inline in the MDL
920/// content blob as a flattened binary tree of axis-aligned bounding boxes.
921///
922/// Binary layout: 4 extra bytes after the 332-byte TriMesh header.
923/// The single field is a pointer to the root AABB tree node.
924///
925/// Verified via `ResetMdlNode` inline processing and `ResetAABBTree`
926/// (`0x004a0260`). See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
927#[derive(Debug, Clone, Default, PartialEq)]
928pub struct MdlAabb {
929    /// The base triangle mesh.
930    pub mesh: MdlMesh,
931    /// Parsed AABB binary search tree. `None` if the root pointer was null.
932    pub aabb_tree: Option<Box<AabbNode>>,
933}
934
935/// A node in the AABB binary search tree used for spatial face queries.
936///
937/// Each node occupies 40 bytes on disk: 6×f32 bounding box, right/left child
938/// pointers, face index, and split direction flags. Internal nodes partition
939/// space along an axis (encoded in [`split_direction_flags`](AabbNode::split_direction_flags))
940/// and have two children. Leaf nodes reference a single face by index.
941///
942/// Binary layout (40 bytes):
943///
944/// | Offset | Size | Field |
945/// |--------|------|-------|
946/// | +0x00 | 12 | `box_min` (3×f32) |
947/// | +0x0C | 12 | `box_max` (3×f32) |
948/// | +0x18 | 4 | `right_child` (u32, content-relative offset, 0 = leaf) |
949/// | +0x1C | 4 | `left_child` (u32, content-relative offset, 0 = leaf) |
950/// | +0x20 | 4 | `face_index` (i32, -1 for internal nodes) |
951/// | +0x24 | 4 | `split_direction_flags` (u32, axis bitmask) |
952///
953/// Verified via Ghidra AABB struct (size 40), `operator_new(0x28)` in
954/// `Parse_AABB`, and `ResetAABBTree` (`0x004a0260`).
955/// See `docs/notes/mdl_mdx.md` §AABB Tree Node Layout.
956#[derive(Debug, Clone, PartialEq)]
957pub struct AabbNode {
958    /// Axis-aligned bounding box minimum corner.
959    pub box_min: [f32; 3],
960    /// Axis-aligned bounding box maximum corner.
961    pub box_max: [f32; 3],
962    /// Face index for leaf nodes (>= 0). Internal nodes use -1.
963    pub face_index: i32,
964    /// Split plane direction flags (internal nodes only, 0 for leaves).
965    ///
966    /// Bitmask encoding which axis the split occurred on:
967    /// 1=+X, 2=+Y, 4=+Z, 8=-X, 16=-Y, 32=-Z.
968    pub split_direction_flags: u32,
969    /// Left child subtree (`None` for leaf nodes).
970    pub left: Option<Box<AabbNode>>,
971    /// Right child subtree (`None` for leaf nodes).
972    pub right: Option<Box<AabbNode>>,
973}
974
975/// Lightsaber blade mesh.
976///
977/// Extends [`MdlMesh`] with saber-specific geometry data for 176 fixed
978/// vertices. All lightsaber models use exactly `NUM_SABER_VERTS` (176)
979/// vertices for blade geometry.
980///
981/// Binary layout: 20 extra bytes (0x14) after the 332-byte TriMesh header.
982/// Contains 3 relocated data pointers for saber vertex data and 2 runtime
983/// GL render pool IDs (allocated by `GLRender::RequestPool` at load time,
984/// not meaningful in the binary file).
985///
986/// Semantic names from kotorblender: `off_saber_verts`, `off_saber_uv`,
987/// `off_saber_normals`, `inv_count1`, `inv_count2`.
988///
989/// Verified via `InputBinary::ResetLightsaber` (`0x004a0460`) and
990/// `ParseNode` allocation (`operator_new(0x1B0)`).
991/// See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
992#[derive(Debug, Clone, Default, PartialEq)]
993pub struct MdlSaber {
994    /// The base triangle mesh.
995    pub mesh: MdlMesh,
996    /// Saber vertex positions (176 × vec3). Saber extra +0x00 pointer.
997    pub saber_verts: Vec<[f32; 3]>,
998    /// Saber texture coordinates (176 × vec2). Saber extra +0x04 pointer.
999    pub saber_uvs: Vec<[f32; 2]>,
1000    /// Saber vertex normals (176 × vec3). Saber extra +0x08 pointer.
1001    pub saber_normals: Vec<[f32; 3]>,
1002    /// GL vertex pool ID (runtime-only, stale in binary). Saber extra +0x0C.
1003    pub gl_pool_vert: u32,
1004    /// GL index pool ID (runtime-only, stale in binary). Saber extra +0x10.
1005    pub gl_pool_index: u32,
1006}
1007
1008/// Cross product of two 3D vectors in f32.
1009fn cross_f32(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
1010    [
1011        a[1] * b[2] - a[2] * b[1],
1012        a[2] * b[0] - a[0] * b[2],
1013        a[0] * b[1] - a[1] * b[0],
1014    ]
1015}
1016
1017/// Cross product of two f32 vectors computed in f64 precision.
1018fn cross_f32_as_f64(a: [f32; 3], b: [f32; 3]) -> [f64; 3] {
1019    [
1020        f64::from(a[1]) * f64::from(b[2]) - f64::from(a[2]) * f64::from(b[1]),
1021        f64::from(a[2]) * f64::from(b[0]) - f64::from(a[0]) * f64::from(b[2]),
1022        f64::from(a[0]) * f64::from(b[1]) - f64::from(a[1]) * f64::from(b[0]),
1023    ]
1024}