Skip to main content

rakata_formats/mdl/
reader.rs

1//! MDL binary reader.
2//!
3//! The reader uses a two-pass architecture:
4//!
5//! **Pass 1 (node tree):** Recursive DFS traversal of the node hierarchy. Each
6//! node's headers and type-specific extra data are parsed, and mesh nodes
7//! collect an `MdxMeshInfo` descriptor with stride, vertex count, and
8//! per-attribute byte offsets. Vertex attribute arrays are left empty at this
9//! stage - only structural metadata is captured.
10//!
11//! **Pass 2 (MDX vertex population):** After the full tree is built, vertex
12//! data is populated from the MDX buffer (or from MDL content-blob fallback
13//! positions when no MDX is present). Meshes are visited in non-skin-first
14//! order - all non-skin meshes in DFS order, then all skin meshes in DFS
15//! order - matching the BioWare engine's canonical MDX layout. A cumulative
16//! cursor advances through the MDX buffer, reading interleaved vertex
17//! attributes and skipping terminator/alignment padding between meshes.
18//!
19//! This two-pass design decouples the node parse order (DFS) from the MDX
20//! data order (non-skin-first), enabling correct vertex assignment for vanilla
21//! game files where these orders differ.
22
23use std::io::{Cursor, Read, Seek, SeekFrom};
24
25use crate::binary::{
26    check_slice_in_bounds, checked_to_usize, read_f32, read_i32, read_u16, read_u32, read_u8,
27};
28
29use super::controllers::{MdlController, MdlControllerType, MdlKey};
30use super::types::{
31    AabbNode, MdlAabb, MdlAnimMesh, MdlCamera, MdlDangly, MdlLight, MdlMesh, MdlNodeData,
32    MdlReference, MdlSaber, MdlSkin,
33};
34use super::{
35    aabb_offsets, anim_header_offsets, anim_mesh_offsets, dangly_offsets, header_offsets,
36    light_offsets, mesh_offsets, node_flags, node_offsets, saber_offsets, skin_offsets, Mdl,
37    MdlAnimEvent, MdlAnimNode, MdlAnimation, MdlError, MdlNode, AABB_EXTRA_SIZE,
38    ANIMATION_EVENT_SIZE, ANIMATION_HEADER_SIZE, ANIM_MESH_EXTRA_SIZE, DANGLY_EXTRA_SIZE,
39    EMITTER_EXTRA_SIZE, LIGHT_EXTRA_SIZE, MAX_FACE_SIZE, MDL_WRAPPER_SIZE, MESH_EXTRA_SIZE,
40    NODE_HEADER_SIZE, NUM_SABER_VERTS, REFERENCE_EXTRA_SIZE, SABER_EXTRA_SIZE, SKIN_EXTRA_SIZE,
41};
42
43/// Reads an MDL file from the given reader.
44///
45/// This buffers the entire stream to memory.
46///
47/// # Errors
48///
49/// [`MdlError::Io`] when the stream will not read to end, and whatever
50/// [`read_mdl_from_bytes`] reports for the bytes it collected.
51#[cfg_attr(
52    feature = "tracing",
53    tracing::instrument(level = "debug", skip(reader, mdx_bytes))
54)]
55pub fn read_mdl<R: Read>(reader: &mut R, mdx_bytes: Option<&[u8]>) -> Result<Mdl, MdlError> {
56    let mut buffer = Vec::new();
57    reader.read_to_end(&mut buffer)?;
58    crate::trace_debug!(bytes_len = buffer.len(), "read mdl bytes from reader");
59    read_mdl_from_bytes(&buffer, mdx_bytes)
60}
61
62/// Reads an MDL file from the given byte slice.
63///
64/// Accepts an optional `mdx_bytes` slice for external geometry data.
65/// This currently supports reading the node hierarchy, names, transforms,
66/// and basic mesh headers (vertex counts).
67///
68/// # Errors
69///
70/// [`MdlError::InvalidHeader`] when `bytes` are shorter than the MDL header or
71/// a declared section runs past the end, [`MdlError::InvalidData`] when the
72/// node tree does not hang together, [`MdlError::ValueOverflow`] when a
73/// declared offset or count will not fit `usize`, and [`MdlError::Binary`]
74/// when a primitive read runs off the end.
75///
76/// An absent `mdx_bytes` is not an error: the model reads without vertex data,
77/// which is what a caller inspecting node structure wants. An MDX that is
78/// present and too short truncates the affected mesh rather than failing, the
79/// way vanilla K1 item models sharing one MDX buffer require.
80#[cfg_attr(
81    feature = "tracing",
82    tracing::instrument(level = "debug", skip(bytes, mdx_bytes), fields(bytes_len = bytes.len()))
83)]
84pub fn read_mdl_from_bytes(bytes: &[u8], mdx_bytes: Option<&[u8]>) -> Result<Mdl, MdlError> {
85    read_mdl_from_cursor(&mut Cursor::new(bytes), mdx_bytes)
86}
87
88// ---------------------------------------------------------------------------
89// Two-pass MDX reading infrastructure
90// ---------------------------------------------------------------------------
91
92/// Metadata collected during Pass 1 (DFS tree parse) for deferred MDX reading.
93///
94/// During Pass 1, the tree is built with empty vertex arrays on every mesh.
95/// Each mesh's MDX-relevant metadata is stored here so that Pass 2 can read
96/// vertex data in the correct ordering (non-skin-first, matching the writer).
97struct MdxMeshInfo {
98    /// Per-vertex byte stride in the MDX data.
99    stride: u32,
100    /// Number of vertices declared in the mesh header.
101    vertex_count: usize,
102    /// Per-mesh offset into the MDX file (mesh header +0x144).
103    /// Used by the engine and community tools to seek to the correct
104    /// position in the MDX buffer for each mesh's interleaved vertex data.
105    mdx_data_offset: usize,
106    /// Content-relative pointer to position-only vertex data (mesh header
107    /// +0x148). Points to vertex_count * 12 bytes (3x f32) in the MDL
108    /// content blob. Used as fallback when no MDX file is available.
109    vert_array_offset: usize,
110    /// Per-attribute byte offsets within the interleaved stride (-1 = absent).
111    pos_off: i32,
112    norm_off: i32,
113    color_off: i32,
114    uv1_off: i32,
115    uv2_off: i32,
116    uv3_off: i32,
117    uv4_off: i32,
118    tangent_off: i32,
119    /// Bone weight/index byte offsets (skin meshes only, -1 when absent).
120    bone_weights_off: i32,
121    bone_indices_off: i32,
122}
123
124/// Vertex data read from MDX for a single mesh, ready to be applied to the tree.
125#[derive(Default)]
126struct MdxVertexData {
127    positions: Vec<[f32; 3]>,
128    normals: Vec<[f32; 3]>,
129    vertex_colors: Vec<[u8; 4]>,
130    uv1: Vec<[f32; 2]>,
131    uv2: Vec<[f32; 2]>,
132    uv3: Vec<[f32; 2]>,
133    uv4: Vec<[f32; 2]>,
134    tangent_space: Vec<[[f32; 3]; 3]>,
135    bone_weights: Vec<[f32; 4]>,
136    bone_indices: Vec<[f32; 4]>,
137    /// If the MDX data was truncated, the clamped vertex count.
138    clamped_vertex_count: Option<u16>,
139}
140
141// ---------------------------------------------------------------------------
142// Shared reader helpers
143// ---------------------------------------------------------------------------
144
145// ---------------------------------------------------------------------------
146// Top-level reader
147// ---------------------------------------------------------------------------
148
149fn read_mdl_from_cursor<R: Read + Seek>(
150    reader: &mut R,
151    mdx_bytes: Option<&[u8]>,
152) -> Result<Mdl, MdlError> {
153    // 1. Skip 12-byte preamble/wrapper.
154    let len = reader.seek(SeekFrom::End(0))?;
155    if len < MDL_WRAPPER_SIZE {
156        return Err(crate::binary::BinaryLayoutError::UnexpectedEof("preamble").into());
157    }
158    reader.seek(SeekFrom::Start(MDL_WRAPPER_SIZE))?;
159
160    // Read remaining content (treating byte 12 as offset 0)
161    let mut content = Vec::new();
162    reader.read_to_end(&mut content)?;
163    let bytes = &content;
164
165    // 2. Parse Header (196 bytes: 80-byte geometry header + 116-byte model header)
166    if bytes.len() < 196 {
167        return Err(crate::binary::BinaryLayoutError::UnexpectedEof("header").into());
168    }
169
170    // --- Geometry header fields ---
171    let geometry_fn_ptr1 = read_u32(bytes, header_offsets::FN_PTR1)?;
172    let geometry_fn_ptr2 = read_u32(bytes, header_offsets::FN_PTR2)?;
173    let root_node_offset = checked_to_usize(
174        read_u32(bytes, header_offsets::ROOT_NODE_PTR)?,
175        "root_offset",
176    )?;
177    let node_count = read_u32(bytes, header_offsets::NODE_COUNT)?;
178    let model_type = read_u8(bytes, header_offsets::MODEL_TYPE)?;
179
180    // --- Model header fields ---
181    let classification = read_u8(bytes, header_offsets::CLASSIFICATION)?;
182    let subclassification = read_u8(bytes, header_offsets::SUBCLASSIFICATION)?;
183    let affected_by_fog = read_u8(bytes, header_offsets::AFFECTED_BY_FOG)?;
184
185    // Bounding box: 6 floats (min_xyz, max_xyz)
186    let mut bounding_box = [0.0f32; 6];
187    for (i, val) in bounding_box.iter_mut().enumerate() {
188        *val = read_f32(bytes, header_offsets::BOUNDING_BOX_MIN + i * 4)?;
189    }
190    let radius = read_f32(bytes, header_offsets::RADIUS)?;
191    let animation_scale = read_f32(bytes, header_offsets::ANIMATION_SCALE)?;
192
193    // Supermodel name: null-terminated string at +0x88, up to 32 bytes.
194    let supermodel_name = read_fixed_string(
195        bytes,
196        header_offsets::SUPERMODEL_NAME,
197        header_offsets::SUPERMODEL_NAME_SIZE,
198    );
199
200    let name_offsets_ptr = checked_to_usize(
201        read_u32(bytes, header_offsets::NAME_OFFSETS_PTR)?,
202        "name_offsets",
203    )?;
204    let name_count = read_u32(bytes, header_offsets::NAME_COUNT)?;
205
206    // 3. Name Resolution
207    let mut names = Vec::new();
208    if name_offsets_ptr > 0 && name_offsets_ptr < bytes.len() {
209        let name_count = checked_to_usize(name_count, "name_count")?;
210        for i in 0..name_count {
211            let ptr_offset = name_offsets_ptr + (i * 4);
212            if ptr_offset + 4 > bytes.len() {
213                crate::trace_warn!(
214                    index = i,
215                    ptr_offset,
216                    bytes_len = bytes.len(),
217                    "name offset array truncated"
218                );
219                break;
220            }
221
222            let name_offset = checked_to_usize(read_u32(bytes, ptr_offset)?, "name_ptr")?;
223
224            if name_offset < bytes.len() {
225                names.push(crate::binary::read_c_string(bytes, name_offset));
226            } else {
227                crate::trace_warn!(
228                    index = i,
229                    name_offset,
230                    bytes_len = bytes.len(),
231                    "name string offset out of bounds, using synthetic name"
232                );
233                names.push(format!("Node_{}", i));
234            }
235        }
236    }
237
238    // 4. Pass 1: Recursive tree traversal (no MDX reading).
239    // Builds the complete node tree with empty vertex arrays on meshes.
240    // MDX metadata is collected into `mdx_infos` for Pass 2.
241    let mut mdx_infos: Vec<MdxMeshInfo> = Vec::new();
242    let mut dfs_mesh_counter: usize = 0;
243    let mut root = read_node(
244        bytes,
245        root_node_offset,
246        &names,
247        None,
248        None,
249        &mut mdx_infos,
250        &mut dfs_mesh_counter,
251    )?;
252
253    // 5. Pass 2: Populate vertex data from MDX or MDL content.
254    //
255    // MDX vertex data is laid out in non-skin-first order (BioWare vanilla
256    // convention): all non-skin meshes first in DFS order, then all skin
257    // meshes in DFS order. The writer uses this same ordering.
258    //
259    // TODO: MDX ordering auto-detection for modded files.
260    //
261    // Currently assumes non-skin-first ordering (matches BioWare vanilla).
262    // Files produced by mdlops (DFS order) or PyKotor (node-ID order) with
263    // mixed skin/non-skin mesh nodes will get vertex data misassigned.
264    //
265    // Future: detect ordering via sentinel float scanning - check for 10M/1M
266    // sentinel values at expected positions for each candidate ordering, pick
267    // the one that matches. See vanilla-inspector's `diagnose_mdx_strategies()`
268    // for the 10-strategy approach.
269    if let Some(mdx) = mdx_bytes {
270        populate_mdx_data(&mut root, mdx, &mdx_infos)?;
271        // Fallback: mesh nodes that the MDX pass skipped (stride=0, e.g.,
272        // saber nodes) still need their embedded content positions read from
273        // vert_array_offset (+0x148). Without this, the writer would emit
274        // vert_array_offset=0 and the engine loses the position fallback.
275        populate_content_positions_fallback(&mut root, bytes, &mdx_infos)?;
276    } else {
277        populate_content_positions(&mut root, bytes, &mdx_infos)?;
278    }
279
280    // 6. Parse Animations
281    let anim_arr_ptr = read_u32(bytes, header_offsets::ANIMATION_ARR_PTR)?;
282    let anim_arr_count = read_u32(bytes, header_offsets::ANIMATION_ARR_COUNT)?;
283    let animations = read_animations(bytes, anim_arr_ptr, anim_arr_count, &names)?;
284
285    // 7. Resolve off_anim_root (+0xA8).
286    // Head models point this to `neck_g` instead of the geometry root.
287    let off_anim_root_raw = read_u32(bytes, header_offsets::OFF_ANIM_ROOT)?;
288    let anim_root_node = if off_anim_root_raw
289        != u32::try_from(root_node_offset)
290            .map_err(|_| MdlError::ValueOverflow("root_node_offset"))?
291        && off_anim_root_raw > 0
292    {
293        let ar_offset = checked_to_usize(off_anim_root_raw, "off_anim_root")?;
294        if ar_offset + 6 <= bytes.len() {
295            let name_idx = usize::from(read_u16(bytes, ar_offset + node_offsets::NAME_INDEX)?);
296            if name_idx < names.len() {
297                Some(names[name_idx].clone())
298            } else {
299                None
300            }
301        } else {
302            None
303        }
304    } else {
305        None
306    };
307
308    Ok(Mdl {
309        root_node: root,
310        geometry_fn_ptr1,
311        geometry_fn_ptr2,
312        model_type,
313        classification,
314        subclassification,
315        affected_by_fog,
316        supermodel_name,
317        node_count,
318        bounding_box,
319        radius,
320        animation_scale,
321        animations,
322        anim_root_node,
323    })
324}
325
326// ---------------------------------------------------------------------------
327// Shared controller reader
328// ---------------------------------------------------------------------------
329
330/// Reads controller key headers and their associated keyframe data.
331///
332/// Used by both geometry nodes and animation nodes - the binary format
333/// is identical. Each controller key header is 16 bytes.
334/// Result of reading controller arrays from a node header.
335///
336/// Contains both parsed controllers (when key_count > 0) and any orphan data
337/// floats (when key_count == 0 but data_count > 0).
338struct ControllerReadResult {
339    controllers: Vec<MdlController>,
340    orphan_data: Vec<f32>,
341}
342
343fn read_controllers(
344    bytes: &[u8],
345    key_ptr: usize,
346    key_count: usize,
347    data_ptr: usize,
348    data_count: usize,
349) -> Result<ControllerReadResult, MdlError> {
350    // Read controller data floats first.
351    let mut controller_data = Vec::with_capacity(data_count);
352    if data_count > 0 && data_ptr > 0 {
353        check_slice_in_bounds(bytes, data_ptr, data_count * 4, "controller_data")?;
354        for i in 0..data_count {
355            let val = read_f32(bytes, data_ptr + (i * 4))?;
356            controller_data.push(val);
357        }
358    }
359
360    let mut controllers = Vec::with_capacity(key_count);
361    if key_count > 0 && key_ptr > 0 {
362        for i in 0..key_count {
363            let key_offset = key_ptr + (i * 16);
364            if key_offset + 16 > bytes.len() {
365                crate::trace_warn!(
366                    index = i,
367                    key_offset,
368                    bytes_len = bytes.len(),
369                    "controller key header truncated"
370                );
371                break;
372            }
373
374            let type_code = read_u32(bytes, key_offset)?;
375            let controller_type = MdlControllerType::from(type_code);
376
377            // Preserve unknown bytes for roundtrip fidelity (reserved field rule).
378            let mut key_unknown_04 = [0u8; 2];
379            key_unknown_04.copy_from_slice(&bytes[key_offset + 4..key_offset + 6]);
380
381            let row_count = usize::from(read_u16(bytes, key_offset + 6)?);
382            let time_index = usize::from(read_u16(bytes, key_offset + 8)?);
383            let data_index = usize::from(read_u16(bytes, key_offset + 10)?);
384            let raw_column_count = read_u8(bytes, key_offset + 12)?;
385
386            let mut key_unknown_0d = [0u8; 3];
387            key_unknown_0d.copy_from_slice(&bytes[key_offset + 13..key_offset + 16]);
388
389            // Decode column_count to determine actual values per keyframe row.
390            //
391            // The raw byte encodes:
392            // - Lower 4 bits: base column count
393            // - Bit 4 (0x10): Bezier flag (3x control point triplets)
394            // - Special case: ORIENTATION with raw == 2 means integral
395            //     compressed quaternion (1 u32 per row, stored as f32 bits)
396            //
397            // See kotorblender reader.py load_controllers() for reference.
398            let actual_columns =
399                if controller_type == MdlControllerType::ORIENTATION && raw_column_count == 2 {
400                    // Integral compressed quaternion: 1 packed u32 per row.
401                    // Read as f32, bit pattern preserved for roundtrip.
402                    1
403                } else {
404                    let base = usize::from(raw_column_count & 0x0F);
405                    let has_bezier = (raw_column_count & super::controllers::CTRL_FLAG_BEZIER) != 0;
406                    if has_bezier {
407                        base * 3
408                    } else {
409                        base
410                    }
411                };
412
413            // Reconstruct keyframe rows.
414            let mut keys = Vec::with_capacity(row_count);
415
416            if time_index + row_count <= controller_data.len() {
417                for r in 0..row_count {
418                    let time = controller_data[time_index + r];
419                    let mut values = Vec::with_capacity(actual_columns);
420
421                    let data_start = data_index + (r * actual_columns);
422                    if data_start + actual_columns <= controller_data.len() {
423                        for c in 0..actual_columns {
424                            values.push(controller_data[data_start + c]);
425                        }
426                    } else {
427                        crate::trace_warn!(
428                            controller = ?controller_type,
429                            row = r,
430                            data_start,
431                            actual_columns,
432                            raw_column_count,
433                            data_len = controller_data.len(),
434                            "controller keyframe data out of bounds"
435                        );
436                    }
437                    keys.push(MdlKey { time, values });
438                }
439            } else {
440                crate::trace_warn!(
441                    controller = ?controller_type,
442                    time_index,
443                    row_count,
444                    data_len = controller_data.len(),
445                    "controller time indices out of bounds"
446                );
447            }
448
449            controllers.push(MdlController {
450                controller_type,
451                keys,
452                raw_column_count,
453                key_unknown_04,
454                key_unknown_0d,
455            });
456        }
457    }
458
459    // When key_count == 0 but data was read, preserve as orphan data.
460    let orphan_data = if controllers.is_empty() && !controller_data.is_empty() {
461        controller_data
462    } else {
463        Vec::new()
464    };
465
466    Ok(ControllerReadResult {
467        controllers,
468        orphan_data,
469    })
470}
471
472// ---------------------------------------------------------------------------
473// Animation reader
474// ---------------------------------------------------------------------------
475
476/// Reads all animations from the animation offset array.
477fn read_animations(
478    bytes: &[u8],
479    anim_arr_ptr: u32,
480    anim_arr_count: u32,
481    names: &[String],
482) -> Result<Vec<MdlAnimation>, MdlError> {
483    if anim_arr_count == 0 {
484        return Ok(Vec::new());
485    }
486
487    let arr_offset = checked_to_usize(anim_arr_ptr, "anim_arr_ptr")?;
488    let anim_count = checked_to_usize(anim_arr_count, "anim_count")?;
489    if arr_offset + (anim_count * 4) > bytes.len() {
490        return Err(MdlError::InvalidData(format!(
491            "animation offset array at {arr_offset} with {anim_arr_count} entries exceeds content"
492        )));
493    }
494
495    // Read the array of content-relative offsets to animation headers.
496    let mut anim_offsets = Vec::with_capacity(anim_count);
497    for i in 0..anim_count {
498        let off = checked_to_usize(read_u32(bytes, arr_offset + i * 4)?, "anim_header_offset")?;
499        anim_offsets.push(off);
500    }
501
502    let mut animations = Vec::with_capacity(anim_offsets.len());
503    for &offset in &anim_offsets {
504        animations.push(read_animation(bytes, offset, names)?);
505    }
506    Ok(animations)
507}
508
509/// Reads a single animation from its header offset.
510fn read_animation(bytes: &[u8], offset: usize, names: &[String]) -> Result<MdlAnimation, MdlError> {
511    check_slice_in_bounds(bytes, offset, ANIMATION_HEADER_SIZE, "animation_header")?;
512
513    let fn_ptr1 = read_u32(bytes, offset + anim_header_offsets::FN_PTR1)?;
514    let fn_ptr2 = read_u32(bytes, offset + anim_header_offsets::FN_PTR2)?;
515
516    // Animation name (32-byte null-terminated).
517    let name = read_fixed_string(
518        bytes,
519        offset + anim_header_offsets::NAME,
520        anim_header_offsets::NAME_SIZE,
521    );
522
523    let root_node_ptr = checked_to_usize(
524        read_u32(bytes, offset + anim_header_offsets::ROOT_NODE_PTR)?,
525        "anim_root_node_ptr",
526    )?;
527
528    // length and transition_time
529    let length = read_f32(bytes, offset + anim_header_offsets::LENGTH)?;
530    let transition_time = read_f32(bytes, offset + anim_header_offsets::TRANSITION)?;
531
532    // Animation root name (32-byte null-terminated).
533    let anim_root = read_fixed_string(
534        bytes,
535        offset + anim_header_offsets::ANIM_ROOT,
536        anim_header_offsets::ANIM_ROOT_SIZE,
537    );
538
539    // Event array
540    let event_arr_ptr = read_u32(bytes, offset + anim_header_offsets::EVENT_ARR_PTR)?;
541    let event_arr_count = read_u32(bytes, offset + anim_header_offsets::EVENT_ARR_COUNT)?;
542
543    let events = read_anim_events(bytes, event_arr_ptr, event_arr_count)?;
544
545    // Parse animation node tree recursively.
546    let root_node = read_anim_node(bytes, root_node_ptr, names)?;
547
548    Ok(MdlAnimation {
549        name,
550        length,
551        transition_time,
552        anim_root,
553        events,
554        root_node,
555        fn_ptr1,
556        fn_ptr2,
557    })
558}
559
560/// Reads animation events from the event array.
561fn read_anim_events(
562    bytes: &[u8],
563    event_arr_ptr: u32,
564    event_arr_count: u32,
565) -> Result<Vec<MdlAnimEvent>, MdlError> {
566    if event_arr_count == 0 {
567        return Ok(Vec::new());
568    }
569
570    let arr_offset = checked_to_usize(event_arr_ptr, "event_arr_ptr")?;
571    let event_count = checked_to_usize(event_arr_count, "event_count")?;
572    let total_size = event_count * ANIMATION_EVENT_SIZE;
573    if arr_offset + total_size > bytes.len() {
574        return Err(MdlError::InvalidData(format!(
575            "event array at {arr_offset} with {event_arr_count} events exceeds content"
576        )));
577    }
578
579    let mut events = Vec::with_capacity(event_count);
580    for i in 0..event_count {
581        let event_offset = arr_offset + i * ANIMATION_EVENT_SIZE;
582        let time = read_f32(bytes, event_offset)?;
583        let name = read_fixed_string(bytes, event_offset + 4, 32);
584        events.push(MdlAnimEvent { time, name });
585    }
586    Ok(events)
587}
588
589/// Reads a single animation node and its children recursively.
590///
591/// Animation nodes use the same 80-byte base header layout as geometry nodes
592/// but carry no type-specific extra data. The `node_number` field maps back
593/// to the corresponding geometry node.
594fn read_anim_node(bytes: &[u8], offset: usize, names: &[String]) -> Result<MdlAnimNode, MdlError> {
595    check_slice_in_bounds(bytes, offset, NODE_HEADER_SIZE, "anim_node_header")?;
596
597    // +0x02: node_number - maps this animation node to its geometry counterpart.
598    let node_number = read_u16(bytes, offset + 0x02)?;
599
600    // +0x04: name_index - index into the model's name table.
601    let name_index = usize::from(read_u16(bytes, offset + node_offsets::NAME_INDEX)?);
602    let name = if name_index < names.len() {
603        names[name_index].clone()
604    } else {
605        format!("AnimNode_{}", name_index)
606    };
607
608    // Children array.
609    let child_offset_ptr = checked_to_usize(
610        read_u32(bytes, offset + node_offsets::CHILD_ARRAY_PTR)?,
611        "anim_child_offset_ptr",
612    )?;
613    let child_count = checked_to_usize(
614        read_u32(bytes, offset + node_offsets::CHILD_COUNT)?,
615        "anim_child_count",
616    )?;
617
618    // Controller arrays.
619    let ctrl_key_ptr = checked_to_usize(
620        read_u32(bytes, offset + node_offsets::CONTROLLER_KEY_PTR)?,
621        "anim_ctrl_key_ptr",
622    )?;
623    let ctrl_key_count = checked_to_usize(
624        read_u32(bytes, offset + node_offsets::CONTROLLER_KEY_COUNT)?,
625        "anim_ctrl_key_count",
626    )?;
627    let ctrl_data_ptr = checked_to_usize(
628        read_u32(bytes, offset + node_offsets::CONTROLLER_DATA_PTR)?,
629        "anim_ctrl_data_ptr",
630    )?;
631    let ctrl_data_count = checked_to_usize(
632        read_u32(bytes, offset + node_offsets::CONTROLLER_DATA_COUNT)?,
633        "anim_ctrl_data_count",
634    )?;
635
636    // Parse controllers (reusing same infrastructure as geometry nodes).
637    let ctrl_result = read_controllers(
638        bytes,
639        ctrl_key_ptr,
640        ctrl_key_count,
641        ctrl_data_ptr,
642        ctrl_data_count,
643    )?;
644
645    // Parse children recursively.
646    let mut children = Vec::with_capacity(child_count);
647    if child_count > 0 && child_offset_ptr > 0 && child_offset_ptr + child_count * 4 <= bytes.len()
648    {
649        for i in 0..child_count {
650            let child_ptr =
651                checked_to_usize(read_u32(bytes, child_offset_ptr + i * 4)?, "anim_child_ptr")?;
652            children.push(read_anim_node(bytes, child_ptr, names)?);
653        }
654    }
655
656    Ok(MdlAnimNode {
657        name,
658        node_number,
659        controllers: ctrl_result.controllers,
660        orphan_controller_data: ctrl_result.orphan_data,
661        children,
662    })
663}
664
665// ---------------------------------------------------------------------------
666// Geometry node reader
667// ---------------------------------------------------------------------------
668
669fn read_node(
670    bytes: &[u8],
671    offset: usize,
672    names: &[String],
673    parent_node_id: Option<u16>,
674    node_end_hint: Option<usize>,
675    mdx_infos: &mut Vec<MdxMeshInfo>,
676    dfs_mesh_counter: &mut usize,
677) -> Result<MdlNode, MdlError> {
678    // Basic Node Header check (0x44 = 68 bytes)
679    check_slice_in_bounds(bytes, offset, NODE_HEADER_SIZE, "node_header")?;
680
681    let flags = u32::from(read_u16(bytes, offset + node_offsets::FLAGS)?);
682
683    // Preserve unknown bytes for roundtrip fidelity (reserved field rule).
684    let mut header_padding_02 = [0u8; 2];
685    header_padding_02.copy_from_slice(&bytes[offset + 0x02..offset + 0x04]);
686
687    let name_index = usize::from(read_u16(bytes, offset + node_offsets::NAME_INDEX)?);
688
689    let mut header_padding_06 = [0u8; 2];
690    header_padding_06.copy_from_slice(&bytes[offset + 0x06..offset + 0x08]);
691
692    let px = read_f32(bytes, offset + node_offsets::POS_X)?;
693    let py = read_f32(bytes, offset + node_offsets::POS_X + 4)?;
694    let pz = read_f32(bytes, offset + node_offsets::POS_X + 8)?;
695
696    // Orientation quaternion (w, x, y, z) - Ghidra-verified field order
697    let rw = read_f32(bytes, offset + node_offsets::ORIENTATION_W)?;
698    let rx = read_f32(bytes, offset + node_offsets::ORIENTATION_W + 4)?;
699    let ry = read_f32(bytes, offset + node_offsets::ORIENTATION_W + 8)?;
700    let rz = read_f32(bytes, offset + node_offsets::ORIENTATION_W + 12)?;
701
702    let child_offset_ptr = checked_to_usize(
703        read_u32(bytes, offset + node_offsets::CHILD_ARRAY_PTR)?,
704        "child_offset",
705    )?;
706    let child_count = checked_to_usize(
707        read_u32(bytes, offset + node_offsets::CHILD_COUNT)?,
708        "child_count",
709    )?;
710
711    let controller_key_ptr = checked_to_usize(
712        read_u32(bytes, offset + node_offsets::CONTROLLER_KEY_PTR)?,
713        "controller_key_ptr",
714    )?;
715    let controller_key_count = checked_to_usize(
716        read_u32(bytes, offset + node_offsets::CONTROLLER_KEY_COUNT)?,
717        "controller_key_count",
718    )?;
719
720    let controller_data_ptr = checked_to_usize(
721        read_u32(bytes, offset + node_offsets::CONTROLLER_DATA_PTR)?,
722        "controller_data_ptr",
723    )?;
724    let controller_data_count = checked_to_usize(
725        read_u32(bytes, offset + node_offsets::CONTROLLER_DATA_COUNT)?,
726        "controller_data_count",
727    )?;
728
729    let name = if name_index < names.len() {
730        names[name_index].clone()
731    } else {
732        format!("Node_{}", name_index)
733    };
734
735    // Read controllers using shared helper.
736    let ctrl_result = read_controllers(
737        bytes,
738        controller_key_ptr,
739        controller_key_count,
740        controller_data_ptr,
741        controller_data_count,
742    )?;
743
744    // Sequential Header Parsing
745    // Standard packing order: Light -> Emitter -> Camera -> Reference -> Mesh -> Skin
746    // WARNING: This assumes fixed sizes which is brittle. Real parser needs exact struct sizes.
747    let mut current_offset = offset + NODE_HEADER_SIZE; // End of Node Header
748
749    if (flags & node_flags::LIGHT) != 0 {
750        current_offset += LIGHT_EXTRA_SIZE;
751    }
752    if (flags & node_flags::EMITTER) != 0 {
753        current_offset += EMITTER_EXTRA_SIZE;
754    }
755    // Camera: 0 extra bytes beyond the base node header (Ghidra-verified,
756    // see mdl_mdx.md: Non-Mesh Node Type Structs).
757    // No offset adjustment needed.
758    if (flags & node_flags::REFERENCE) != 0 {
759        current_offset += REFERENCE_EXTRA_SIZE;
760    }
761
762    // Determine node_data variant from flags.
763    // Priority: check mesh subtypes first (most specific), then non-mesh types,
764    // then plain mesh, then base.
765    let node_data = if (flags & node_flags::MESH) != 0 {
766        *dfs_mesh_counter += 1;
767
768        let mut node_data_end_hint = node_end_hint.unwrap_or(bytes.len());
769        for ptr in [child_offset_ptr, controller_key_ptr, controller_data_ptr] {
770            if ptr > 0 {
771                node_data_end_hint = node_data_end_hint.min(ptr);
772            }
773        }
774
775        let (mesh, info) = read_mesh_header(bytes, current_offset)?;
776        mdx_infos.push(info);
777
778        if (flags & node_flags::SABER) != 0 {
779            let saber = read_saber_extra(bytes, current_offset + MESH_EXTRA_SIZE, mesh)?;
780            MdlNodeData::Saber(saber)
781        } else if (flags & node_flags::AABB) != 0 {
782            let aabb = read_aabb_extra(
783                bytes,
784                current_offset + MESH_EXTRA_SIZE,
785                node_data_end_hint,
786                mesh,
787            )?;
788            MdlNodeData::Aabb(aabb)
789        } else if (flags & node_flags::DANGLY) != 0 {
790            let dangly = read_dangly_extra(bytes, current_offset + MESH_EXTRA_SIZE, mesh)?;
791            MdlNodeData::Dangly(dangly)
792        } else if (flags & node_flags::ANIM) != 0 {
793            let anim = read_anim_mesh_extra(bytes, current_offset + MESH_EXTRA_SIZE, mesh)?;
794            MdlNodeData::AnimMesh(anim)
795        } else if (flags & node_flags::SKIN) != 0 {
796            let skin = read_skin_extra(bytes, current_offset + MESH_EXTRA_SIZE, mesh)?;
797            // Propagate bone weight/index offsets into the MdxMeshInfo so the
798            // MDX vertex reader can extract typed bone data.
799            if let Some(last_info) = mdx_infos.last_mut() {
800                last_info.bone_weights_off = skin.mdx_bone_weights_offset;
801                last_info.bone_indices_off = skin.mdx_bone_indices_offset;
802            }
803            MdlNodeData::Skin(skin)
804        } else {
805            MdlNodeData::Mesh(mesh)
806        }
807    } else if (flags & node_flags::LIGHT) != 0 {
808        MdlNodeData::Light(read_light_header(bytes, offset + NODE_HEADER_SIZE)?)
809    } else if (flags & node_flags::EMITTER) != 0 {
810        MdlNodeData::Emitter(read_emitter_header(bytes, offset + NODE_HEADER_SIZE)?)
811    } else if (flags & node_flags::CAMERA) != 0 {
812        MdlNodeData::Camera(MdlCamera::new())
813    } else if (flags & node_flags::REFERENCE) != 0 {
814        MdlNodeData::Reference(read_reference_header(bytes, offset + NODE_HEADER_SIZE)?)
815    } else {
816        MdlNodeData::Base
817    };
818
819    let mut children = Vec::with_capacity(child_count);
820    if child_count > 0 && child_offset_ptr > 0 {
821        let mut child_ptrs = Vec::with_capacity(child_count);
822        for i in 0..child_count {
823            let ptr_loc = child_offset_ptr + (i * 4);
824            let child_ptr = checked_to_usize(read_u32(bytes, ptr_loc)?, "child_ptr")?;
825            child_ptrs.push(child_ptr);
826        }
827
828        for (i, child_ptr) in child_ptrs.iter().copied().enumerate() {
829            let mut child_end_hint: Option<usize> = None;
830            let mut consider_end = |candidate: Option<usize>| {
831                if let Some(end) = candidate.filter(|end| *end > child_ptr) {
832                    child_end_hint = Some(child_end_hint.map_or(end, |curr| curr.min(end)));
833                }
834            };
835            consider_end(child_ptrs.get(i + 1).copied());
836            consider_end((controller_key_ptr > 0).then_some(controller_key_ptr));
837            consider_end((controller_data_ptr > 0).then_some(controller_data_ptr));
838            consider_end(node_end_hint);
839
840            let child_node = read_node(
841                bytes,
842                child_ptr,
843                names,
844                u16::try_from(name_index).ok(),
845                child_end_hint,
846                mdx_infos,
847                dfs_mesh_counter,
848            )?;
849            children.push(child_node);
850        }
851    }
852
853    Ok(MdlNode {
854        name,
855        parent_index: parent_node_id,
856        children,
857        position: [px, py, pz],
858        rotation: [rw, rx, ry, rz],
859        node_data,
860        controllers: ctrl_result.controllers,
861        orphan_controller_data: ctrl_result.orphan_data,
862        header_padding_02,
863        header_padding_06,
864    })
865}
866
867// ---------------------------------------------------------------------------
868// Mesh header (Pass 1 - no MDX reading)
869// ---------------------------------------------------------------------------
870
871fn read_mesh_header(bytes: &[u8], offset: usize) -> Result<(MdlMesh, MdxMeshInfo), MdlError> {
872    use super::types::MdlFace;
873
874    // Full mesh extra header is 332 bytes (MdlNodeTriMesh 0x50..0x19C).
875    check_slice_in_bounds(bytes, offset, MESH_EXTRA_SIZE, "mesh_header")?;
876
877    let face_offset = checked_to_usize(
878        read_u32(bytes, offset + mesh_offsets::FACE_ARRAY_OFFSET)?,
879        "face_offset",
880    )?;
881    let face_count = checked_to_usize(
882        read_u32(bytes, offset + mesh_offsets::FACE_COUNT)?,
883        "face_count",
884    )?;
885
886    // +0x98: vertex_indices - dead field, skipped.
887    // +0xA4: left_over_faces - always empty, skipped.
888
889    let vertex_indices_count_ptr = checked_to_usize(
890        read_u32(bytes, offset + mesh_offsets::VERTEX_INDICES_COUNT_ARRAY_PTR)?,
891        "vertex_indices_count_ptr",
892    )?;
893    let vertex_indices_count_count = read_u32(
894        bytes,
895        offset + mesh_offsets::VERTEX_INDICES_COUNT_ARRAY_COUNT,
896    )?;
897    let _vertex_indices_count_alloc = read_u32(
898        bytes,
899        offset + mesh_offsets::VERTEX_INDICES_COUNT_ARRAY_ALLOC,
900    )?;
901
902    // +0xBC: mdx_offsets - data derived from faces, only count/ptr used for reading.
903    let _mdx_offsets_count = read_u32(bytes, offset + mesh_offsets::MDX_OFFSETS_ARRAY_COUNT)?;
904    let _mdx_offsets_alloc = read_u32(bytes, offset + mesh_offsets::MDX_OFFSETS_ARRAY_ALLOC)?;
905
906    let index_buffer_pools_ptr = checked_to_usize(
907        read_u32(bytes, offset + mesh_offsets::INDEX_BUFFER_POOLS_ARRAY_PTR)?,
908        "index_buffer_pools_ptr",
909    )?;
910    let index_buffer_pools_count =
911        read_u32(bytes, offset + mesh_offsets::INDEX_BUFFER_POOLS_ARRAY_COUNT)?;
912    let _index_buffer_pools_alloc =
913        read_u32(bytes, offset + mesh_offsets::INDEX_BUFFER_POOLS_ARRAY_ALLOC)?;
914
915    let shared_index_offset = read_i32(bytes, offset + mesh_offsets::SHARED_INDEX_OFFSET)?;
916    let shared_index_pool = read_i32(bytes, offset + mesh_offsets::SHARED_INDEX_POOL)?;
917    let shared_index_size = read_i32(bytes, offset + mesh_offsets::SHARED_INDEX_SIZE)?;
918    let indices_per_face = read_u32(bytes, offset + mesh_offsets::INDICES_PER_FACE)?;
919
920    // Vertex info from Ghidra-verified offsets.
921    let vertex_count = usize::from(read_u16(bytes, offset + mesh_offsets::VERTEX_COUNT)?);
922    let mdx_data_offset = checked_to_usize(
923        read_u32(bytes, offset + mesh_offsets::MDX_DATA_OFFSET)?,
924        "mdx_data_offset",
925    )?;
926    let vert_array_offset = checked_to_usize(
927        read_u32(bytes, offset + mesh_offsets::VERT_ARRAY_OFFSET)?,
928        "vert_array_offset",
929    )?;
930    let vertex_stride = read_u32(bytes, offset + mesh_offsets::VERTEX_STRUCT_SIZE)?;
931
932    // Render/shadow flags from Ghidra-verified offsets.
933    let render = read_u8(bytes, offset + mesh_offsets::RENDER)? != 0;
934    let shadow = read_u8(bytes, offset + mesh_offsets::SHADOW)? != 0;
935
936    // Toolset function pointer stubs (extra +0x00, +0x04).
937    let fn_ptr_gen_vertices = read_u32(bytes, offset + mesh_offsets::FN_PTR_GEN_VERTICES)?;
938    let fn_ptr_remove_temp_array =
939        read_u32(bytes, offset + mesh_offsets::FN_PTR_REMOVE_TEMP_ARRAY)?;
940
941    // Bounding box and sphere.
942    let bounding_min = [
943        read_f32(bytes, offset + mesh_offsets::BOUNDING_MIN)?,
944        read_f32(bytes, offset + mesh_offsets::BOUNDING_MIN + 4)?,
945        read_f32(bytes, offset + mesh_offsets::BOUNDING_MIN + 8)?,
946    ];
947    let bounding_max = [
948        read_f32(bytes, offset + mesh_offsets::BOUNDING_MAX)?,
949        read_f32(bytes, offset + mesh_offsets::BOUNDING_MAX + 4)?,
950        read_f32(bytes, offset + mesh_offsets::BOUNDING_MAX + 8)?,
951    ];
952    let bsphere_radius = read_f32(bytes, offset + mesh_offsets::BSPHERE_RADIUS)?;
953    let bsphere_center = [
954        read_f32(bytes, offset + mesh_offsets::BSPHERE_CENTER)?,
955        read_f32(bytes, offset + mesh_offsets::BSPHERE_CENTER + 4)?,
956        read_f32(bytes, offset + mesh_offsets::BSPHERE_CENTER + 8)?,
957    ];
958
959    // Colors.
960    let diffuse_color = [
961        read_f32(bytes, offset + mesh_offsets::DIFFUSE_COLOR)?,
962        read_f32(bytes, offset + mesh_offsets::DIFFUSE_COLOR + 4)?,
963        read_f32(bytes, offset + mesh_offsets::DIFFUSE_COLOR + 8)?,
964    ];
965    let ambient_color = [
966        read_f32(bytes, offset + mesh_offsets::AMBIENT_COLOR)?,
967        read_f32(bytes, offset + mesh_offsets::AMBIENT_COLOR + 4)?,
968        read_f32(bytes, offset + mesh_offsets::AMBIENT_COLOR + 8)?,
969    ];
970    let transparency_hint = read_i32(bytes, offset + mesh_offsets::TRANSPARENCY_HINT)?;
971
972    // Texture names (null-terminated char[32]).
973    let texture_0 = read_fixed_string(
974        bytes,
975        offset + mesh_offsets::TEXTURE_0,
976        mesh_offsets::TEXTURE_NAME_SIZE,
977    );
978    let texture_1 = read_fixed_string(
979        bytes,
980        offset + mesh_offsets::TEXTURE_1,
981        mesh_offsets::TEXTURE_NAME_SIZE,
982    );
983
984    // UV animation.
985    let animate_uv = read_i32(bytes, offset + mesh_offsets::ANIMATE_UV)?;
986    let uv_direction_x = read_f32(bytes, offset + mesh_offsets::UV_DIRECTION_X)?;
987    let uv_direction_y = read_f32(bytes, offset + mesh_offsets::UV_DIRECTION_Y)?;
988    let uv_jitter = read_f32(bytes, offset + mesh_offsets::UV_JITTER)?;
989    let uv_jitter_speed = read_f32(bytes, offset + mesh_offsets::UV_JITTER_SPEED)?;
990
991    // Remaining scalar/boolean fields.
992    let texture_channel_count = read_u16(bytes, offset + mesh_offsets::TEXTURE_CHANNEL_COUNT)?;
993    let light_mapped = read_u8(bytes, offset + mesh_offsets::LIGHT_MAPPED)? != 0;
994    let rotate_texture = read_u8(bytes, offset + mesh_offsets::ROTATE_TEXTURE)? != 0;
995    let is_background_geometry =
996        read_u8(bytes, offset + mesh_offsets::IS_BACKGROUND_GEOMETRY)? != 0;
997    let beaming = read_u8(bytes, offset + mesh_offsets::BEAMING)? != 0;
998    let total_surface_area = read_f32(bytes, offset + mesh_offsets::TOTAL_SURFACE_AREA)?;
999
1000    // Read Faces - MaxFace is 32 bytes per entry.
1001    let mut faces = Vec::with_capacity(face_count);
1002    if face_count > 0 && face_offset > 0 {
1003        if face_offset + (face_count * MAX_FACE_SIZE) <= bytes.len() {
1004            for i in 0..face_count {
1005                let curr = face_offset + (i * MAX_FACE_SIZE);
1006                faces.push(MdlFace {
1007                    plane_normal: [
1008                        read_f32(bytes, curr)?,
1009                        read_f32(bytes, curr + 4)?,
1010                        read_f32(bytes, curr + 8)?,
1011                    ],
1012                    plane_distance: read_f32(bytes, curr + 12)?,
1013                    surface_id: read_u32(bytes, curr + 16)?,
1014                    adjacent: [
1015                        read_u16(bytes, curr + 20)?,
1016                        read_u16(bytes, curr + 22)?,
1017                        read_u16(bytes, curr + 24)?,
1018                    ],
1019                    vertex_indices: [
1020                        read_u16(bytes, curr + 26)?,
1021                        read_u16(bytes, curr + 28)?,
1022                        read_u16(bytes, curr + 30)?,
1023                    ],
1024                });
1025            }
1026        } else {
1027            crate::trace_warn!(
1028                face_offset,
1029                face_count,
1030                bytes_len = bytes.len(),
1031                "face array extends beyond buffer, skipping faces"
1032            );
1033        }
1034    }
1035
1036    // --- TriMesh internal CExoArrayList typed extraction ---
1037    // These 5 fields are now stored as typed values rather than raw blobs.
1038    // See `docs/src/formats/models/mesh_derived_fields.md` for full documentation.
1039
1040    // +0x98: Dead field (always zeros in KotOR). Skipped - writer emits zeros.
1041
1042    // +0xC8: Inverted counter (single u32 data value).
1043    let inverted_counter = if index_buffer_pools_count > 0 && index_buffer_pools_ptr > 0 {
1044        if index_buffer_pools_ptr + 4 <= bytes.len() {
1045            read_u32(bytes, index_buffer_pools_ptr)?
1046        } else {
1047            0
1048        }
1049    } else {
1050        0
1051    };
1052
1053    // +0xB0: Detect embedded-position variant (count==1 with positions at ptr+4).
1054    let has_embedded_positions = vertex_indices_count_count == 1
1055        && vertex_indices_count_ptr > 0
1056        && vert_array_offset == vertex_indices_count_ptr.saturating_add(4);
1057
1058    // Read per-attribute byte offsets from mesh header (+0x104..+0x120).
1059    // Each is an i32; value -1 (0xFFFFFFFF) means not present.
1060    let pos_off = read_i32(bytes, offset + mesh_offsets::MDX_POSITION_OFFSET)?;
1061    let norm_off = read_i32(bytes, offset + mesh_offsets::MDX_NORMAL_OFFSET)?;
1062    let color_off = read_i32(bytes, offset + mesh_offsets::MDX_COLOR_OFFSET)?;
1063    let uv1_off = read_i32(bytes, offset + mesh_offsets::MDX_UV1_OFFSET)?;
1064    let uv2_off = read_i32(bytes, offset + mesh_offsets::MDX_UV2_OFFSET)?;
1065    let uv3_off = read_i32(bytes, offset + mesh_offsets::MDX_UV3_OFFSET)?;
1066    let uv4_off = read_i32(bytes, offset + mesh_offsets::MDX_UV4_OFFSET)?;
1067    let tangent_off = read_i32(bytes, offset + mesh_offsets::MDX_TANGENT_SPACE_OFFSET)?;
1068
1069    let info = MdxMeshInfo {
1070        stride: vertex_stride,
1071        vertex_count,
1072        mdx_data_offset,
1073        vert_array_offset,
1074        pos_off,
1075        norm_off,
1076        color_off,
1077        uv1_off,
1078        uv2_off,
1079        uv3_off,
1080        uv4_off,
1081        tangent_off,
1082        bone_weights_off: -1,
1083        bone_indices_off: -1,
1084    };
1085
1086    let mesh = MdlMesh {
1087        fn_ptr_gen_vertices,
1088        fn_ptr_remove_temp_array,
1089        bounding_min,
1090        bounding_max,
1091        bsphere_radius,
1092        bsphere_center,
1093        diffuse_color,
1094        ambient_color,
1095        transparency_hint,
1096        texture_0,
1097        texture_1,
1098        animate_uv,
1099        uv_direction_x,
1100        uv_direction_y,
1101        uv_jitter,
1102        uv_jitter_speed,
1103        texture_channel_count,
1104        light_mapped,
1105        rotate_texture,
1106        is_background_geometry,
1107        beaming,
1108        total_surface_area,
1109        positions: Vec::new(),
1110        normals: Vec::new(),
1111        vertex_colors: Vec::new(),
1112        uv1: Vec::new(),
1113        uv2: Vec::new(),
1114        uv3: Vec::new(),
1115        uv4: Vec::new(),
1116        tangent_space: Vec::new(),
1117        faces,
1118        inverted_counter,
1119        has_embedded_positions,
1120        shared_index_offset,
1121        shared_index_pool,
1122        shared_index_size,
1123        indices_per_face,
1124        vertex_count: u16::try_from(vertex_count)
1125            .map_err(|_| MdlError::ValueOverflow("vertex_count"))?,
1126        render,
1127        shadow,
1128    };
1129
1130    Ok((mesh, info))
1131}
1132
1133// ---------------------------------------------------------------------------
1134// Pass 2: MDX vertex data population (per-mesh seeking via mdx_data_offset)
1135// ---------------------------------------------------------------------------
1136
1137/// Reads MDX vertex data using each mesh's `mdx_data_offset` (+0x144) to seek
1138/// directly to the correct position in the MDX buffer.
1139///
1140/// This matches how the engine and community tools (kotorblender, mdledit)
1141/// consume MDX data -- each mesh's header stores the byte offset where its
1142/// interleaved vertex data begins in the MDX file, so the reader does not
1143/// need to assume any particular mesh ordering within the MDX buffer.
1144fn populate_mdx_data(
1145    root: &mut MdlNode,
1146    mdx: &[u8],
1147    infos: &[MdxMeshInfo],
1148) -> Result<(), MdlError> {
1149    if infos.is_empty() {
1150        return Ok(());
1151    }
1152
1153    // Read vertex data for each mesh by seeking to its mdx_data_offset.
1154    let mut results: Vec<MdxVertexData> =
1155        (0..infos.len()).map(|_| MdxVertexData::default()).collect();
1156
1157    for (idx, info) in infos.iter().enumerate() {
1158        let data = read_mdx_vertices(mdx, info)?;
1159        results[idx] = data;
1160    }
1161
1162    // Apply vertex data to the tree in DFS order.
1163    let mut counter = 0;
1164    apply_vertex_data(root, &mut results, &mut counter);
1165
1166    Ok(())
1167}
1168
1169/// Reads all vertex attributes for a single mesh from the MDX buffer,
1170/// seeking directly to the mesh's `mdx_data_offset` position.
1171fn read_mdx_vertices(mdx: &[u8], info: &MdxMeshInfo) -> Result<MdxVertexData, MdlError> {
1172    let mut data = MdxVertexData::default();
1173
1174    if info.vertex_count == 0 || info.stride == 0 {
1175        return Ok(data);
1176    }
1177
1178    let stride = checked_to_usize(info.stride, "vertex_stride")?;
1179    let mdx_base = info.mdx_data_offset;
1180
1181    for i in 0..info.vertex_count {
1182        let base = mdx_base + (i * stride);
1183        if base + stride > mdx.len() {
1184            crate::trace_warn!(
1185                vertex_index = i,
1186                base,
1187                stride,
1188                mdx_len = mdx.len(),
1189                mdx_cursor = mdx_base,
1190                "MDX vertex data truncated"
1191            );
1192            break;
1193        }
1194
1195        // Position (3×f32 = 12 bytes)
1196        if let Ok(off) = usize::try_from(info.pos_off) {
1197            let o = base + off;
1198            data.positions.push([
1199                read_f32(mdx, o)?,
1200                read_f32(mdx, o + 4)?,
1201                read_f32(mdx, o + 8)?,
1202            ]);
1203        }
1204
1205        // Normal (3×f32 = 12 bytes)
1206        if let Ok(off) = usize::try_from(info.norm_off) {
1207            let o = base + off;
1208            data.normals.push([
1209                read_f32(mdx, o)?,
1210                read_f32(mdx, o + 4)?,
1211                read_f32(mdx, o + 8)?,
1212            ]);
1213        }
1214
1215        // Vertex color (4×u8 = 4 bytes)
1216        if let Ok(off) = usize::try_from(info.color_off) {
1217            let o = base + off;
1218            data.vertex_colors.push([
1219                read_u8(mdx, o)?,
1220                read_u8(mdx, o + 1)?,
1221                read_u8(mdx, o + 2)?,
1222                read_u8(mdx, o + 3)?,
1223            ]);
1224        }
1225
1226        // UV1 (2×f32 = 8 bytes)
1227        if let Ok(off) = usize::try_from(info.uv1_off) {
1228            let o = base + off;
1229            data.uv1.push([read_f32(mdx, o)?, read_f32(mdx, o + 4)?]);
1230        }
1231
1232        // UV2 (2×f32 = 8 bytes)
1233        if let Ok(off) = usize::try_from(info.uv2_off) {
1234            let o = base + off;
1235            data.uv2.push([read_f32(mdx, o)?, read_f32(mdx, o + 4)?]);
1236        }
1237
1238        // UV3 (2×f32 = 8 bytes)
1239        if let Ok(off) = usize::try_from(info.uv3_off) {
1240            let o = base + off;
1241            data.uv3.push([read_f32(mdx, o)?, read_f32(mdx, o + 4)?]);
1242        }
1243
1244        // UV4 (2×f32 = 8 bytes)
1245        if let Ok(off) = usize::try_from(info.uv4_off) {
1246            let o = base + off;
1247            data.uv4.push([read_f32(mdx, o)?, read_f32(mdx, o + 4)?]);
1248        }
1249
1250        // Tangent space (3×3×f32 = 36 bytes)
1251        if let Ok(off) = usize::try_from(info.tangent_off) {
1252            let o = base + off;
1253            data.tangent_space.push([
1254                [
1255                    read_f32(mdx, o)?,
1256                    read_f32(mdx, o + 4)?,
1257                    read_f32(mdx, o + 8)?,
1258                ],
1259                [
1260                    read_f32(mdx, o + 12)?,
1261                    read_f32(mdx, o + 16)?,
1262                    read_f32(mdx, o + 20)?,
1263                ],
1264                [
1265                    read_f32(mdx, o + 24)?,
1266                    read_f32(mdx, o + 28)?,
1267                    read_f32(mdx, o + 32)?,
1268                ],
1269            ]);
1270        }
1271
1272        // Bone weights (4×f32 = 16 bytes)
1273        if let Ok(off) = usize::try_from(info.bone_weights_off) {
1274            let o = base + off;
1275            data.bone_weights.push([
1276                read_f32(mdx, o)?,
1277                read_f32(mdx, o + 4)?,
1278                read_f32(mdx, o + 8)?,
1279                read_f32(mdx, o + 12)?,
1280            ]);
1281        }
1282
1283        // Bone indices (4×f32 = 16 bytes)
1284        if let Ok(off) = usize::try_from(info.bone_indices_off) {
1285            let o = base + off;
1286            data.bone_indices.push([
1287                read_f32(mdx, o)?,
1288                read_f32(mdx, o + 4)?,
1289                read_f32(mdx, o + 8)?,
1290                read_f32(mdx, o + 12)?,
1291            ]);
1292        }
1293    }
1294
1295    // Clamp vertex_count to actual vertex data length. Vanilla item models
1296    // may declare more vertices than the MDX buffer contains (shared buffer
1297    // truncation). The typed vertex_count must match the actual data arrays
1298    // for roundtrip fidelity - the writer uses vertex_count to size the MDX
1299    // output region, and over-declaring causes cross-contamination between
1300    // meshes on re-read.
1301    let actual_vertex_count = data
1302        .positions
1303        .len()
1304        .max(data.normals.len())
1305        .max(data.vertex_colors.len())
1306        .max(data.uv1.len())
1307        .max(data.uv2.len())
1308        .max(data.uv3.len())
1309        .max(data.uv4.len())
1310        .max(data.tangent_space.len());
1311
1312    if actual_vertex_count > 0 && actual_vertex_count < info.vertex_count {
1313        data.clamped_vertex_count = Some(
1314            u16::try_from(actual_vertex_count)
1315                .map_err(|_| MdlError::ValueOverflow("actual_vertex_count"))?,
1316        );
1317    }
1318
1319    Ok(data)
1320}
1321
1322/// Reads position-only vertex data from the MDL content blob (no-MDX fallback).
1323///
1324/// Each mesh's `vert_array_offset` (+0x148) is a content-relative offset into
1325/// the MDL blob pointing to position-only data (12 bytes per vertex = 3x f32).
1326/// This path is used when no MDX file is available. Ordering doesn't matter
1327/// here since each mesh reads from its own independent content offset.
1328fn populate_content_positions(
1329    root: &mut MdlNode,
1330    bytes: &[u8],
1331    infos: &[MdxMeshInfo],
1332) -> Result<(), MdlError> {
1333    if infos.is_empty() {
1334        return Ok(());
1335    }
1336
1337    // Read position-only data for each mesh from its stored MDL content offset.
1338    let mut results: Vec<MdxVertexData> =
1339        (0..infos.len()).map(|_| MdxVertexData::default()).collect();
1340
1341    for (idx, info) in infos.iter().enumerate() {
1342        if info.vertex_count == 0 || info.vert_array_offset == 0 {
1343            continue;
1344        }
1345
1346        let pos_end = info.vert_array_offset + (info.vertex_count * 12);
1347        if pos_end <= bytes.len() {
1348            let mut positions = Vec::with_capacity(info.vertex_count);
1349            for i in 0..info.vertex_count {
1350                let o = info.vert_array_offset + (i * 12);
1351                positions.push([
1352                    read_f32(bytes, o)?,
1353                    read_f32(bytes, o + 4)?,
1354                    read_f32(bytes, o + 8)?,
1355                ]);
1356            }
1357            results[idx].positions = positions;
1358        } else {
1359            crate::trace_warn!(
1360                vert_array_offset = info.vert_array_offset,
1361                vertex_count = info.vertex_count,
1362                pos_end,
1363                bytes_len = bytes.len(),
1364                "MDL content position data extends beyond buffer"
1365            );
1366        }
1367    }
1368
1369    // Apply to tree in DFS order.
1370    let mut counter = 0;
1371    apply_vertex_data(root, &mut results, &mut counter);
1372
1373    Ok(())
1374}
1375
1376/// Reads content positions for mesh nodes that the MDX pass skipped.
1377///
1378/// After `populate_mdx_data`, mesh nodes with stride=0 (e.g., saber nodes)
1379/// have empty position arrays because the MDX path returns no data for them.
1380/// However, these nodes may still have valid embedded positions at their
1381/// `vert_array_offset` (+0x148). This function fills in positions only for
1382/// nodes where the MDX pass left positions empty.
1383fn populate_content_positions_fallback(
1384    root: &mut MdlNode,
1385    bytes: &[u8],
1386    infos: &[MdxMeshInfo],
1387) -> Result<(), MdlError> {
1388    if infos.is_empty() {
1389        return Ok(());
1390    }
1391
1392    let mut counter = 0;
1393    fill_missing_positions(root, bytes, infos, &mut counter)?;
1394    Ok(())
1395}
1396
1397/// Recursive helper: walks DFS and reads content positions for mesh nodes
1398/// that still have empty positions but a valid vert_array_offset.
1399fn fill_missing_positions(
1400    node: &mut MdlNode,
1401    bytes: &[u8],
1402    infos: &[MdxMeshInfo],
1403    counter: &mut usize,
1404) -> Result<(), MdlError> {
1405    if node.node_data.mesh().is_some() {
1406        let idx = *counter;
1407        *counter += 1;
1408
1409        if let (Some(info), Some(mesh)) = (infos.get(idx), node.node_data.mesh_mut()) {
1410            if mesh.positions.is_empty() && info.vertex_count > 0 && info.vert_array_offset > 0 {
1411                let pos_end = info.vert_array_offset + (info.vertex_count * 12);
1412                if pos_end <= bytes.len() {
1413                    let mut positions = Vec::with_capacity(info.vertex_count);
1414                    for i in 0..info.vertex_count {
1415                        let o = info.vert_array_offset + (i * 12);
1416                        positions.push([
1417                            read_f32(bytes, o)?,
1418                            read_f32(bytes, o + 4)?,
1419                            read_f32(bytes, o + 8)?,
1420                        ]);
1421                    }
1422                    mesh.positions = positions;
1423                } else {
1424                    crate::trace_warn!(
1425                        vert_array_offset = info.vert_array_offset,
1426                        vertex_count = info.vertex_count,
1427                        pos_end,
1428                        bytes_len = bytes.len(),
1429                        "fallback content position data extends beyond buffer"
1430                    );
1431                }
1432            }
1433        }
1434    }
1435
1436    for child in &mut node.children {
1437        fill_missing_positions(child, bytes, infos, counter)?;
1438    }
1439
1440    Ok(())
1441}
1442
1443/// Walks the node tree in DFS order, applying vertex data from `results`
1444/// to each mesh node. The results are indexed by DFS mesh order (matching
1445/// the order meshes were encountered during Pass 1).
1446fn apply_vertex_data(node: &mut MdlNode, results: &mut Vec<MdxVertexData>, counter: &mut usize) {
1447    if node.node_data.mesh().is_some() {
1448        let idx = *counter;
1449        *counter += 1;
1450
1451        if idx < results.len() {
1452            let mut data = std::mem::take(&mut results[idx]);
1453            let bone_weights = std::mem::take(&mut data.bone_weights);
1454            let bone_indices = std::mem::take(&mut data.bone_indices);
1455
1456            if let Some(mesh) = node.node_data.mesh_mut() {
1457                mesh.positions = data.positions;
1458                mesh.normals = data.normals;
1459                mesh.vertex_colors = data.vertex_colors;
1460                mesh.uv1 = data.uv1;
1461                mesh.uv2 = data.uv2;
1462                mesh.uv3 = data.uv3;
1463                mesh.uv4 = data.uv4;
1464                mesh.tangent_space = data.tangent_space;
1465                if let Some(clamped) = data.clamped_vertex_count {
1466                    mesh.vertex_count = clamped;
1467                }
1468            }
1469
1470            // Move bone weight/index data to the skin wrapper (only skins
1471            // carry these fields).
1472            if let MdlNodeData::Skin(skin) = &mut node.node_data {
1473                skin.bone_weights = bone_weights;
1474                skin.bone_indices = bone_indices;
1475            }
1476        }
1477    }
1478
1479    for child in &mut node.children {
1480        apply_vertex_data(child, results, counter);
1481    }
1482}
1483
1484// ---------------------------------------------------------------------------
1485// Non-mesh node type readers (unchanged)
1486// ---------------------------------------------------------------------------
1487
1488/// Reads a fixed-size null-terminated string from the byte buffer.
1489fn read_fixed_string(bytes: &[u8], offset: usize, max_len: usize) -> String {
1490    crate::binary::read_fixed_c_string(bytes, offset, max_len)
1491}
1492
1493/// Reads a variable-length null-terminated string from the byte buffer.
1494fn read_cstring(bytes: &[u8], offset: usize) -> String {
1495    crate::binary::read_c_string(bytes, offset)
1496}
1497
1498/// Reads a CExoArrayList (ptr/count pair) with bounds checking and OOB warning.
1499///
1500/// Extracts the content-relative pointer and element count from the given field
1501/// offsets, performs a bounds check, and delegates to `reader(bytes, ptr, count)`.
1502/// Returns an empty vec if count is zero, ptr is null, or data is out of bounds.
1503fn read_cexo_array<T>(
1504    bytes: &[u8],
1505    offset: usize,
1506    ptr_field: usize,
1507    count_field: usize,
1508    element_size: usize,
1509    label: &'static str,
1510    reader: impl FnOnce(&[u8], usize, usize) -> Result<Vec<T>, MdlError>,
1511) -> Result<Vec<T>, MdlError> {
1512    let ptr = checked_to_usize(read_u32(bytes, offset + ptr_field)?, label)?;
1513    let count = checked_to_usize(read_u32(bytes, offset + count_field)?, label)?;
1514
1515    if count == 0 || ptr == 0 {
1516        return Ok(Vec::new());
1517    }
1518
1519    let byte_size = count * element_size;
1520    if ptr + byte_size > bytes.len() {
1521        crate::trace_warn!(
1522            ptr,
1523            count,
1524            bytes_len = bytes.len(),
1525            label,
1526            "CExoArrayList extends beyond buffer"
1527        );
1528        return Ok(Vec::new());
1529    }
1530
1531    reader(bytes, ptr, count)
1532}
1533
1534/// Reads `count` sequential `f32` values starting at `ptr`.
1535fn read_f32_array(bytes: &[u8], ptr: usize, count: usize) -> Result<Vec<f32>, MdlError> {
1536    let mut result = Vec::with_capacity(count);
1537    for i in 0..count {
1538        result.push(read_f32(bytes, ptr + i * 4)?);
1539    }
1540    Ok(result)
1541}
1542
1543/// Reads `count` sequential `[f32; 3]` vectors (12 bytes each) starting at `ptr`.
1544fn read_vec3_array(bytes: &[u8], ptr: usize, count: usize) -> Result<Vec<[f32; 3]>, MdlError> {
1545    let mut result = Vec::with_capacity(count);
1546    for i in 0..count {
1547        let base = ptr + i * 12;
1548        result.push([
1549            read_f32(bytes, base)?,
1550            read_f32(bytes, base + 4)?,
1551            read_f32(bytes, base + 8)?,
1552        ]);
1553    }
1554    Ok(result)
1555}
1556
1557/// Reads `count` sequential `[f32; 4]` quaternions (16 bytes each) starting at `ptr`.
1558fn read_quat_array(bytes: &[u8], ptr: usize, count: usize) -> Result<Vec<[f32; 4]>, MdlError> {
1559    let mut result = Vec::with_capacity(count);
1560    for i in 0..count {
1561        let base = ptr + i * 16;
1562        result.push([
1563            read_f32(bytes, base)?,
1564            read_f32(bytes, base + 4)?,
1565            read_f32(bytes, base + 8)?,
1566            read_f32(bytes, base + 12)?,
1567        ]);
1568    }
1569    Ok(result)
1570}
1571
1572/// Reads a Reference node header (36 extra bytes after the base node header).
1573///
1574/// Layout: `ref_model` (char[32]) + `reattachable` (i32).
1575/// See `docs/src/internals/mdl_deep_dive.md` "Node types in depth".
1576fn read_reference_header(bytes: &[u8], offset: usize) -> Result<MdlReference, MdlError> {
1577    check_slice_in_bounds(bytes, offset, REFERENCE_EXTRA_SIZE, "reference_header")?;
1578
1579    let ref_model = read_fixed_string(bytes, offset, 32);
1580    let reattachable = read_i32(bytes, offset + 0x20)?;
1581
1582    Ok(MdlReference {
1583        ref_model,
1584        reattachable,
1585    })
1586}
1587
1588/// Reads a Light node header (92 extra bytes after the base node header).
1589///
1590/// Scalar fields are parsed into typed fields. Array headers (5 × 12 bytes)
1591/// are preserved as raw bytes since they contain file-relative pointers.
1592/// See `docs/src/internals/mdl_deep_dive.md` "Node types in depth".
1593/// Reads Light extension fields (92 extra bytes after the base node header).
1594///
1595/// Parses scalar fields and follows CExoArrayList pointers for flare data:
1596/// sizes, positions, color shifts, and texture names.
1597///
1598/// See `docs/src/internals/mdl_deep_dive.md` "Node types in depth".
1599fn read_light_header(bytes: &[u8], offset: usize) -> Result<MdlLight, MdlError> {
1600    check_slice_in_bounds(bytes, offset, LIGHT_EXTRA_SIZE, "light_header")?;
1601
1602    let flare_radius = read_f32(bytes, offset + light_offsets::FLARE_RADIUS)?;
1603
1604    // Texture SafePointers (runtime-only, 3×u32 at +0x04).
1605    let sp_base = offset + light_offsets::TEXTURE_SAFE_PTRS_PTR;
1606    let texture_safe_ptrs = [
1607        read_u32(bytes, sp_base)?,
1608        read_u32(bytes, sp_base + 4)?,
1609        read_u32(bytes, sp_base + 8)?,
1610    ];
1611
1612    // Flare sizes: CExoArrayList<float> at +0x10
1613    let flare_sizes = read_cexo_array(
1614        bytes,
1615        offset,
1616        light_offsets::FLARE_SIZES_PTR,
1617        light_offsets::FLARE_SIZES_COUNT,
1618        4,
1619        "flare_sizes",
1620        read_f32_array,
1621    )?;
1622
1623    // Flare positions: CExoArrayList<float> at +0x1C
1624    let flare_positions = read_cexo_array(
1625        bytes,
1626        offset,
1627        light_offsets::FLARE_POSITIONS_PTR,
1628        light_offsets::FLARE_POSITIONS_COUNT,
1629        4,
1630        "flare_positions",
1631        read_f32_array,
1632    )?;
1633
1634    // Flare color shifts: CExoArrayList<Vector> at +0x28
1635    let flare_color_shifts = read_cexo_array(
1636        bytes,
1637        offset,
1638        light_offsets::FLARE_COLOR_SHIFTS_PTR,
1639        light_offsets::FLARE_COLOR_SHIFTS_COUNT,
1640        12,
1641        "flare_color_shifts",
1642        read_vec3_array,
1643    )?;
1644
1645    // Flare texture names: CExoArrayList<char*> at +0x34
1646    // Each entry is a u32 content-relative offset to a null-terminated string.
1647    let flare_texture_names = read_cexo_array(
1648        bytes,
1649        offset,
1650        light_offsets::FLARE_TEX_NAMES_PTR,
1651        light_offsets::FLARE_TEX_NAMES_COUNT,
1652        4,
1653        "flare_tex_names",
1654        |b, ptr, count| {
1655            let mut names = Vec::with_capacity(count);
1656            for i in 0..count {
1657                let str_offset =
1658                    checked_to_usize(read_u32(b, ptr + i * 4)?, "flare_tex_name_str_ptr")?;
1659                if str_offset > 0 && str_offset < b.len() {
1660                    names.push(read_cstring(b, str_offset));
1661                } else {
1662                    names.push(String::new());
1663                }
1664            }
1665            Ok(names)
1666        },
1667    )?;
1668
1669    let priority = read_i32(bytes, offset + light_offsets::PRIORITY)?;
1670    let num_dynamic_types = read_i32(bytes, offset + light_offsets::NUM_DYNAMIC_TYPES)?;
1671    let affectdynamic = read_i32(bytes, offset + light_offsets::AFFECTDYNAMIC)?;
1672    let shadow = read_i32(bytes, offset + light_offsets::SHADOW)?;
1673    let ambientonly = read_i32(bytes, offset + light_offsets::AMBIENTONLY)?;
1674    let generateflare = read_i32(bytes, offset + light_offsets::GENERATEFLARE)?;
1675    let fading_light = read_i32(bytes, offset + light_offsets::FADING_LIGHT)?;
1676
1677    Ok(MdlLight {
1678        flare_radius,
1679        texture_safe_ptrs,
1680        flare_sizes,
1681        flare_positions,
1682        flare_color_shifts,
1683        flare_texture_names,
1684        priority,
1685        num_dynamic_types,
1686        affectdynamic,
1687        shadow,
1688        ambientonly,
1689        generateflare,
1690        fading_light,
1691    })
1692}
1693
1694/// Reads an Emitter node header (224 extra bytes after the base node header).
1695///
1696/// All fields are inline fixed-size data - no pointer relocation needed.
1697/// See `docs/src/internals/mdl_deep_dive.md` "Node types in depth".
1698fn read_emitter_header(bytes: &[u8], offset: usize) -> Result<super::types::MdlEmitter, MdlError> {
1699    check_slice_in_bounds(bytes, offset, EMITTER_EXTRA_SIZE, "emitter_header")?;
1700
1701    let deadspace = read_f32(bytes, offset)?;
1702    let blast_radius = read_f32(bytes, offset + 0x04)?;
1703    let blast_length = read_f32(bytes, offset + 0x08)?;
1704    let num_branches = read_i32(bytes, offset + 0x0C)?;
1705    let control_pt_smoothing = read_i32(bytes, offset + 0x10)?;
1706    let x_grid = read_i32(bytes, offset + 0x14)?;
1707    let y_grid = read_i32(bytes, offset + 0x18)?;
1708    let spawn_type = read_i32(bytes, offset + 0x1C)?;
1709
1710    let update = read_fixed_string(bytes, offset + 0x20, 32);
1711    let render = read_fixed_string(bytes, offset + 0x40, 32);
1712    let blend = read_fixed_string(bytes, offset + 0x60, 32);
1713    let texture = read_fixed_string(bytes, offset + 0x80, 32);
1714    let chunk_name = read_fixed_string(bytes, offset + 0xA0, 16);
1715
1716    let two_sided_tex = read_i32(bytes, offset + 0xB0)?;
1717    let loop_emitter = read_i32(bytes, offset + 0xB4)?;
1718    let render_order = read_u16(bytes, offset + 0xB8)?;
1719    let frame_blending = read_u8(bytes, offset + 0xBA)? != 0;
1720    let depth_texture_name = read_fixed_string(bytes, offset + 0xBB, 16);
1721
1722    // +0xCB..+0xE0: 21 bytes reserved/padding - read verbatim
1723    let mut reserved = [0u8; 21];
1724    reserved.copy_from_slice(&bytes[offset + 0xCB..offset + 0xE0]);
1725
1726    Ok(super::types::MdlEmitter {
1727        deadspace,
1728        blast_radius,
1729        blast_length,
1730        num_branches,
1731        control_pt_smoothing,
1732        x_grid,
1733        y_grid,
1734        spawn_type,
1735        update,
1736        render,
1737        blend,
1738        texture,
1739        chunk_name,
1740        two_sided_tex,
1741        loop_emitter,
1742        render_order,
1743        frame_blending,
1744        depth_texture_name,
1745        reserved,
1746    })
1747}
1748
1749/// Reads DanglyMesh extension fields (28 extra bytes after the TriMesh header).
1750///
1751/// Parses the constraint array (per-vertex floats), three inline physics
1752/// parameters, and the per-vertex dangly position array.
1753///
1754/// The data pointer at +0x18 references `vertex_count` vec3 positions in the
1755/// MDL content blob. At runtime, `PartDanglyMesh` (`0x00447980`) copies these
1756/// into a GL vertex pool for the dangly physics simulation.
1757///
1758/// See `docs/src/internals/mdl_deep_dive.md` "Node types in depth".
1759fn read_dangly_extra(bytes: &[u8], offset: usize, mesh: MdlMesh) -> Result<MdlDangly, MdlError> {
1760    check_slice_in_bounds(bytes, offset, DANGLY_EXTRA_SIZE, "dangly_extra")?;
1761
1762    // CExoArrayList<float> at +0x00: per-vertex constraint weights.
1763    let constraints = read_cexo_array(
1764        bytes,
1765        offset,
1766        dangly_offsets::CONSTRAINTS_PTR,
1767        dangly_offsets::CONSTRAINTS_COUNT,
1768        4,
1769        "dangly_constraints",
1770        read_f32_array,
1771    )?;
1772
1773    let displacement = read_f32(bytes, offset + dangly_offsets::DISPLACEMENT)?;
1774    let tightness = read_f32(bytes, offset + dangly_offsets::TIGHTNESS)?;
1775    let period = read_f32(bytes, offset + dangly_offsets::PERIOD)?;
1776
1777    // Conditional data pointer at +0x18: vertex_count × vec3 positions.
1778    // Relocated against MDL content base when vertex_count > 0 (ResetDangly).
1779    let dangly_verts_ptr = checked_to_usize(
1780        read_u32(bytes, offset + dangly_offsets::DATA_PTR)?,
1781        "dangly_verts_ptr",
1782    )?;
1783
1784    // Read per-vertex dangly positions (vertex_count × vec3, 12 bytes each).
1785    let vert_count = usize::from(mesh.vertex_count);
1786    let mut dangly_vertices = Vec::with_capacity(vert_count);
1787    if vert_count > 0 && dangly_verts_ptr > 0 {
1788        let required = vert_count * 12;
1789        if dangly_verts_ptr + required <= bytes.len() {
1790            dangly_vertices = read_vec3_array(bytes, dangly_verts_ptr, vert_count)?;
1791        } else {
1792            crate::trace_warn!(
1793                dangly_verts_ptr,
1794                vert_count,
1795                bytes_len = bytes.len(),
1796                "dangly vertices array extends beyond buffer"
1797            );
1798        }
1799    }
1800
1801    Ok(MdlDangly {
1802        mesh,
1803        constraints,
1804        displacement,
1805        tightness,
1806        period,
1807        dangly_vertices,
1808    })
1809}
1810
1811/// Reads Skin extension fields (100 extra bytes after the TriMesh header).
1812///
1813/// Parses the weights CExoArrayList, MDX bone weight/index offsets, bonemap,
1814/// three CExoArrayList arrays (qbone, tbone, bone_constant_indices), and the
1815/// fixed-size bone_node_numbers array.
1816///
1817/// All fields are fully typed - no raw blob is preserved.
1818///
1819/// See `docs/src/internals/mdl_deep_dive.md` "Node types in depth".
1820fn read_skin_extra(bytes: &[u8], offset: usize, mesh: MdlMesh) -> Result<MdlSkin, MdlError> {
1821    check_slice_in_bounds(bytes, offset, SKIN_EXTRA_SIZE, "skin_extra")?;
1822
1823    // Weights CExoArrayList header at +0x00: ptr, count, alloc (12 bytes).
1824    // Always zeros in vanilla binary files - engine uses MDX bone data instead.
1825    // SkinVertexWeight (52 bytes/element) is only populated by the ASCII parser.
1826    // Weights CExoArrayList at +0x00: always zeros in binary - writer emits zeros.
1827
1828    // MDX per-vertex bone weight/index offsets at +0x0C/+0x10.
1829    let mdx_bone_weights_offset = read_i32(bytes, offset + skin_offsets::MDX_BONE_WEIGHTS_OFFSET)?;
1830    let mdx_bone_indices_offset = read_i32(bytes, offset + skin_offsets::MDX_BONE_INDICES_OFFSET)?;
1831
1832    // Bonemap pointer + count at +0x14/+0x18.
1833    let bonemap_ptr = checked_to_usize(
1834        read_u32(bytes, offset + skin_offsets::BONEMAP_PTR)?,
1835        "skin_bonemap_ptr",
1836    )?;
1837    let bonemap_count = read_u32(bytes, offset + skin_offsets::BONEMAP_COUNT)?;
1838
1839    let mut bonemap = Vec::new();
1840    if bonemap_count > 0 && bonemap_ptr > 0 {
1841        let entry_count = checked_to_usize(bonemap_count, "bonemap_count")?;
1842        let byte_len = entry_count.checked_mul(4).unwrap_or(0);
1843        if byte_len > 0 && bonemap_ptr + byte_len <= bytes.len() {
1844            bonemap.reserve(entry_count);
1845            for i in 0..entry_count {
1846                bonemap.push(read_u32(bytes, bonemap_ptr + i * 4)?);
1847            }
1848        } else {
1849            crate::trace_warn!(
1850                bonemap_ptr,
1851                bonemap_count,
1852                bytes_len = bytes.len(),
1853                "skin bonemap extends beyond buffer"
1854            );
1855        }
1856    }
1857
1858    // CExoArrayList<Quaternion> at +0x1C: inverse bind rotations
1859    let qbone_ref_inv = read_cexo_array(
1860        bytes,
1861        offset,
1862        skin_offsets::QBONE_REF_INV_PTR,
1863        skin_offsets::QBONE_REF_INV_COUNT,
1864        16,
1865        "skin_qbone_ref_inv",
1866        read_quat_array,
1867    )?;
1868
1869    // CExoArrayList<Vector> at +0x28: inverse bind translations
1870    let tbone_ref_inv = read_cexo_array(
1871        bytes,
1872        offset,
1873        skin_offsets::TBONE_REF_INV_PTR,
1874        skin_offsets::TBONE_REF_INV_COUNT,
1875        12,
1876        "skin_tbone_ref_inv",
1877        read_vec3_array,
1878    )?;
1879
1880    // CExoArrayList<int> at +0x34: bone constant indices
1881    let bone_constant_indices = read_cexo_array(
1882        bytes,
1883        offset,
1884        skin_offsets::BONE_CONSTANT_INDICES_PTR,
1885        skin_offsets::BONE_CONSTANT_INDICES_COUNT,
1886        4,
1887        "skin_bone_constant_indices",
1888        |b, ptr, count| {
1889            let mut indices = Vec::with_capacity(count);
1890            for i in 0..count {
1891                indices.push(read_i32(b, ptr + (i * 4))?);
1892            }
1893            Ok(indices)
1894        },
1895    )?;
1896
1897    // bone_node_numbers: 16 × u16 at +0x40.
1898    let mut bone_node_numbers = [0u16; 16];
1899    let bnn_offset = offset + skin_offsets::BONE_NODE_NUMBERS;
1900    for (i, slot) in bone_node_numbers.iter_mut().enumerate() {
1901        *slot = read_u16(bytes, bnn_offset + i * 2)?;
1902    }
1903
1904    // +0x60..+0x63: Padding (leaked runtime pointers in ~74 vanilla models).
1905    // Writer emits zeros - this is garbage data the engine doesn't read.
1906
1907    Ok(MdlSkin {
1908        mesh,
1909        mdx_bone_weights_offset,
1910        mdx_bone_indices_offset,
1911        bone_weights: Vec::new(),
1912        bone_indices: Vec::new(),
1913        bonemap,
1914        qbone_ref_inv,
1915        tbone_ref_inv,
1916        bone_constant_indices,
1917        bone_node_numbers,
1918    })
1919}
1920
1921/// Reads AnimMesh extension fields (56 extra bytes after the TriMesh header).
1922///
1923/// Parses one inline scalar (`sample_period`), two CExoArrayList arrays,
1924/// and six runtime-only fields:
1925/// - `anim_verts`: animated vertex positions (Vector = 3 × f32 each)
1926/// - `anim_t_verts`: animated texture coordinates (Vector = 3 × f32 each)
1927/// - Runtime fields at +0x1C..+0x37: always zero in authored files, preserved
1928///   for roundtrip fidelity.
1929///
1930/// See `docs/src/internals/mdl_deep_dive.md` "Node types in depth".
1931fn read_anim_mesh_extra(
1932    bytes: &[u8],
1933    offset: usize,
1934    mesh: MdlMesh,
1935) -> Result<MdlAnimMesh, MdlError> {
1936    check_slice_in_bounds(bytes, offset, ANIM_MESH_EXTRA_SIZE, "anim_mesh_extra")?;
1937
1938    let sample_period = read_f32(bytes, offset + anim_mesh_offsets::SAMPLE_PERIOD)?;
1939
1940    // CExoArrayList<Vector> at +0x04: animated vertex positions
1941    let anim_verts = read_cexo_array(
1942        bytes,
1943        offset,
1944        anim_mesh_offsets::ANIM_VERTS_PTR,
1945        anim_mesh_offsets::ANIM_VERTS_COUNT,
1946        12,
1947        "anim_verts",
1948        read_vec3_array,
1949    )?;
1950
1951    // CExoArrayList<Vector> at +0x10: animated texture coordinates
1952    let anim_t_verts = read_cexo_array(
1953        bytes,
1954        offset,
1955        anim_mesh_offsets::ANIM_T_VERTS_PTR,
1956        anim_mesh_offsets::ANIM_T_VERTS_COUNT,
1957        12,
1958        "anim_t_verts",
1959        read_vec3_array,
1960    )?;
1961
1962    // Runtime-only fields at +0x1C..+0x37 (no ASCII parser names).
1963    // Always zero in authored files; preserved for roundtrip fidelity.
1964    let data_ptr_1 = read_u32(bytes, offset + anim_mesh_offsets::DATA_PTR_1)?;
1965    let data_count_1 = read_u32(bytes, offset + anim_mesh_offsets::DATA_COUNT_1)?;
1966    let padding_24 = read_u32(bytes, offset + anim_mesh_offsets::PADDING_24)?;
1967    let anim_vertices_ptr = read_u32(bytes, offset + anim_mesh_offsets::ANIM_VERTICES_PTR)?;
1968    let anim_tex_vertices_ptr = read_u32(bytes, offset + anim_mesh_offsets::ANIM_TEX_VERTICES_PTR)?;
1969    let anim_vertices_count_val = read_u32(bytes, offset + anim_mesh_offsets::ANIM_VERTICES_COUNT)?;
1970    let anim_tex_vertices_count =
1971        read_u32(bytes, offset + anim_mesh_offsets::ANIM_TEX_VERTICES_COUNT)?;
1972
1973    Ok(MdlAnimMesh {
1974        mesh,
1975        sample_period,
1976        anim_verts,
1977        anim_t_verts,
1978        data_ptr_1,
1979        data_count_1,
1980        padding_24,
1981        anim_vertices_ptr,
1982        anim_tex_vertices_ptr,
1983        anim_vertices_count: anim_vertices_count_val,
1984        anim_tex_vertices_count,
1985    })
1986}
1987
1988/// Reads AABB extension fields (4 extra bytes after the TriMesh header).
1989///
1990/// The 4-byte extra header contains a single pointer to the AABB binary
1991/// search tree root. The tree is parsed recursively by following child
1992/// pointers - each node is 40 bytes containing bounding box, child
1993/// pointers, face index, and split direction flags.
1994///
1995/// See `docs/src/internals/mdl_deep_dive.md` "AABB walkmesh".
1996fn read_aabb_extra(
1997    bytes: &[u8],
1998    offset: usize,
1999    _node_data_end_hint: usize,
2000    mesh: MdlMesh,
2001) -> Result<MdlAabb, MdlError> {
2002    check_slice_in_bounds(bytes, offset, AABB_EXTRA_SIZE, "aabb_extra")?;
2003
2004    let aabb_tree_ptr = checked_to_usize(
2005        read_u32(bytes, offset + aabb_offsets::TREE_PTR)?,
2006        "aabb_tree_ptr",
2007    )?;
2008
2009    let aabb_tree = if aabb_tree_ptr > 0 {
2010        Some(Box::new(read_aabb_tree(bytes, aabb_tree_ptr)?))
2011    } else {
2012        None
2013    };
2014
2015    Ok(MdlAabb { mesh, aabb_tree })
2016}
2017
2018/// Recursively reads an AABB binary search tree node and its children.
2019///
2020/// Each node is 40 bytes on disk. Child pointers are content-blob-relative
2021/// offsets (0 = no child / leaf). The tree is followed depth-first.
2022///
2023/// See `docs/src/internals/mdl_deep_dive.md` "AABB walkmesh".
2024fn read_aabb_tree(bytes: &[u8], ptr: usize) -> Result<AabbNode, MdlError> {
2025    const AABB_NODE_SIZE: usize = 40;
2026
2027    check_slice_in_bounds(bytes, ptr, AABB_NODE_SIZE, "aabb_node")?;
2028
2029    let box_min = [
2030        read_f32(bytes, ptr)?,
2031        read_f32(bytes, ptr + 4)?,
2032        read_f32(bytes, ptr + 8)?,
2033    ];
2034    let box_max = [
2035        read_f32(bytes, ptr + 12)?,
2036        read_f32(bytes, ptr + 16)?,
2037        read_f32(bytes, ptr + 20)?,
2038    ];
2039    // Note: right_child at +0x18, left_child at +0x1C (Ghidra struct order).
2040    let right_ptr = checked_to_usize(read_u32(bytes, ptr + 24)?, "aabb_right_child")?;
2041    let left_ptr = checked_to_usize(read_u32(bytes, ptr + 28)?, "aabb_left_child")?;
2042    let face_index = read_i32(bytes, ptr + 32)?;
2043    let split_direction_flags = read_u32(bytes, ptr + 36)?;
2044
2045    let left = if left_ptr > 0 {
2046        Some(Box::new(read_aabb_tree(bytes, left_ptr)?))
2047    } else {
2048        None
2049    };
2050    let right = if right_ptr > 0 {
2051        Some(Box::new(read_aabb_tree(bytes, right_ptr)?))
2052    } else {
2053        None
2054    };
2055
2056    Ok(AabbNode {
2057        box_min,
2058        box_max,
2059        face_index,
2060        split_direction_flags,
2061        left,
2062        right,
2063    })
2064}
2065
2066/// Reads Saber extension fields (20 extra bytes after the TriMesh header).
2067///
2068/// The 20-byte extra header contains 3 relocated data pointers and 2 runtime
2069/// GL pool IDs. All fields are preserved as raw bytes - the data pointers
2070/// contain file-relative offsets and the GL pool IDs are runtime-only values.
2071///
2072/// See `docs/src/internals/mdl_deep_dive.md` "Node types in depth".
2073/// Reads Saber extension fields (20 extra bytes after the TriMesh header).
2074///
2075/// Structurally parses the three saber vertex arrays (positions, UVs, normals)
2076/// at known fixed sizes (176 vertices each), plus two runtime GL pool IDs.
2077///
2078/// kotorblender names these `off_saber_verts`, `off_saber_uv` and
2079/// `off_saber_normals`. Noted for cross-reference; the identifiers below are
2080/// this crate's own.
2081///
2082/// See `docs/src/internals/mdl_deep_dive.md` "Node types in depth".
2083fn read_saber_extra(bytes: &[u8], offset: usize, mesh: MdlMesh) -> Result<MdlSaber, MdlError> {
2084    check_slice_in_bounds(bytes, offset, SABER_EXTRA_SIZE, "saber_extra")?;
2085
2086    let verts_ptr = checked_to_usize(
2087        read_u32(bytes, offset + saber_offsets::VERTS_PTR)?,
2088        "saber_verts_ptr",
2089    )?;
2090    let uvs_ptr = checked_to_usize(
2091        read_u32(bytes, offset + saber_offsets::UVS_PTR)?,
2092        "saber_uvs_ptr",
2093    )?;
2094    let normals_ptr = checked_to_usize(
2095        read_u32(bytes, offset + saber_offsets::NORMALS_PTR)?,
2096        "saber_normals_ptr",
2097    )?;
2098    let gl_pool_vert = read_u32(bytes, offset + saber_offsets::GL_POOL_VERT)?;
2099    let gl_pool_index = read_u32(bytes, offset + saber_offsets::GL_POOL_INDEX)?;
2100
2101    // Positions: NUM_SABER_VERTS × vec3 (12 bytes each)
2102    let mut saber_verts = Vec::with_capacity(NUM_SABER_VERTS);
2103    if verts_ptr > 0 {
2104        let byte_size = NUM_SABER_VERTS * 12;
2105        if verts_ptr + byte_size <= bytes.len() {
2106            saber_verts = read_vec3_array(bytes, verts_ptr, NUM_SABER_VERTS)?;
2107        } else {
2108            crate::trace_warn!(
2109                verts_ptr,
2110                byte_size,
2111                bytes_len = bytes.len(),
2112                "saber_verts array extends beyond buffer"
2113            );
2114        }
2115    }
2116
2117    // UVs: NUM_SABER_VERTS × vec2 (8 bytes each)
2118    let mut saber_uvs = Vec::with_capacity(NUM_SABER_VERTS);
2119    if uvs_ptr > 0 {
2120        let byte_size = NUM_SABER_VERTS * 8;
2121        if uvs_ptr + byte_size <= bytes.len() {
2122            for i in 0..NUM_SABER_VERTS {
2123                let base = uvs_ptr + i * 8;
2124                saber_uvs.push([read_f32(bytes, base)?, read_f32(bytes, base + 4)?]);
2125            }
2126        } else {
2127            crate::trace_warn!(
2128                uvs_ptr,
2129                byte_size,
2130                bytes_len = bytes.len(),
2131                "saber_uvs array extends beyond buffer"
2132            );
2133        }
2134    }
2135
2136    // Normals: NUM_SABER_VERTS × vec3 (12 bytes each)
2137    let mut saber_normals = Vec::with_capacity(NUM_SABER_VERTS);
2138    if normals_ptr > 0 {
2139        let byte_size = NUM_SABER_VERTS * 12;
2140        if normals_ptr + byte_size <= bytes.len() {
2141            saber_normals = read_vec3_array(bytes, normals_ptr, NUM_SABER_VERTS)?;
2142        } else {
2143            crate::trace_warn!(
2144                normals_ptr,
2145                byte_size,
2146                bytes_len = bytes.len(),
2147                "saber_normals array extends beyond buffer"
2148            );
2149        }
2150    }
2151
2152    Ok(MdlSaber {
2153        mesh,
2154        saber_verts,
2155        saber_uvs,
2156        saber_normals,
2157        gl_pool_vert,
2158        gl_pool_index,
2159    })
2160}