Skip to main content

rakata_formats/mdl/
writer.rs

1//! MDL binary writer.
2//!
3//! Serializes an [`Mdl`] struct back into the binary MDL+MDX format used by
4//! the Odyssey engine. The writer uses a single-pass cursor approach: it
5//! reserves placeholder space for headers and arrays, writes child data
6//! forward, then backpatches pointers and counts using seek-back helpers.
7//!
8//! MDX vertex data is written separately via [`write_mdl_with_mdx_to_vec`] or
9//! deferred to a second buffer. Non-skin meshes are emitted first (DFS order),
10//! then skin meshes (DFS order), matching the vanilla engine layout.
11
12use std::collections::HashMap;
13use std::io::{Cursor, Write};
14
15use crate::binary::{write_f32, write_i32, write_u16, write_u32};
16
17use super::controllers::MdlController;
18use super::types::MdlNodeData;
19use super::{
20    aabb_offsets, anim_mesh_offsets, dangly_offsets, header_offsets, light_offsets, mesh_offsets,
21    node_offsets, saber_offsets, skin_offsets, Mdl, MdlError, MdlNode, AABB_EXTRA_SIZE,
22    ANIM_MESH_EXTRA_SIZE, DANGLY_EXTRA_SIZE, LIGHT_EXTRA_SIZE, MDL_WRAPPER_SIZE, MESH_EXTRA_SIZE,
23    NODE_HEADER_SIZE, SABER_EXTRA_SIZE, SKIN_EXTRA_SIZE,
24};
25
26/// Mesh reference collected during DFS traversal for deferred MDX writing.
27struct MeshRef<'a> {
28    /// The base mesh data.
29    mesh: &'a super::MdlMesh,
30    /// Skin wrapper (if this is a skin node), carrying bone weight/index data.
31    skin: Option<&'a super::MdlSkin>,
32    /// Whether this node is a skin mesh.
33    is_skin: bool,
34}
35
36/// Writes an MDL file structure to a writer.
37///
38/// # Errors
39///
40/// [`MdlError::ValueOverflow`] when a node count, offset or array length will
41/// not fit its on-disk width, and [`MdlError::InvalidData`] when the node tree
42/// does not describe a writable model. [`MdlError::Io`] when the writer fails,
43/// which can leave a partial file.
44#[cfg_attr(
45    feature = "tracing",
46    tracing::instrument(level = "debug", skip(writer, mdl))
47)]
48pub fn write_mdl<W: Write>(writer: &mut W, mdl: &Mdl) -> Result<(), MdlError> {
49    let bytes = write_mdl_to_vec(mdl)?;
50    crate::trace_debug!(bytes_len = bytes.len(), "wrote mdl to writer");
51    writer.write_all(&bytes)?;
52    Ok(())
53}
54
55/// Writes an MDL file structure to a byte vector.
56///
57/// # Errors
58///
59/// Every non-I/O failure [`write_mdl`] describes. The `Vec` target has no I/O
60/// to fail at.
61#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(mdl)))]
62pub fn write_mdl_to_vec(mdl: &Mdl) -> Result<Vec<u8>, MdlError> {
63    let writer = MdlWriter::new();
64    let (bytes, _) = writer.write(mdl)?;
65    crate::trace_debug!(bytes_len = bytes.len(), "serialized mdl to vec");
66    Ok(bytes)
67}
68
69/// Writes an MDL model and its companion MDX vertex data to byte vectors.
70///
71/// Unlike [`write_mdl_to_vec`], this function computes the MDX vertex buffer
72/// layout (stride, flags, per-attribute offsets) from the mesh vertex data and
73/// writes interleaved vertex attributes into a separate MDX buffer.
74///
75/// # Errors
76///
77/// Everything [`write_mdl_to_vec`] reports, plus the MDX layout's own
78/// [`MdlError::ValueOverflow`] and [`MdlError::InvalidData`]: a mesh declaring
79/// more vertices than it has attribute data for is the case vanilla K1 item
80/// models actually hit.
81#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(mdl)))]
82pub fn write_mdl_with_mdx_to_vec(mdl: &Mdl) -> Result<super::MdlWriteResult, MdlError> {
83    let writer = MdlWriter::new_with_mdx();
84    let (mdl_bytes, mdx_bytes) = writer.write(mdl)?;
85    crate::trace_debug!(
86        mdl_bytes_len = mdl_bytes.len(),
87        mdx_bytes_len = mdx_bytes.len(),
88        "serialized mdl+mdx to vec"
89    );
90    Ok(super::MdlWriteResult {
91        mdl_bytes,
92        mdx_bytes,
93    })
94}
95
96/// Checked narrowing cast from `usize` to `u32`.
97fn count_u32(len: usize, field: &'static str) -> Result<u32, MdlError> {
98    u32::try_from(len).map_err(|_| MdlError::ValueOverflow(field))
99}
100
101/// Checked narrowing cast from `usize` to `u16`.
102fn count_u16(len: usize, field: &'static str) -> Result<u16, MdlError> {
103    u16::try_from(len).map_err(|_| MdlError::ValueOverflow(field))
104}
105
106/// Entry for deferred MDX writing. Collected during the node DFS traversal,
107/// then flushed in non-skin-first/skin-second order to match the BioWare
108/// engine's canonical MDX layout.
109struct DeferredMdxEntry {
110    /// Position of the mesh extra header in the MDL buffer (for backpatching).
111    mesh_header_pos: u64,
112    /// Position of the skin extra header (for backpatching bone offsets), if skin.
113    skin_header_pos: Option<u64>,
114    /// Whether this mesh belongs to a Skin node.
115    is_skin: bool,
116    /// DFS encounter index (for stable sort within each group).
117    dfs_index: usize,
118}
119
120struct MdlWriter {
121    buffer: Cursor<Vec<u8>>,
122    mdx_buffer: Cursor<Vec<u8>>,
123    write_mdx: bool,
124    names: Vec<String>,
125    name_map: HashMap<String, u16>,
126    /// Content-relative byte offset of each name string (indexed by node_id).
127    name_string_offsets: Vec<u32>,
128    /// MDX position right after the last terminator, before alignment padding.
129    /// Used to trim trailing alignment from the final mesh.
130    mdx_pre_align_pos: u64,
131    /// Deferred MDX write entries, collected during DFS node traversal.
132    deferred_mdx: Vec<DeferredMdxEntry>,
133    /// Map of geometry node name -> written content-relative offset.
134    /// Used to resolve `anim_root_node` during header backpatch.
135    node_offset_map: HashMap<String, u32>,
136}
137
138impl MdlWriter {
139    fn new() -> Self {
140        MdlWriter {
141            buffer: Cursor::new(Vec::new()),
142            mdx_buffer: Cursor::new(Vec::new()),
143            write_mdx: false,
144            names: Vec::new(),
145            name_map: HashMap::new(),
146            name_string_offsets: Vec::new(),
147            mdx_pre_align_pos: 0,
148            deferred_mdx: Vec::new(),
149            node_offset_map: HashMap::new(),
150        }
151    }
152
153    fn new_with_mdx() -> Self {
154        MdlWriter {
155            buffer: Cursor::new(Vec::new()),
156            mdx_buffer: Cursor::new(Vec::new()),
157            write_mdx: true,
158            names: Vec::new(),
159            name_map: HashMap::new(),
160            name_string_offsets: Vec::new(),
161            mdx_pre_align_pos: 0,
162            deferred_mdx: Vec::new(),
163            node_offset_map: HashMap::new(),
164        }
165    }
166
167    /// Returns current buffer position as content-relative u32 offset.
168    /// Content byte 0 = on-disk byte 12 (after the 12-byte MDL wrapper).
169    fn content_position(&self) -> Result<u32, MdlError> {
170        let pos = self.buffer.position();
171        let content = pos
172            .checked_sub(MDL_WRAPPER_SIZE)
173            .ok_or(MdlError::ValueOverflow("content_position underflow"))?;
174        u32::try_from(content).map_err(|_| MdlError::ValueOverflow("content_position"))
175    }
176
177    /// Returns current MDX buffer position as u32.
178    fn mdx_position(&self) -> Result<u32, MdlError> {
179        u32::try_from(self.mdx_buffer.position())
180            .map_err(|_| MdlError::ValueOverflow("mdx_position"))
181    }
182
183    /// Seeks the MDL buffer to `base + offset`.
184    fn seek_to(&mut self, base: u64, offset: usize) {
185        self.buffer
186            .set_position(base + u64::try_from(offset).expect("offset fits in u64"));
187    }
188
189    fn write(mut self, mdl: &Mdl) -> Result<(Vec<u8>, Vec<u8>), MdlError> {
190        // 1. Gather Names via DFS tree walk (geometry + animation nodes).
191        self.collect_names(&mdl.root_node);
192        for anim in &mdl.animations {
193            self.collect_anim_names(&anim.root_node);
194        }
195
196        // 2. Write Wrapper placeholder (12 bytes, backpatched at end).
197        #[allow(clippy::as_conversions)]
198        const WRAPPER_BYTES: usize = MDL_WRAPPER_SIZE as usize;
199        // Static assert: MDL_WRAPPER_SIZE fits in usize without truncation.
200        #[allow(clippy::as_conversions)]
201        const _: () = assert!(MDL_WRAPPER_SIZE == WRAPPER_BYTES as u64);
202        self.buffer.write_all(&[0u8; WRAPPER_BYTES])?;
203
204        // 3. Reserve Header (196 bytes: 80-byte geometry + 116-byte model).
205        let header_start = self.buffer.position();
206        self.buffer.write_all(&[0u8; 196])?;
207
208        // 4. Write Name Table.
209        let name_offsets_ptr = self.content_position()?;
210        let name_count = count_u32(self.names.len(), "name_count")?;
211
212        // Reserve name offsets array.
213        let name_offsets_start = self.buffer.position();
214        for _ in 0..name_count {
215            write_u32(&mut self.buffer, 0)?;
216        }
217
218        // Write strings and record offsets.
219        self.name_string_offsets = Vec::with_capacity(self.names.len());
220        let mut current_offset_pos = name_offsets_start;
221        for name in &self.names {
222            let str_start = self.content_position()?;
223            self.name_string_offsets.push(str_start);
224
225            // Go back and write the offset for this string.
226            let save_pos = self.buffer.position();
227            self.buffer.set_position(current_offset_pos);
228            write_u32(&mut self.buffer, str_start)?;
229            self.buffer.set_position(save_pos);
230            current_offset_pos += 4;
231
232            // Write string + null terminator.
233            self.buffer.write_all(name.as_bytes())?;
234            self.buffer.write_all(&[0u8])?;
235        }
236
237        // 5. Write Nodes Recursively
238        let root_node_offset = self.write_node(&mdl.root_node, None)?;
239
240        // 5b. Flush deferred MDX writes in canonical order:
241        // non-skin meshes first (DFS order), then skin meshes (DFS order).
242        // See mdl_mdx.md §MDX Per-Mesh Terminator Rows (Finding 4).
243        if self.write_mdx && !self.deferred_mdx.is_empty() {
244            self.write_deferred_mdx(&mdl.root_node)?;
245        }
246
247        // 5c. Write Animations (after geometry nodes, before header backpatch).
248        let (anim_arr_ptr, anim_arr_count) = self.write_animations(&mdl.animations)?;
249
250        // 6. Backpatch Header - write all fields explicitly.
251
252        // --- Geometry header (+0x00..+0x4F) ---
253        self.seek_to(header_start, header_offsets::FN_PTR1);
254        write_u32(&mut self.buffer, mdl.geometry_fn_ptr1)?;
255
256        self.seek_to(header_start, header_offsets::FN_PTR2);
257        write_u32(&mut self.buffer, mdl.geometry_fn_ptr2)?;
258
259        // Model name at +0x08 (32-byte null-terminated, same as root node name).
260        self.seek_to(header_start, header_offsets::MODEL_NAME);
261        let model_name = mdl.root_node.name.as_bytes();
262        let model_name_len = model_name.len().min(header_offsets::MODEL_NAME_SIZE - 1);
263        self.buffer.write_all(&model_name[..model_name_len])?;
264        self.buffer.write_all(&[0])?;
265
266        self.seek_to(header_start, header_offsets::ROOT_NODE_PTR);
267        write_u32(&mut self.buffer, root_node_offset)?;
268
269        self.seek_to(header_start, header_offsets::NODE_COUNT);
270        write_u32(&mut self.buffer, mdl.node_count)?;
271
272        // Runtime arrays at +0x30..+0x47 stay zero (written at init).
273        // ref_count at +0x48 stays zero.
274
275        // Model type at +0x4C (always 2 for geometry models).
276        self.seek_to(header_start, header_offsets::MODEL_TYPE);
277        self.buffer.write_all(&[mdl.model_type])?;
278
279        // --- Model header (+0x50..+0xC3) ---
280        self.seek_to(header_start, header_offsets::CLASSIFICATION);
281        self.buffer.write_all(&[mdl.classification])?;
282
283        self.seek_to(header_start, header_offsets::SUBCLASSIFICATION);
284        self.buffer.write_all(&[mdl.subclassification])?;
285
286        // +0x52: unknown byte, stays zero.
287
288        self.seek_to(header_start, header_offsets::AFFECTED_BY_FOG);
289        self.buffer.write_all(&[mdl.affected_by_fog])?;
290
291        // num_child_models at +0x54: always 0 (stays zero from init).
292
293        // Animation array CExoArrayList at +0x58 (ptr/count/alloc).
294        self.seek_to(header_start, header_offsets::ANIMATION_ARR_PTR);
295        write_u32(&mut self.buffer, anim_arr_ptr)?;
296        write_u32(&mut self.buffer, anim_arr_count)?;
297        write_u32(&mut self.buffer, anim_arr_count)?; // alloc mirrors count
298
299        // supermodel_ref at +0x64: always 0 (stays zero from init).
300
301        // Bounding box at +0x68 (6 floats).
302        self.seek_to(header_start, header_offsets::BOUNDING_BOX_MIN);
303        for &val in &mdl.bounding_box {
304            write_f32(&mut self.buffer, val)?;
305        }
306
307        // Radius at +0x80.
308        self.seek_to(header_start, header_offsets::RADIUS);
309        write_f32(&mut self.buffer, mdl.radius)?;
310
311        // Animation scale at +0x84.
312        self.seek_to(header_start, header_offsets::ANIMATION_SCALE);
313        write_f32(&mut self.buffer, mdl.animation_scale)?;
314
315        // Supermodel name at +0x88 (null-terminated, 32-byte field).
316        self.seek_to(header_start, header_offsets::SUPERMODEL_NAME);
317        let name_bytes = mdl.supermodel_name.as_bytes();
318        let write_len = name_bytes
319            .len()
320            .min(header_offsets::SUPERMODEL_NAME_SIZE - 1);
321        self.buffer.write_all(&name_bytes[..write_len])?;
322        self.buffer.write_all(&[0])?;
323
324        // off_anim_root at +0xA8: points to the animation root node.
325        // For most models this is the geometry root. Head models point to
326        // `neck_g` so the engine applies head animations from the neck bone.
327        let anim_root_offset = mdl
328            .anim_root_node
329            .as_ref()
330            .and_then(|name| self.node_offset_map.get(name).copied())
331            .unwrap_or(root_node_offset);
332        self.seek_to(header_start, header_offsets::OFF_ANIM_ROOT);
333        write_u32(&mut self.buffer, anim_root_offset)?;
334
335        // +0xAC: padding, stays zero.
336
337        // Name array at +0xB8.
338        self.seek_to(header_start, header_offsets::NAME_OFFSETS_PTR);
339        write_u32(&mut self.buffer, name_offsets_ptr)?;
340
341        self.seek_to(header_start, header_offsets::NAME_COUNT);
342        write_u32(&mut self.buffer, name_count)?;
343
344        let mut mdl_bytes = self.buffer.into_inner();
345        let mut mdx_bytes = self.mdx_buffer.into_inner();
346
347        // Trim trailing 16-byte alignment padding from the last mesh's
348        // terminator. The MDX file ends right after the last terminator,
349        // without alignment padding. The writer tracks the pre-alignment
350        // position to truncate precisely.
351        // See mdl_mdx.md §MDX Per-Mesh Terminator Rows (Finding 3).
352        let trim_to =
353            usize::try_from(self.mdx_pre_align_pos).expect("mdx_pre_align_pos fits in usize");
354        if trim_to > 0 && trim_to < mdx_bytes.len() {
355            mdx_bytes.truncate(trim_to);
356        }
357
358        // Backpatch wrapper sizes:
359        // +0x00 zero marker (always 0), +0x04 MDL content size, +0x08 MDX file size.
360        let mdl_content_size = mdl_bytes
361            .len()
362            .checked_sub(WRAPPER_BYTES)
363            .ok_or_else(|| MdlError::InvalidData("serialized MDL shorter than wrapper".into()))?;
364        let mdl_content_size_u32 = u32::try_from(mdl_content_size)
365            .map_err(|_| MdlError::ValueOverflow("mdl_content_size"))?;
366        let mdx_size_u32 =
367            u32::try_from(mdx_bytes.len()).map_err(|_| MdlError::ValueOverflow("mdx_file_size"))?;
368        mdl_bytes[0..4].copy_from_slice(&0u32.to_le_bytes());
369        mdl_bytes[4..8].copy_from_slice(&mdl_content_size_u32.to_le_bytes());
370        mdl_bytes[8..12].copy_from_slice(&mdx_size_u32.to_le_bytes());
371
372        // Backpatch MDX size in model header (+0xB0).
373        let mdx_size_offset = WRAPPER_BYTES + header_offsets::MDX_SIZE;
374        mdl_bytes[mdx_size_offset..mdx_size_offset + 4]
375            .copy_from_slice(&mdx_size_u32.to_le_bytes());
376
377        Ok((mdl_bytes, mdx_bytes))
378    }
379
380    /// Flush deferred MDX writes in canonical order: non-skin meshes first
381    /// (DFS order), then skin meshes (DFS order). Each mesh gets its vertex
382    /// data written followed by a terminator row with 16-byte alignment
383    /// (except the very last mesh, whose trailing alignment is trimmed by
384    /// the caller).
385    ///
386    /// The DFS-order mesh references are obtained by walking the node tree
387    /// in the same pre-order traversal used by `write_node`.
388    fn write_deferred_mdx(&mut self, root: &MdlNode) -> Result<(), MdlError> {
389        // Collect mesh node references in DFS order.
390        let mut mesh_refs: Vec<MeshRef<'_>> = Vec::new();
391        Self::collect_mesh_refs(root, &mut mesh_refs);
392
393        // Sanity check: deferred entries must match mesh refs 1:1.
394        debug_assert_eq!(mesh_refs.len(), self.deferred_mdx.len());
395
396        // Build ordered indices: non-skin first, then skin (both in DFS order).
397        // DFS order is preserved because `deferred_mdx` was populated in DFS
398        // order and we use a stable partition.
399        let entries: Vec<DeferredMdxEntry> = std::mem::take(&mut self.deferred_mdx);
400        let (non_skin, skin): (Vec<usize>, Vec<usize>) =
401            (0..entries.len()).partition(|&i| !entries[i].is_skin);
402        let ordered: Vec<usize> = non_skin.into_iter().chain(skin).collect();
403
404        for idx in &ordered {
405            let entry = &entries[*idx];
406            let mesh_ref = &mesh_refs[entry.dfs_index];
407
408            // Record MDX file offset BEFORE writing vertex data -- this is
409            // where the engine will find this mesh's vertices in the MDX buffer.
410            let mdx_offset = self.mdx_position()?;
411
412            let stride = self.write_mesh_mdx_data(
413                mesh_ref.mesh,
414                mesh_ref.skin,
415                entry.mesh_header_pos,
416                entry.skin_header_pos,
417            )?;
418            self.write_mdx_terminator(stride, mesh_ref.is_skin)?;
419
420            // Backpatch mesh header +0x144 with the MDX file offset.
421            // Community tools (mdlops, kotorblender) write this value and the
422            // engine uses it to locate per-mesh vertex data within the bulk
423            // MDX GL vertex buffer.
424            let save = self.buffer.position();
425            self.seek_to(entry.mesh_header_pos, mesh_offsets::MDX_DATA_OFFSET);
426            write_u32(&mut self.buffer, mdx_offset)?;
427            self.buffer.set_position(save);
428        }
429
430        Ok(())
431    }
432
433    /// Collect mesh references from the node tree in pre-order DFS, matching
434    /// the traversal order of `write_node`. Returns mesh, optional skin, and
435    /// whether the node is a skin.
436    ///
437    /// Saber nodes are excluded because they have no MDX vertex data (stride=0).
438    fn collect_mesh_refs<'a>(node: &'a MdlNode, out: &mut Vec<MeshRef<'a>>) {
439        if !node.is_saber() {
440            if let Some(mesh) = node.node_data.mesh() {
441                let skin = if let MdlNodeData::Skin(s) = &node.node_data {
442                    Some(s)
443                } else {
444                    None
445                };
446                out.push(MeshRef {
447                    mesh,
448                    skin,
449                    is_skin: node.is_skin(),
450                });
451            }
452        }
453        for child in &node.children {
454            Self::collect_mesh_refs(child, out);
455        }
456    }
457
458    fn collect_names(&mut self, node: &MdlNode) {
459        if !self.name_map.contains_key(&node.name) {
460            let index = count_u16(self.names.len(), "name_index")
461                .expect("name table must not exceed u16::MAX entries during collect_names");
462            // Both the name table (Vec) and the lookup map (HashMap) need owned
463            // copies of the name. The double clone is unavoidable without an
464            // index-based refactor; model name tables are small (~100 entries).
465            self.name_map.insert(node.name.clone(), index);
466            self.names.push(node.name.clone());
467        }
468        for child in &node.children {
469            self.collect_names(child);
470        }
471    }
472
473    fn collect_anim_names(&mut self, node: &super::MdlAnimNode) {
474        if !self.name_map.contains_key(&node.name) {
475            let index = count_u16(self.names.len(), "name_index")
476                .expect("name table must not exceed u16::MAX entries during collect_anim_names");
477            self.name_map.insert(node.name.clone(), index);
478            self.names.push(node.name.clone());
479        }
480        for child in &node.children {
481            self.collect_anim_names(child);
482        }
483    }
484
485    // -----------------------------------------------------------------------
486    // Animation writer
487    // -----------------------------------------------------------------------
488
489    /// Writes all animations and returns `(anim_arr_ptr, anim_count)` for
490    /// backpatching into the model header's CExoArrayList at +0x58.
491    fn write_animations(
492        &mut self,
493        animations: &[super::MdlAnimation],
494    ) -> Result<(u32, u32), MdlError> {
495        if animations.is_empty() {
496            return Ok((0, 0));
497        }
498
499        // Write the offset array (one u32 per animation, content-relative).
500        let arr_ptr = self.content_position()?;
501        let arr_count = count_u32(animations.len(), "animation_count")?;
502
503        // Reserve the offset array, backpatch after writing each animation.
504        let arr_start = self.buffer.position();
505        for _ in 0..arr_count {
506            write_u32(&mut self.buffer, 0)?;
507        }
508
509        let mut anim_offsets = Vec::with_capacity(animations.len());
510        for anim in animations {
511            let offset = self.write_animation(anim)?;
512            anim_offsets.push(offset);
513        }
514
515        // Backpatch the offset array.
516        let save_pos = self.buffer.position();
517        self.buffer.set_position(arr_start);
518        for &offset in &anim_offsets {
519            write_u32(&mut self.buffer, offset)?;
520        }
521        self.buffer.set_position(save_pos);
522
523        Ok((arr_ptr, arr_count))
524    }
525
526    /// Writes a single animation: header (136 bytes) + events + node tree.
527    /// Returns the content-relative offset of the animation header.
528    fn write_animation(&mut self, anim: &super::MdlAnimation) -> Result<u32, MdlError> {
529        let anim_offset = self.content_position()?;
530
531        // Reserve 136-byte animation header (backpatched below).
532        let header_pos = self.buffer.position();
533        self.buffer
534            .write_all(&[0u8; super::ANIMATION_HEADER_SIZE])?;
535
536        // Write events.
537        let events_ptr = if anim.events.is_empty() {
538            0u32
539        } else {
540            let ptr = self.content_position()?;
541            for event in &anim.events {
542                write_f32(&mut self.buffer, event.time)?;
543                self.write_fixed_string(&event.name, 32)?;
544            }
545            ptr
546        };
547        let event_count = count_u32(anim.events.len(), "event_count")?;
548
549        // Write animation node tree recursively.
550        let root_node_offset = self.write_anim_node(&anim.root_node, anim_offset, None)?;
551
552        // Count total animation nodes.
553        let total_nodes = super::count_anim_nodes(&anim.root_node);
554
555        // Backpatch animation header.
556        let save_pos = self.buffer.position();
557        self.buffer.set_position(header_pos);
558
559        // fn_ptr1, fn_ptr2 at +0x00, +0x04
560        write_u32(&mut self.buffer, anim.fn_ptr1)?;
561        write_u32(&mut self.buffer, anim.fn_ptr2)?;
562
563        // name at +0x08 (32 bytes)
564        self.write_fixed_string(&anim.name, super::anim_header_offsets::NAME_SIZE)?;
565
566        // root_node_ptr at +0x28
567        write_u32(&mut self.buffer, root_node_offset)?;
568
569        // total_num_nodes at +0x2C
570        write_u32(&mut self.buffer, total_nodes)?;
571
572        // runtime_arr1 at +0x30 (12 bytes zeros) - already zero from init
573        self.buffer.write_all(&[0u8; 12])?;
574        // runtime_arr2 at +0x3C (12 bytes zeros) - already zero from init
575        self.buffer.write_all(&[0u8; 12])?;
576        // ref_count at +0x48
577        write_u32(&mut self.buffer, 0)?;
578
579        // model_type at +0x4C (u8 = 5 for animations, + 3 padding)
580        self.buffer.write_all(&[5, 0, 0, 0])?;
581
582        // length at +0x50
583        write_f32(&mut self.buffer, anim.length)?;
584
585        // transition at +0x54
586        write_f32(&mut self.buffer, anim.transition_time)?;
587
588        // anim_root at +0x58 (32 bytes)
589        self.write_fixed_string(&anim.anim_root, super::anim_header_offsets::ANIM_ROOT_SIZE)?;
590
591        // event_arr at +0x78 (ptr/count/alloc)
592        write_u32(&mut self.buffer, events_ptr)?;
593        write_u32(&mut self.buffer, event_count)?;
594        write_u32(&mut self.buffer, event_count)?;
595
596        // padding at +0x84 (4 bytes, stays zero from init)
597        write_u32(&mut self.buffer, 0)?;
598
599        self.buffer.set_position(save_pos);
600        Ok(anim_offset)
601    }
602
603    /// Writes a single animation node (80-byte base header) and its children.
604    /// Returns the content-relative offset.
605    fn write_anim_node(
606        &mut self,
607        node: &super::MdlAnimNode,
608        anim_header_offset: u32,
609        parent_node_offset: Option<u32>,
610    ) -> Result<u32, MdlError> {
611        let node_offset = self.content_position()?;
612
613        // Reserve 80-byte node header.
614        let header_pos = self.buffer.position();
615        self.buffer.write_all(&[0u8; NODE_HEADER_SIZE])?;
616
617        // Write children array.
618        let child_arr_ptr = if node.children.is_empty() {
619            0u32
620        } else {
621            let ptr = self.content_position()?;
622            // Reserve child offset array.
623            let children_start = self.buffer.position();
624            for _ in 0..node.children.len() {
625                write_u32(&mut self.buffer, 0)?;
626            }
627
628            // Write each child and collect offsets.
629            let mut child_offsets = Vec::with_capacity(node.children.len());
630            for child in &node.children {
631                let child_off =
632                    self.write_anim_node(child, anim_header_offset, Some(node_offset))?;
633                child_offsets.push(child_off);
634            }
635
636            // Backpatch child offsets.
637            let save = self.buffer.position();
638            self.buffer.set_position(children_start);
639            for &off in &child_offsets {
640                write_u32(&mut self.buffer, off)?;
641            }
642            self.buffer.set_position(save);
643
644            ptr
645        };
646        let child_count = count_u32(node.children.len(), "anim_child_count")?;
647
648        // Write controllers.
649        let (ctrl_key_ptr, ctrl_key_count, ctrl_data_ptr, ctrl_data_count) =
650            self.write_controllers(&node.controllers, &node.orphan_controller_data)?;
651
652        // Backpatch node header.
653        let save_pos = self.buffer.position();
654        self.buffer.set_position(header_pos);
655
656        // +0x00: type_flags (u16) -- animation nodes are always NODE_BASE (0x0001)
657        write_u16(
658            &mut self.buffer,
659            u16::try_from(super::node_flags::HEADER)
660                .map_err(|_| MdlError::ValueOverflow("anim_node_flags"))?,
661        )?;
662
663        // +0x02: node_number (u16) -- maps to the geometry node index
664        write_u16(&mut self.buffer, node.node_number)?;
665
666        // +0x04: name_index (u16) -- index into model name table
667        let name_index = *self.name_map.get(&node.name).unwrap_or(&0);
668        write_u16(&mut self.buffer, name_index)?;
669
670        // +0x06: padding (2 bytes)
671        self.buffer.write_all(&[0u8; 2])?;
672
673        // +0x08: root pointer (content-relative offset to animation header)
674        write_u32(&mut self.buffer, anim_header_offset)?;
675
676        // +0x0C: parent pointer
677        write_u32(&mut self.buffer, parent_node_offset.unwrap_or(0))?;
678
679        // +0x10: position (3 × f32) - animation nodes use identity transform
680        write_f32(&mut self.buffer, 0.0)?;
681        write_f32(&mut self.buffer, 0.0)?;
682        write_f32(&mut self.buffer, 0.0)?;
683
684        // +0x1C: orientation quaternion (w, x, y, z)
685        write_f32(&mut self.buffer, 1.0)?;
686        write_f32(&mut self.buffer, 0.0)?;
687        write_f32(&mut self.buffer, 0.0)?;
688        write_f32(&mut self.buffer, 0.0)?;
689
690        // +0x2C: children array (ptr/count/alloc)
691        write_u32(&mut self.buffer, child_arr_ptr)?;
692        write_u32(&mut self.buffer, child_count)?;
693        write_u32(&mut self.buffer, child_count)?;
694
695        // +0x38: controller key array (ptr/count/alloc)
696        write_u32(&mut self.buffer, ctrl_key_ptr)?;
697        write_u32(&mut self.buffer, ctrl_key_count)?;
698        write_u32(&mut self.buffer, ctrl_key_count)?;
699
700        // +0x44: controller data array (ptr/count/alloc)
701        write_u32(&mut self.buffer, ctrl_data_ptr)?;
702        write_u32(&mut self.buffer, ctrl_data_count)?;
703        write_u32(&mut self.buffer, ctrl_data_count)?;
704
705        self.buffer.set_position(save_pos);
706        Ok(node_offset)
707    }
708
709    // -----------------------------------------------------------------------
710    // Geometry node writer
711    // -----------------------------------------------------------------------
712
713    fn write_node(
714        &mut self,
715        node: &MdlNode,
716        parent_node_offset: Option<u32>,
717    ) -> Result<u32, MdlError> {
718        // Current position relative to wrapper end (byte 12 is offset 0)
719        let node_start_offset = self.content_position()?;
720
721        // Record this node's written offset for anim_root_node resolution.
722        self.node_offset_map
723            .insert(node.name.clone(), node_start_offset);
724
725        // Reserve Node Header (0x44 = 68 bytes)
726        let header_pos = self.buffer.position();
727        self.buffer.write_all(&[0u8; NODE_HEADER_SIZE])?;
728
729        // Write type-specific headers based on node_data variant.
730        // Non-mesh types with stub headers come first (matching binary packing order),
731        // then mesh header if this is a mesh-derived variant.
732        let light_header_pos = match &node.node_data {
733            MdlNodeData::Light(light) => Some(self.write_light_header(light)?),
734            _ => None,
735        };
736        match &node.node_data {
737            MdlNodeData::Light(_) => {} // header already written above
738            MdlNodeData::Emitter(emitter) => {
739                self.write_emitter_header(emitter)?;
740            }
741            MdlNodeData::Camera(_) => {
742                // Camera: 0 extra bytes (Ghidra-verified, mdl_mdx.md Non-Mesh Node Type Structs)
743            }
744            MdlNodeData::Reference(reference) => {
745                self.write_reference_header(reference)?;
746            }
747            _ => {}
748        }
749
750        // Phase 1: Write all headers contiguously (mesh + subtype).
751        // The binary format requires headers to be contiguous, with array
752        // data following separately (pointed to by CExoArrayList pointers).
753        let mesh_header_pos = if let Some(mesh) = node.node_data.mesh() {
754            Some(self.write_mesh_header(mesh)?)
755        } else {
756            None
757        };
758
759        // Write mesh subtype extra headers (immediately after TriMesh header).
760        let skin_header_pos = if let MdlNodeData::Skin(skin) = &node.node_data {
761            Some(self.write_skin_header(skin)?)
762        } else {
763            None
764        };
765        let anim_header_pos = if let MdlNodeData::AnimMesh(anim) = &node.node_data {
766            Some(self.write_anim_mesh_header(anim)?)
767        } else {
768            None
769        };
770        let dangly_header_pos = if let MdlNodeData::Dangly(dangly) = &node.node_data {
771            Some(self.write_dangly_header(dangly)?)
772        } else {
773            None
774        };
775        let aabb_header_pos = if let MdlNodeData::Aabb(_) = &node.node_data {
776            Some(self.write_aabb_header()?)
777        } else {
778            None
779        };
780        let saber_header_pos = if let MdlNodeData::Saber(saber) = &node.node_data {
781            Some(self.write_saber_header(saber)?)
782        } else {
783            None
784        };
785
786        // Phase 2: Write array data (faces, constraints, bones, verts, etc.) after all headers.
787        //
788        // Light flare arrays are written first (non-mesh type with CExoArrayList data).
789        if let (MdlNodeData::Light(light), Some(lhp)) = (&node.node_data, light_header_pos) {
790            self.write_light_arrays(light, lhp)?;
791        }
792        // Skin nodes are ordered specially: skin-bone arrays are emitted before
793        // TriMesh internal arrays in vanilla binaries. This ordering affects
794        // TriMesh pointer fields (+0xB0/+0xBC/+0xC8) and must match for
795        // roundtrip parity.
796        if let (MdlNodeData::Skin(skin), Some(shp)) = (&node.node_data, skin_header_pos) {
797            self.write_skin_arrays(skin, shp)?;
798        }
799        if let (MdlNodeData::Saber(saber), Some(shp)) = (&node.node_data, saber_header_pos) {
800            self.write_saber_arrays(saber, shp)?;
801        }
802        if let (MdlNodeData::Dangly(dangly), Some(dhp)) = (&node.node_data, dangly_header_pos) {
803            self.write_dangly_arrays(dangly, dhp)?;
804            self.write_dangly_pre_mesh_payload(dangly, dhp)?;
805        }
806        if let (MdlNodeData::Aabb(aabb), Some(ahp)) = (&node.node_data, aabb_header_pos) {
807            self.write_aabb_arrays(aabb, ahp)?;
808        }
809        let embedded_pos_ptr =
810            if let (Some(mesh), Some(mhp)) = (node.node_data.mesh(), mesh_header_pos) {
811                self.write_mesh_arrays(mesh, mhp)?
812            } else {
813                None
814            };
815        if let (MdlNodeData::AnimMesh(anim), Some(ahp)) = (&node.node_data, anim_header_pos) {
816            self.write_anim_mesh_arrays(anim, ahp)?;
817        }
818        // Phase 2b: Write position-only vertex data into MDL content blob.
819        //
820        // Embedded vertex positions are ALWAYS written (even when MDX is also
821        // being generated). The vert_array_offset field (+0x148) is a
822        // content-relative pointer to this data, and the engine relocates it
823        // during Reset. The separate mdx_data_offset (+0x144) points into the
824        // MDX file for the full interleaved vertex buffer.
825        if let (Some(mesh), Some(mhp)) = (node.node_data.mesh(), mesh_header_pos) {
826            self.write_mesh_content_positions(mesh, mhp, embedded_pos_ptr)?;
827        }
828
829        // Phase 3: Defer MDX vertex data (if enabled).
830        // MDX data is written after all MDL nodes are complete, in canonical
831        // order: non-skin meshes first, then skin meshes (both in DFS order).
832        // See mdl_mdx.md §MDX Per-Mesh Terminator Rows (Finding 4).
833        //
834        // Saber nodes are excluded: they have stride=0 and no MDX vertex data.
835        // Saber vertex positions live in the MDL content blob (vert_array_offset),
836        // not in the MDX file.
837        if self.write_mdx && !node.is_saber() {
838            if let Some(mhp) = mesh_header_pos {
839                if node.node_data.mesh().is_some() {
840                    let dfs_index = self.deferred_mdx.len();
841                    self.deferred_mdx.push(DeferredMdxEntry {
842                        mesh_header_pos: mhp,
843                        skin_header_pos,
844                        is_skin: node.is_skin(),
845                        dfs_index,
846                    });
847                }
848            }
849        }
850
851        // Write Children (pointer table + recursive child payloads) before
852        // controller arrays to match vanilla binary node packing.
853        let child_count = count_u32(node.children.len(), "child_count")?;
854        let mut child_array_ptr = 0;
855
856        if child_count > 0 {
857            // Reserve Child Pointer Array
858            let array_start = self.buffer.position();
859            child_array_ptr = self.content_position()?;
860            let child_arr_bytes =
861                usize::try_from(child_count).expect("child_count fits in usize") * 4;
862            self.buffer.write_all(&vec![0u8; child_arr_bytes])?;
863
864            // Recurse for each child
865            for (i, child) in node.children.iter().enumerate() {
866                let child_offset = self.write_node(child, Some(node_start_offset))?;
867
868                // Backpatch pointer
869                let save = self.buffer.position();
870                // i is bounded by child_count (verified to fit u32 above).
871                self.seek_to(array_start, i * 4);
872                write_u32(&mut self.buffer, child_offset)?;
873                self.buffer.set_position(save);
874            }
875        }
876
877        // Write Controllers after child payloads.
878        let (key_ptr, key_count, data_ptr, data_count) =
879            self.write_controllers(&node.controllers, &node.orphan_controller_data)?;
880
881        // Backpatch Node Header
882        let save_end = self.buffer.position();
883        self.buffer.set_position(header_pos);
884
885        write_u16(
886            &mut self.buffer,
887            u16::try_from(node.node_data.flags())
888                .map_err(|_| MdlError::ValueOverflow("node_flags"))?,
889        )?; // 0x00
890        self.buffer.write_all(&node.header_padding_02)?; // 0x02..0x03
891
892        let node_id = *self.name_map.get(&node.name).unwrap_or(&0);
893        write_u16(&mut self.buffer, node_id)?; // 0x04
894
895        // +0x06: struct alignment padding.
896        self.buffer.write_all(&node.header_padding_06)?; // 0x06..0x07
897
898        // +0x08: binary MDL files preserve this slot as 0 in vanilla assets.
899        // Node naming is resolved via node_id + global name table.
900        write_u32(&mut self.buffer, 0)?; // 0x08..0x0B
901
902        // +0x0C: parent node offset (content-relative, relocated if non-zero).
903        // Root nodes have no parent (0). See mdl_mdx.md §MdlNode Binary Layout.
904        write_u32(&mut self.buffer, parent_node_offset.unwrap_or(0))?; // 0x0C..0x0F
905
906        self.seek_to(header_pos, node_offsets::POS_X);
907        write_f32(&mut self.buffer, node.position[0])?;
908        write_f32(&mut self.buffer, node.position[1])?;
909        write_f32(&mut self.buffer, node.position[2])?;
910
911        // Orientation quaternion (w, x, y, z) - direct, no swizzle
912        self.seek_to(header_pos, node_offsets::ORIENTATION_W);
913        write_f32(&mut self.buffer, node.rotation[0])?; // w
914        write_f32(&mut self.buffer, node.rotation[1])?; // x
915        write_f32(&mut self.buffer, node.rotation[2])?; // y
916        write_f32(&mut self.buffer, node.rotation[3])?; // z
917
918        // 3-field arrays: ptr, count_used, count_allocated (alloc = used on disk)
919        self.seek_to(header_pos, node_offsets::CHILD_ARRAY_PTR);
920        let child_ptr_field = if child_count > 0 {
921            child_array_ptr
922        } else if key_count > 0 {
923            // Vanilla binaries often mirror controller-key pointer here even
924            // when child_count is zero.
925            key_ptr
926        } else {
927            0
928        };
929        write_u32(&mut self.buffer, child_ptr_field)?;
930        write_u32(&mut self.buffer, child_count)?; // used
931        write_u32(&mut self.buffer, child_count)?; // allocated
932
933        self.seek_to(header_pos, node_offsets::CONTROLLER_KEY_PTR);
934        write_u32(&mut self.buffer, key_ptr)?;
935        write_u32(&mut self.buffer, key_count)?; // used
936        write_u32(&mut self.buffer, key_count)?; // allocated
937
938        self.seek_to(header_pos, node_offsets::CONTROLLER_DATA_PTR);
939        write_u32(&mut self.buffer, data_ptr)?;
940        write_u32(&mut self.buffer, data_count)?; // used
941        write_u32(&mut self.buffer, data_count)?; // allocated
942
943        self.buffer.set_position(save_end);
944        Ok(node_start_offset)
945    }
946
947    /// Writes a f32 at `header_pos + field_offset` without moving the cursor.
948    fn write_f32_at(
949        &mut self,
950        header_pos: u64,
951        field_offset: usize,
952        val: f32,
953    ) -> Result<(), MdlError> {
954        let save = self.buffer.position();
955        self.seek_to(header_pos, field_offset);
956        write_f32(&mut self.buffer, val)?;
957        self.buffer.set_position(save);
958        Ok(())
959    }
960
961    /// Writes a u32 at `header_pos + field_offset` without moving the cursor.
962    fn write_u32_at(
963        &mut self,
964        header_pos: u64,
965        field_offset: usize,
966        val: u32,
967    ) -> Result<(), MdlError> {
968        let save = self.buffer.position();
969        self.seek_to(header_pos, field_offset);
970        write_u32(&mut self.buffer, val)?;
971        self.buffer.set_position(save);
972        Ok(())
973    }
974
975    /// Writes an i32 at `header_pos + field_offset` without moving the cursor.
976    fn write_i32_at(
977        &mut self,
978        header_pos: u64,
979        field_offset: usize,
980        val: i32,
981    ) -> Result<(), MdlError> {
982        let save = self.buffer.position();
983        self.seek_to(header_pos, field_offset);
984        write_i32(&mut self.buffer, val)?;
985        self.buffer.set_position(save);
986        Ok(())
987    }
988
989    /// Writes a u16 at `header_pos + field_offset` without moving the cursor.
990    fn write_u16_at(
991        &mut self,
992        header_pos: u64,
993        field_offset: usize,
994        val: u16,
995    ) -> Result<(), MdlError> {
996        let save = self.buffer.position();
997        self.seek_to(header_pos, field_offset);
998        write_u16(&mut self.buffer, val)?;
999        self.buffer.set_position(save);
1000        Ok(())
1001    }
1002
1003    /// Writes a u8 at `header_pos + field_offset` without moving the cursor.
1004    fn write_u8_at(
1005        &mut self,
1006        header_pos: u64,
1007        field_offset: usize,
1008        val: u8,
1009    ) -> Result<(), MdlError> {
1010        let save = self.buffer.position();
1011        self.seek_to(header_pos, field_offset);
1012        self.buffer.write_all(&[val])?;
1013        self.buffer.set_position(save);
1014        Ok(())
1015    }
1016
1017    /// Writes a 3×f32 vector at `header_pos + field_offset` without moving the cursor.
1018    fn write_vec3_at(
1019        &mut self,
1020        header_pos: u64,
1021        field_offset: usize,
1022        v: &[f32; 3],
1023    ) -> Result<(), MdlError> {
1024        let save = self.buffer.position();
1025        self.seek_to(header_pos, field_offset);
1026        write_f32(&mut self.buffer, v[0])?;
1027        write_f32(&mut self.buffer, v[1])?;
1028        write_f32(&mut self.buffer, v[2])?;
1029        self.buffer.set_position(save);
1030        Ok(())
1031    }
1032
1033    /// Writes a null-terminated string at `header_pos + field_offset`,
1034    /// padded with zeros to `max_len` bytes.
1035    fn write_string_at(
1036        &mut self,
1037        header_pos: u64,
1038        field_offset: usize,
1039        s: &str,
1040        max_len: usize,
1041    ) -> Result<(), MdlError> {
1042        let save = self.buffer.position();
1043        self.seek_to(header_pos, field_offset);
1044        crate::binary::write_fixed_c_string(&mut self.buffer, s, max_len)?;
1045        self.buffer.set_position(save);
1046        Ok(())
1047    }
1048
1049    /// Writes the 332-byte mesh extra header from typed fields.
1050    ///
1051    /// Returns the header position for deferred backpatching by
1052    /// [`write_mesh_arrays`].
1053    fn write_mesh_header(&mut self, mesh: &super::MdlMesh) -> Result<u64, MdlError> {
1054        let header_pos = self.buffer.position();
1055        // Zero-filled 332-byte mesh extra header; typed fields are overlaid below.
1056        self.buffer.write_all(&[0u8; MESH_EXTRA_SIZE])?;
1057
1058        // Toolset function pointer stubs (extra +0x00, +0x04).
1059        self.write_u32_at(
1060            header_pos,
1061            mesh_offsets::FN_PTR_GEN_VERTICES,
1062            mesh.fn_ptr_gen_vertices,
1063        )?;
1064        self.write_u32_at(
1065            header_pos,
1066            mesh_offsets::FN_PTR_REMOVE_TEMP_ARRAY,
1067            mesh.fn_ptr_remove_temp_array,
1068        )?;
1069
1070        // Bounding box and sphere.
1071        self.write_vec3_at(header_pos, mesh_offsets::BOUNDING_MIN, &mesh.bounding_min)?;
1072        self.write_vec3_at(header_pos, mesh_offsets::BOUNDING_MAX, &mesh.bounding_max)?;
1073        self.write_f32_at(
1074            header_pos,
1075            mesh_offsets::BSPHERE_RADIUS,
1076            mesh.bsphere_radius,
1077        )?;
1078        self.write_vec3_at(
1079            header_pos,
1080            mesh_offsets::BSPHERE_CENTER,
1081            &mesh.bsphere_center,
1082        )?;
1083
1084        // Colors.
1085        self.write_vec3_at(header_pos, mesh_offsets::DIFFUSE_COLOR, &mesh.diffuse_color)?;
1086        self.write_vec3_at(header_pos, mesh_offsets::AMBIENT_COLOR, &mesh.ambient_color)?;
1087
1088        // Transparency.
1089        self.write_i32_at(
1090            header_pos,
1091            mesh_offsets::TRANSPARENCY_HINT,
1092            mesh.transparency_hint,
1093        )?;
1094
1095        // Texture names.
1096        self.write_string_at(
1097            header_pos,
1098            mesh_offsets::TEXTURE_0,
1099            &mesh.texture_0,
1100            mesh_offsets::TEXTURE_NAME_SIZE,
1101        )?;
1102        self.write_string_at(
1103            header_pos,
1104            mesh_offsets::TEXTURE_1,
1105            &mesh.texture_1,
1106            mesh_offsets::TEXTURE_NAME_SIZE,
1107        )?;
1108
1109        // Shared index scalars.
1110        {
1111            let save = self.buffer.position();
1112            self.seek_to(header_pos, mesh_offsets::SHARED_INDEX_OFFSET);
1113            write_i32(&mut self.buffer, mesh.shared_index_offset)?;
1114            write_i32(&mut self.buffer, mesh.shared_index_pool)?;
1115            write_i32(&mut self.buffer, mesh.shared_index_size)?;
1116            write_u32(&mut self.buffer, mesh.indices_per_face)?;
1117            self.buffer.set_position(save);
1118        }
1119
1120        // UV animation.
1121        self.write_i32_at(header_pos, mesh_offsets::ANIMATE_UV, mesh.animate_uv)?;
1122        self.write_f32_at(
1123            header_pos,
1124            mesh_offsets::UV_DIRECTION_X,
1125            mesh.uv_direction_x,
1126        )?;
1127        self.write_f32_at(
1128            header_pos,
1129            mesh_offsets::UV_DIRECTION_Y,
1130            mesh.uv_direction_y,
1131        )?;
1132        self.write_f32_at(header_pos, mesh_offsets::UV_JITTER, mesh.uv_jitter)?;
1133        self.write_f32_at(
1134            header_pos,
1135            mesh_offsets::UV_JITTER_SPEED,
1136            mesh.uv_jitter_speed,
1137        )?;
1138
1139        // Vertex count and channel count.
1140        self.write_u16_at(header_pos, mesh_offsets::VERTEX_COUNT, mesh.vertex_count)?;
1141        self.write_u16_at(
1142            header_pos,
1143            mesh_offsets::TEXTURE_CHANNEL_COUNT,
1144            mesh.texture_channel_count,
1145        )?;
1146
1147        // Boolean flags.
1148        self.write_u8_at(
1149            header_pos,
1150            mesh_offsets::LIGHT_MAPPED,
1151            u8::from(mesh.light_mapped),
1152        )?;
1153        self.write_u8_at(
1154            header_pos,
1155            mesh_offsets::ROTATE_TEXTURE,
1156            u8::from(mesh.rotate_texture),
1157        )?;
1158        self.write_u8_at(
1159            header_pos,
1160            mesh_offsets::IS_BACKGROUND_GEOMETRY,
1161            u8::from(mesh.is_background_geometry),
1162        )?;
1163        self.write_u8_at(header_pos, mesh_offsets::SHADOW, u8::from(mesh.shadow))?;
1164        self.write_u8_at(header_pos, mesh_offsets::BEAMING, u8::from(mesh.beaming))?;
1165        self.write_u8_at(header_pos, mesh_offsets::RENDER, u8::from(mesh.render))?;
1166
1167        // Total surface area.
1168        self.write_f32_at(
1169            header_pos,
1170            mesh_offsets::TOTAL_SURFACE_AREA,
1171            mesh.total_surface_area,
1172        )?;
1173
1174        // MDX layout sentinel defaults for offset slots that are always -1
1175        // in vanilla. The active slots (+0xFC stride, +0x100 flags,
1176        // +0x104..+0x120 attribute offsets) are filled by write_mesh_mdx_data.
1177        // The 3 reserved slots at +0x124..+0x12C are never used by K1 but
1178        // the constructor initializes them to -1.
1179        {
1180            let save = self.buffer.position();
1181            // 8 active attribute offset slots (+0x104..+0x120) default to -1.
1182            // write_mesh_mdx_data overwrites the ones it uses.
1183            self.seek_to(header_pos, mesh_offsets::MDX_POSITION_OFFSET);
1184            for _ in 0..8 {
1185                write_u32(&mut self.buffer, 0xFFFF_FFFF)?;
1186            }
1187            // 3 reserved slots at +0x124..+0x12C: always -1.
1188            for _ in 0..3 {
1189                write_u32(&mut self.buffer, 0xFFFF_FFFF)?;
1190            }
1191            self.buffer.set_position(save);
1192        }
1193
1194        Ok(header_pos)
1195    }
1196
1197    /// Writes a raw CExoArrayList payload and backpatches the ptr/count/alloc
1198    /// Writes face array data and backpatches the mesh header's face pointer.
1199    ///
1200    /// Must be called after all contiguous headers (mesh + subtype) are written,
1201    /// so the array data sits after the header block - matching the binary format
1202    /// layout where headers are contiguous and data follows.
1203    fn write_mesh_arrays(
1204        &mut self,
1205        mesh: &super::MdlMesh,
1206        mesh_header_pos: u64,
1207    ) -> Result<Option<u32>, MdlError> {
1208        let face_count = count_u32(mesh.faces.len(), "face_count")?;
1209        let mut face_offset = 0u32;
1210
1211        if face_count > 0 {
1212            face_offset = self.content_position()?;
1213            for face in &mesh.faces {
1214                write_f32(&mut self.buffer, face.plane_normal[0])?;
1215                write_f32(&mut self.buffer, face.plane_normal[1])?;
1216                write_f32(&mut self.buffer, face.plane_normal[2])?;
1217                write_f32(&mut self.buffer, face.plane_distance)?;
1218                write_u32(&mut self.buffer, face.surface_id)?;
1219                write_u16(&mut self.buffer, face.adjacent[0])?;
1220                write_u16(&mut self.buffer, face.adjacent[1])?;
1221                write_u16(&mut self.buffer, face.adjacent[2])?;
1222                write_u16(&mut self.buffer, face.vertex_indices[0])?;
1223                write_u16(&mut self.buffer, face.vertex_indices[1])?;
1224                write_u16(&mut self.buffer, face.vertex_indices[2])?;
1225            }
1226        }
1227
1228        // Backpatch face CExoArrayList: ptr, count, alloc
1229        let save_end = self.buffer.position();
1230        self.seek_to(mesh_header_pos, mesh_offsets::FACE_ARRAY_OFFSET);
1231        write_u32(&mut self.buffer, face_offset)?;
1232        write_u32(&mut self.buffer, face_count)?;
1233        write_u32(&mut self.buffer, face_count)?; // alloc = count on disk
1234        self.buffer.set_position(save_end);
1235
1236        // --- Write TriMesh internal CExoArrayList data payloads ---
1237        //
1238        // The 5 CExoArrayLists at +0x98..+0xC8:
1239        //   +0x98 vertex_indices - dead in KotOR, always zeros
1240        //   +0xA4 left_over_faces - always empty in vanilla (ptr=0, count=0)
1241        //   +0xB0 vertex_indices_count - single u32 (face_count*3) or embedded positions
1242        //   +0xBC mdx_offsets - single u32 content pointer -> packed u16 face indices
1243        //   +0xC8 index_buffer_pools - single u32 inverted counter
1244        //
1245        // Corpus analysis (76,767 meshes): vertex_indices and left_over_faces
1246        // are ALWAYS ptr=0 in vanilla. Only the last 3 have data payloads.
1247        //
1248        // Write ordering for the 3 active arrays: sort by source pointers when
1249        // available (vanilla-backed), otherwise use default order.
1250        // Packed u16 face indices are written after all CExoArrayList payloads,
1251        // then the mdx_offsets placeholder is backpatched to point to them.
1252        //
1253        // See `docs/src/formats/models/mesh_derived_fields.md` for full documentation.
1254
1255        // +0x98: vertex_indices - dead field, always zeros (already zeroed from init).
1256
1257        // +0xA4: left_over_faces - always empty in vanilla binary files.
1258        self.backpatch_cexolist(
1259            mesh_header_pos,
1260            mesh_offsets::LEFT_OVER_FACES_ARRAY_PTR,
1261            0,
1262            0,
1263            0,
1264        )?;
1265
1266        // Write the 3 active CExoArrayList data payloads in canonical order.
1267        let vertex_count = usize::from(mesh.vertex_count);
1268        let can_embed_positions = mesh.has_embedded_positions
1269            && !mesh.positions.is_empty()
1270            && mesh.positions.len() >= vertex_count;
1271
1272        let mut vertex_indices_count_data_ptr = 0u32;
1273
1274        // +0xB0: vertex_indices_count - single u32 (face_count*3) or embedded positions.
1275        if can_embed_positions {
1276            let ptr = self.content_position()?;
1277            vertex_indices_count_data_ptr = ptr;
1278            write_u32(&mut self.buffer, face_count * 3)?;
1279            for pos in mesh.positions.iter().take(vertex_count) {
1280                write_f32(&mut self.buffer, pos[0])?;
1281                write_f32(&mut self.buffer, pos[1])?;
1282                write_f32(&mut self.buffer, pos[2])?;
1283            }
1284            self.backpatch_cexolist(
1285                mesh_header_pos,
1286                mesh_offsets::VERTEX_INDICES_COUNT_ARRAY_PTR,
1287                ptr,
1288                1,
1289                1,
1290            )?;
1291        } else if face_count > 0 {
1292            let ptr = self.content_position()?;
1293            vertex_indices_count_data_ptr = ptr;
1294            write_u32(&mut self.buffer, face_count * 3)?;
1295            self.backpatch_cexolist(
1296                mesh_header_pos,
1297                mesh_offsets::VERTEX_INDICES_COUNT_ARRAY_PTR,
1298                ptr,
1299                1,
1300                1,
1301            )?;
1302        } else {
1303            self.backpatch_cexolist(
1304                mesh_header_pos,
1305                mesh_offsets::VERTEX_INDICES_COUNT_ARRAY_PTR,
1306                0,
1307                0,
1308                0,
1309            )?;
1310        }
1311
1312        // +0xBC: mdx_offsets - single u32 content pointer to packed u16 face indices.
1313        if face_count > 0 {
1314            let ptr = self.content_position()?;
1315            write_u32(&mut self.buffer, 0)?; // placeholder, backpatched below
1316            self.backpatch_cexolist(
1317                mesh_header_pos,
1318                mesh_offsets::MDX_OFFSETS_ARRAY_PTR,
1319                ptr,
1320                1,
1321                1,
1322            )?;
1323        } else {
1324            self.backpatch_cexolist(
1325                mesh_header_pos,
1326                mesh_offsets::MDX_OFFSETS_ARRAY_PTR,
1327                0,
1328                0,
1329                0,
1330            )?;
1331        }
1332
1333        // +0xC8: index_buffer_pools - inverted counter value.
1334        if mesh.inverted_counter > 0 || face_count > 0 {
1335            let ptr = self.content_position()?;
1336            write_u32(&mut self.buffer, mesh.inverted_counter)?;
1337            self.backpatch_cexolist(
1338                mesh_header_pos,
1339                mesh_offsets::INDEX_BUFFER_POOLS_ARRAY_PTR,
1340                ptr,
1341                1,
1342                1,
1343            )?;
1344        } else {
1345            self.backpatch_cexolist(
1346                mesh_header_pos,
1347                mesh_offsets::INDEX_BUFFER_POOLS_ARRAY_PTR,
1348                0,
1349                0,
1350                0,
1351            )?;
1352        }
1353
1354        // Write packed u16 face indices after all CExoArrayList payloads,
1355        // then backpatch the mdx_offsets placeholder to point here.
1356        if face_count > 0 {
1357            let packed_ptr = self.content_position()?;
1358            for face in &mesh.faces {
1359                write_u16(&mut self.buffer, face.vertex_indices[0])?;
1360                write_u16(&mut self.buffer, face.vertex_indices[1])?;
1361                write_u16(&mut self.buffer, face.vertex_indices[2])?;
1362            }
1363            self.backpatch_mdx_offsets_data(mesh_header_pos, packed_ptr)?;
1364        }
1365
1366        // NOTE: shared_index_offset/pool/size/indices_per_face already written
1367        // in write_mesh_header() - no need to repeat here.
1368
1369        let embedded_pos_ptr = if can_embed_positions && vertex_indices_count_data_ptr > 0 {
1370            Some(vertex_indices_count_data_ptr.saturating_add(4))
1371        } else {
1372            None
1373        };
1374
1375        Ok(embedded_pos_ptr)
1376    }
1377
1378    /// Backpatch a CExoArrayList header (ptr, count, alloc) at a mesh header offset.
1379    fn backpatch_cexolist(
1380        &mut self,
1381        mesh_header_pos: u64,
1382        ptr_field: usize,
1383        ptr: u32,
1384        count: u32,
1385        alloc: u32,
1386    ) -> Result<(), MdlError> {
1387        let save = self.buffer.position();
1388        self.seek_to(mesh_header_pos, ptr_field);
1389        write_u32(&mut self.buffer, ptr)?;
1390        write_u32(&mut self.buffer, count)?;
1391        write_u32(&mut self.buffer, alloc)?;
1392        self.buffer.set_position(save);
1393        Ok(())
1394    }
1395
1396    /// Backpatch the mdx_offsets (+0xBC) data value to point to packed face indices.
1397    ///
1398    /// The mdx_offsets CExoArrayList data is a single u32 containing a
1399    /// content-relative pointer to the packed u16 face vertex indices.
1400    /// The data payload was written as a placeholder 0 during the ordering loop.
1401    fn backpatch_mdx_offsets_data(
1402        &mut self,
1403        mesh_header_pos: u64,
1404        packed_ptr: u32,
1405    ) -> Result<(), MdlError> {
1406        // Read the mdx_offsets data pointer from the header.
1407        let save = self.buffer.position();
1408        self.seek_to(mesh_header_pos, mesh_offsets::MDX_OFFSETS_ARRAY_PTR);
1409        let mut ptr_bytes = [0u8; 4];
1410        std::io::Read::read_exact(&mut self.buffer, &mut ptr_bytes)?;
1411        let data_ptr = u32::from_le_bytes(ptr_bytes);
1412        if data_ptr > 0 {
1413            // Backpatch the placeholder u32 at the data location.
1414            self.buffer
1415                .set_position(u64::from(data_ptr) + MDL_WRAPPER_SIZE);
1416            write_u32(&mut self.buffer, packed_ptr)?;
1417        }
1418        self.buffer.set_position(save);
1419        Ok(())
1420    }
1421
1422    /// Writes position-only vertex data into the MDL content blob and backpatches
1423    /// the mesh header's `mdx_data_offset` field (+0x148) to point to it.
1424    ///
1425    /// The binary format stores position data (12 bytes/vertex = 3×f32) within
1426    /// the MDL content blob, referenced by `mdx_data_offset` which is an MDL
1427    /// content-relative offset (NOT an MDX file offset). The engine relocates
1428    /// this pointer against the MDL content base during Reset.
1429    ///
1430    /// See `docs/src/internals/mdl_deep_dive.md` "What this means for `mdx_data_offset`".
1431    fn write_mesh_content_positions(
1432        &mut self,
1433        mesh: &super::MdlMesh,
1434        mesh_header_pos: u64,
1435        embedded_pos_ptr: Option<u32>,
1436    ) -> Result<(), MdlError> {
1437        if let Some(pos_ptr) = embedded_pos_ptr {
1438            let save = self.buffer.position();
1439            self.seek_to(mesh_header_pos, mesh_offsets::VERT_ARRAY_OFFSET);
1440            write_u32(&mut self.buffer, pos_ptr)?;
1441            self.buffer.set_position(save);
1442            return Ok(());
1443        }
1444
1445        if mesh.positions.is_empty() {
1446            return Ok(());
1447        }
1448
1449        // Write position data into MDL content buffer.
1450        let pos_start = self.content_position()?; // content-relative
1451
1452        for pos in &mesh.positions {
1453            write_f32(&mut self.buffer, pos[0])?;
1454            write_f32(&mut self.buffer, pos[1])?;
1455            write_f32(&mut self.buffer, pos[2])?;
1456        }
1457
1458        // Backpatch vert_array_offset at mesh header +0x148.
1459        let save = self.buffer.position();
1460        self.seek_to(mesh_header_pos, mesh_offsets::VERT_ARRAY_OFFSET);
1461        write_u32(&mut self.buffer, pos_start)?;
1462        self.buffer.set_position(save);
1463
1464        Ok(())
1465    }
1466
1467    /// Writes vertex attribute data into the MDX buffer and backpatches the mesh
1468    /// header's MDX-related fields (stride, flags, attribute offsets).
1469    ///
1470    /// Returns the computed stride (bytes per vertex). Returns 0 if no vertex
1471    /// data was written.
1472    ///
1473    /// The canonical layout order follows the engine's `InternalPostProcess`:
1474    /// position -> normal -> vertex_colors -> UV1 -> UV2 -> UV3 -> UV4 -> tangent_space.
1475    fn write_mesh_mdx_data(
1476        &mut self,
1477        mesh: &super::MdlMesh,
1478        skin: Option<&super::MdlSkin>,
1479        mesh_header_pos: u64,
1480        skin_header_pos: Option<u64>,
1481    ) -> Result<u32, MdlError> {
1482        #[derive(Clone, Copy)]
1483        struct MdxLayout {
1484            stride: u32,
1485            flags: u32,
1486            pos_off: i32,
1487            norm_off: i32,
1488            color_off: i32,
1489            uv1_off: i32,
1490            uv2_off: i32,
1491            uv3_off: i32,
1492            uv4_off: i32,
1493            tangent_off: i32,
1494            bone_weights_off: i32,
1495            bone_indices_off: i32,
1496        }
1497
1498        let vertex_count = usize::from(mesh.vertex_count);
1499        if vertex_count == 0 {
1500            return Ok(0);
1501        }
1502
1503        // Determine the effective vertex count for MDX output.
1504        //
1505        // The mesh header declares `vertex_count` which may be larger than
1506        // the number of vertices there is data for. This happens in vanilla
1507        // K1 item models such as `i_adrnaline_001`, where the MDX offset
1508        // points deep into a shared MDX buffer and the reader truncates when
1509        // data runs out. The writer emits only the vertices with data.
1510        //
1511        // All populated attribute arrays must have the same length - if they
1512        // disagree, that's an actual data error.
1513        let mut effective_vertex_count = vertex_count;
1514        macro_rules! clamp_attr {
1515            ($arr:expr, $name:expr) => {
1516                if !$arr.is_empty() {
1517                    if effective_vertex_count == vertex_count {
1518                        // First populated attribute sets the bound
1519                        effective_vertex_count = $arr.len();
1520                    } else if $arr.len() != effective_vertex_count {
1521                        return Err(MdlError::InvalidData(format!(
1522                            "mesh {}: {} has {} elements but {} has {}",
1523                            $name,
1524                            stringify!($arr),
1525                            $arr.len(),
1526                            "other attribute",
1527                            effective_vertex_count
1528                        )));
1529                    }
1530                }
1531            };
1532        }
1533        clamp_attr!(mesh.positions, "positions");
1534        clamp_attr!(mesh.normals, "normals");
1535        clamp_attr!(mesh.vertex_colors, "vertex_colors");
1536        clamp_attr!(mesh.uv1, "uv1");
1537        clamp_attr!(mesh.uv2, "uv2");
1538        clamp_attr!(mesh.uv3, "uv3");
1539        clamp_attr!(mesh.uv4, "uv4");
1540        clamp_attr!(mesh.tangent_space, "tangent_space");
1541        if let Some(s) = skin {
1542            clamp_attr!(s.bone_weights, "bone_weights");
1543            clamp_attr!(s.bone_indices, "bone_indices");
1544        }
1545
1546        // Determine which attributes are present.
1547        let has_pos = !mesh.positions.is_empty();
1548        let has_norm = !mesh.normals.is_empty();
1549        let has_color = !mesh.vertex_colors.is_empty();
1550        let has_uv1 = !mesh.uv1.is_empty();
1551        let has_uv2 = !mesh.uv2.is_empty();
1552        let has_uv3 = !mesh.uv3.is_empty();
1553        let has_uv4 = !mesh.uv4.is_empty();
1554        let has_tangent = !mesh.tangent_space.is_empty();
1555        let has_bone_weights = skin.is_some_and(|s| !s.bone_weights.is_empty());
1556        let has_bone_indices = skin.is_some_and(|s| !s.bone_indices.is_empty());
1557
1558        // Compute canonical layout: position -> normal -> color -> UV1..4 -> tangent -> bone weights -> bone indices.
1559        // Each attribute gets a byte offset within the per-vertex stride.
1560        let mut current_byte = 0u32;
1561
1562        let pos_off: i32 = if has_pos {
1563            let o = i32::try_from(current_byte)
1564                .map_err(|_| MdlError::ValueOverflow("mdx_attribute_offset"))?;
1565            current_byte += 12;
1566            o
1567        } else {
1568            -1
1569        };
1570        let norm_off: i32 = if has_norm {
1571            let o = i32::try_from(current_byte)
1572                .map_err(|_| MdlError::ValueOverflow("mdx_attribute_offset"))?;
1573            current_byte += 12;
1574            o
1575        } else {
1576            -1
1577        };
1578        let color_off: i32 = if has_color {
1579            let o = i32::try_from(current_byte)
1580                .map_err(|_| MdlError::ValueOverflow("mdx_attribute_offset"))?;
1581            current_byte += 4;
1582            o
1583        } else {
1584            -1
1585        };
1586        let uv1_off: i32 = if has_uv1 {
1587            let o = i32::try_from(current_byte)
1588                .map_err(|_| MdlError::ValueOverflow("mdx_attribute_offset"))?;
1589            current_byte += 8;
1590            o
1591        } else {
1592            -1
1593        };
1594        let uv2_off: i32 = if has_uv2 {
1595            let o = i32::try_from(current_byte)
1596                .map_err(|_| MdlError::ValueOverflow("mdx_attribute_offset"))?;
1597            current_byte += 8;
1598            o
1599        } else {
1600            -1
1601        };
1602        let uv3_off: i32 = if has_uv3 {
1603            let o = i32::try_from(current_byte)
1604                .map_err(|_| MdlError::ValueOverflow("mdx_attribute_offset"))?;
1605            current_byte += 8;
1606            o
1607        } else {
1608            -1
1609        };
1610        let uv4_off: i32 = if has_uv4 {
1611            let o = i32::try_from(current_byte)
1612                .map_err(|_| MdlError::ValueOverflow("mdx_attribute_offset"))?;
1613            current_byte += 8;
1614            o
1615        } else {
1616            -1
1617        };
1618        let tangent_off: i32 = if has_tangent {
1619            let o = i32::try_from(current_byte)
1620                .map_err(|_| MdlError::ValueOverflow("mdx_attribute_offset"))?;
1621            current_byte += 36;
1622            o
1623        } else {
1624            -1
1625        };
1626        let bone_weights_off: i32 = if has_bone_weights {
1627            let o = i32::try_from(current_byte)
1628                .map_err(|_| MdlError::ValueOverflow("mdx_attribute_offset"))?;
1629            current_byte += 16;
1630            o
1631        } else {
1632            -1
1633        };
1634        let bone_indices_off: i32 = if has_bone_indices {
1635            let o = i32::try_from(current_byte)
1636                .map_err(|_| MdlError::ValueOverflow("mdx_attribute_offset"))?;
1637            current_byte += 16;
1638            o
1639        } else {
1640            -1
1641        };
1642
1643        let canonical_stride = current_byte;
1644
1645        // Compute flags (vertex colors have no flag bit - presence is
1646        // determined by offset != -1, per InternalPostProcess evidence).
1647        let mut canonical_flags = 0u32;
1648        if has_pos {
1649            canonical_flags |= 0x01;
1650        }
1651        if has_norm {
1652            canonical_flags |= 0x20;
1653        }
1654        if has_uv1 {
1655            canonical_flags |= 0x02;
1656        }
1657        if has_uv2 {
1658            canonical_flags |= 0x04;
1659        }
1660        if has_uv3 {
1661            canonical_flags |= 0x08;
1662        }
1663        if has_uv4 {
1664            canonical_flags |= 0x10;
1665        }
1666        if has_tangent {
1667            canonical_flags |= 0x80;
1668        }
1669
1670        let layout = MdxLayout {
1671            stride: canonical_stride,
1672            flags: canonical_flags,
1673            pos_off,
1674            norm_off,
1675            color_off,
1676            uv1_off,
1677            uv2_off,
1678            uv3_off,
1679            uv4_off,
1680            tangent_off,
1681            bone_weights_off,
1682            bone_indices_off,
1683        };
1684
1685        if layout.stride == 0 {
1686            return Ok(0); // No attributes to write
1687        }
1688
1689        // Canonical interleaved write: position -> normal -> color -> UV1..4 -> tangent -> bone weights -> bone indices.
1690        for i in 0..effective_vertex_count {
1691            if has_pos {
1692                let p = &mesh.positions[i];
1693                write_f32(&mut self.mdx_buffer, p[0])?;
1694                write_f32(&mut self.mdx_buffer, p[1])?;
1695                write_f32(&mut self.mdx_buffer, p[2])?;
1696            }
1697            if has_norm {
1698                let n = &mesh.normals[i];
1699                write_f32(&mut self.mdx_buffer, n[0])?;
1700                write_f32(&mut self.mdx_buffer, n[1])?;
1701                write_f32(&mut self.mdx_buffer, n[2])?;
1702            }
1703            if has_color {
1704                let c = &mesh.vertex_colors[i];
1705                self.mdx_buffer.write_all(c)?;
1706            }
1707            if has_uv1 {
1708                let u = &mesh.uv1[i];
1709                write_f32(&mut self.mdx_buffer, u[0])?;
1710                write_f32(&mut self.mdx_buffer, u[1])?;
1711            }
1712            if has_uv2 {
1713                let u = &mesh.uv2[i];
1714                write_f32(&mut self.mdx_buffer, u[0])?;
1715                write_f32(&mut self.mdx_buffer, u[1])?;
1716            }
1717            if has_uv3 {
1718                let u = &mesh.uv3[i];
1719                write_f32(&mut self.mdx_buffer, u[0])?;
1720                write_f32(&mut self.mdx_buffer, u[1])?;
1721            }
1722            if has_uv4 {
1723                let u = &mesh.uv4[i];
1724                write_f32(&mut self.mdx_buffer, u[0])?;
1725                write_f32(&mut self.mdx_buffer, u[1])?;
1726            }
1727            if has_tangent {
1728                let t = &mesh.tangent_space[i];
1729                for row in t {
1730                    write_f32(&mut self.mdx_buffer, row[0])?;
1731                    write_f32(&mut self.mdx_buffer, row[1])?;
1732                    write_f32(&mut self.mdx_buffer, row[2])?;
1733                }
1734            }
1735            if has_bone_weights {
1736                // skin is guaranteed Some when has_bone_weights is true.
1737                let bw = &skin
1738                    .expect("has_bone_weights implies skin is Some")
1739                    .bone_weights[i];
1740                write_f32(&mut self.mdx_buffer, bw[0])?;
1741                write_f32(&mut self.mdx_buffer, bw[1])?;
1742                write_f32(&mut self.mdx_buffer, bw[2])?;
1743                write_f32(&mut self.mdx_buffer, bw[3])?;
1744            }
1745            if has_bone_indices {
1746                // skin is guaranteed Some when has_bone_indices is true.
1747                let bi = &skin
1748                    .expect("has_bone_indices implies skin is Some")
1749                    .bone_indices[i];
1750                write_f32(&mut self.mdx_buffer, bi[0])?;
1751                write_f32(&mut self.mdx_buffer, bi[1])?;
1752                write_f32(&mut self.mdx_buffer, bi[2])?;
1753                write_f32(&mut self.mdx_buffer, bi[3])?;
1754            }
1755        }
1756
1757        // Backpatch mesh header with MDX layout fields.
1758        let save = self.buffer.position();
1759
1760        // Stride at +0xFC
1761        self.seek_to(mesh_header_pos, mesh_offsets::VERTEX_STRUCT_SIZE);
1762        write_u32(&mut self.buffer, layout.stride)?;
1763
1764        // Flags at +0x100
1765        self.seek_to(mesh_header_pos, mesh_offsets::MDX_VERTEX_FLAGS);
1766        write_u32(&mut self.buffer, layout.flags)?;
1767
1768        // Per-attribute byte offsets at +0x104..+0x120 (contiguous)
1769        self.seek_to(mesh_header_pos, mesh_offsets::MDX_POSITION_OFFSET);
1770        write_i32(&mut self.buffer, layout.pos_off)?;
1771        write_i32(&mut self.buffer, layout.norm_off)?;
1772        write_i32(&mut self.buffer, layout.color_off)?;
1773        write_i32(&mut self.buffer, layout.uv1_off)?;
1774        write_i32(&mut self.buffer, layout.uv2_off)?;
1775        write_i32(&mut self.buffer, layout.uv3_off)?;
1776        write_i32(&mut self.buffer, layout.uv4_off)?;
1777        write_i32(&mut self.buffer, layout.tangent_off)?;
1778
1779        // NOTE: mdx_data_offset (+0x148) is backpatched by write_deferred_mdx()
1780        // with the MDX file offset, not set here. When not writing MDX, it's
1781        // backpatched by write_mesh_content_positions() with MDL content offset.
1782
1783        // Backpatch skin header bone weight/index offsets with the canonical
1784        // layout values computed above (the initial values from write_skin_header
1785        // may differ when the source model used a different attribute ordering).
1786        if let Some(shp) = skin_header_pos {
1787            self.write_i32_at(
1788                shp,
1789                skin_offsets::MDX_BONE_WEIGHTS_OFFSET,
1790                layout.bone_weights_off,
1791            )?;
1792            self.write_i32_at(
1793                shp,
1794                skin_offsets::MDX_BONE_INDICES_OFFSET,
1795                layout.bone_indices_off,
1796            )?;
1797        }
1798
1799        self.buffer.set_position(save);
1800        Ok(layout.stride)
1801    }
1802
1803    /// Writes a per-mesh terminator row into the MDX buffer.
1804    ///
1805    /// BioWare's build tools emit one stride-sized row of sentinel floats after
1806    /// each mesh's vertex data, followed by zero-padding to the next 16-byte
1807    /// boundary. Non-skin meshes use 10,000,000.0 as the sentinel; skin meshes
1808    /// use 1,000,000.0.
1809    ///
1810    /// See `docs/src/internals/mdl_deep_dive.md` "Per-mesh terminators and alignment".
1811    fn write_mdx_terminator(&mut self, stride: u32, is_skin: bool) -> Result<(), MdlError> {
1812        if stride == 0 {
1813            return Ok(());
1814        }
1815
1816        let stride = usize::try_from(stride).expect("u32 stride fits in usize");
1817        let sentinel: f32 = if is_skin { 1_000_000.0 } else { 10_000_000.0 };
1818
1819        // Write 3× sentinel float (position-like xyz), then zero-fill remaining bytes.
1820        write_f32(&mut self.mdx_buffer, sentinel)?;
1821        write_f32(&mut self.mdx_buffer, sentinel)?;
1822        write_f32(&mut self.mdx_buffer, sentinel)?;
1823
1824        // Fill the rest of the stride with zeros.
1825        let remaining = stride.saturating_sub(12);
1826        if remaining > 0 {
1827            let zeros = vec![0u8; remaining];
1828            self.mdx_buffer.write_all(&zeros)?;
1829        }
1830
1831        // Save position before alignment - the final mesh's padding will be
1832        // trimmed by truncating to this position after all nodes are written.
1833        self.mdx_pre_align_pos = self.mdx_buffer.position();
1834
1835        // Pad to 16-byte alignment (will be trimmed for the last mesh).
1836        let pos =
1837            usize::try_from(self.mdx_buffer.position()).expect("mdx buffer position fits in usize");
1838        let aligned = (pos + 15) & !15;
1839        let pad = aligned - pos;
1840        if pad > 0 {
1841            let zeros = vec![0u8; pad];
1842            self.mdx_buffer.write_all(&zeros)?;
1843        }
1844
1845        Ok(())
1846    }
1847
1848    /// Writes the 100-byte Skin extra header from typed fields.
1849    ///
1850    /// Emits 100 zero bytes then backpatches the inline scalar fields:
1851    /// MDX bone weight/index offsets (+0x0C/+0x10) and bone_node_numbers (+0x40).
1852    /// CExoArrayList pointers are backpatched later by [`write_skin_arrays`].
1853    fn write_skin_header(&mut self, skin: &super::MdlSkin) -> Result<u64, MdlError> {
1854        let header_pos = self.buffer.position();
1855        self.buffer.write_all(&[0u8; SKIN_EXTRA_SIZE])?;
1856
1857        let save = self.buffer.position();
1858
1859        // MDX bone weight/index offsets at +0x0C/+0x10.
1860        self.write_i32_at(
1861            header_pos,
1862            skin_offsets::MDX_BONE_WEIGHTS_OFFSET,
1863            skin.mdx_bone_weights_offset,
1864        )?;
1865        self.write_i32_at(
1866            header_pos,
1867            skin_offsets::MDX_BONE_INDICES_OFFSET,
1868            skin.mdx_bone_indices_offset,
1869        )?;
1870
1871        // bone_node_numbers: 16 × u16 at +0x40.
1872        self.seek_to(header_pos, skin_offsets::BONE_NODE_NUMBERS);
1873        for &n in &skin.bone_node_numbers {
1874            write_u16(&mut self.buffer, n)?;
1875        }
1876
1877        // +0x60..+0x63: Padding - write zeros (vanilla has leaked pointers here
1878        // in ~74 models, but the engine never reads this field).
1879
1880        self.buffer.set_position(save);
1881        Ok(header_pos)
1882    }
1883
1884    /// Writes the skin data arrays and backpatches the skin header pointers.
1885    fn write_skin_arrays(
1886        &mut self,
1887        skin: &super::MdlSkin,
1888        skin_header_pos: u64,
1889    ) -> Result<(), MdlError> {
1890        // Write bonemap (+0x14 pointer, +0x18 count).
1891        let bonemap_count = count_u32(skin.bonemap.len(), "bonemap_count")?;
1892        let mut bonemap_ptr = 0u32;
1893        if bonemap_count > 0 {
1894            bonemap_ptr = self.content_position()?;
1895            for &idx in &skin.bonemap {
1896                write_u32(&mut self.buffer, idx)?;
1897            }
1898        }
1899
1900        // Write qbone_ref_inv (Quaternion = 4 × f32 each)
1901        let qbone_count = count_u32(skin.qbone_ref_inv.len(), "qbone_count")?;
1902        let mut qbone_ptr = 0u32;
1903        if qbone_count > 0 {
1904            qbone_ptr = self.content_position()?;
1905            for q in &skin.qbone_ref_inv {
1906                write_f32(&mut self.buffer, q[0])?;
1907                write_f32(&mut self.buffer, q[1])?;
1908                write_f32(&mut self.buffer, q[2])?;
1909                write_f32(&mut self.buffer, q[3])?;
1910            }
1911        }
1912
1913        // Write tbone_ref_inv (Vector = 3 × f32 each)
1914        let tbone_count = count_u32(skin.tbone_ref_inv.len(), "tbone_count")?;
1915        let mut tbone_ptr = 0u32;
1916        if tbone_count > 0 {
1917            tbone_ptr = self.content_position()?;
1918            for t in &skin.tbone_ref_inv {
1919                write_f32(&mut self.buffer, t[0])?;
1920                write_f32(&mut self.buffer, t[1])?;
1921                write_f32(&mut self.buffer, t[2])?;
1922            }
1923        }
1924
1925        // Write bone_constant_indices (i32 each)
1926        let bone_idx_count = count_u32(skin.bone_constant_indices.len(), "bone_idx_count")?;
1927        let mut bone_idx_ptr = 0u32;
1928        if bone_idx_count > 0 {
1929            bone_idx_ptr = self.content_position()?;
1930            for &idx in &skin.bone_constant_indices {
1931                write_i32(&mut self.buffer, idx)?;
1932            }
1933        }
1934
1935        // Backpatch CExoArrayList headers: ptr, count, alloc
1936        let save_end = self.buffer.position();
1937
1938        // Weights CExoArrayList at +0x00: always zeros in binary files (already zeroed from init).
1939
1940        // bonemap pointer/count at +0x14/+0x18
1941        self.seek_to(skin_header_pos, skin_offsets::BONEMAP_PTR);
1942        write_u32(&mut self.buffer, bonemap_ptr)?;
1943        write_u32(&mut self.buffer, bonemap_count)?;
1944
1945        // qbone_ref_inv at skin extra +0x1C
1946        self.seek_to(skin_header_pos, skin_offsets::QBONE_REF_INV_PTR);
1947        write_u32(&mut self.buffer, qbone_ptr)?;
1948        write_u32(&mut self.buffer, qbone_count)?;
1949        write_u32(&mut self.buffer, qbone_count)?; // alloc = count on disk
1950
1951        // tbone_ref_inv at skin extra +0x28
1952        self.seek_to(skin_header_pos, skin_offsets::TBONE_REF_INV_PTR);
1953        write_u32(&mut self.buffer, tbone_ptr)?;
1954        write_u32(&mut self.buffer, tbone_count)?;
1955        write_u32(&mut self.buffer, tbone_count)?; // alloc = count on disk
1956
1957        // bone_constant_indices at skin extra +0x34
1958        self.seek_to(skin_header_pos, skin_offsets::BONE_CONSTANT_INDICES_PTR);
1959        write_u32(&mut self.buffer, bone_idx_ptr)?;
1960        write_u32(&mut self.buffer, bone_idx_count)?;
1961        write_u32(&mut self.buffer, bone_idx_count)?; // alloc = count on disk
1962
1963        self.buffer.set_position(save_end);
1964        Ok(())
1965    }
1966
1967    /// Writes the 56-byte AnimMesh extra header from typed fields.
1968    ///
1969    /// Returns the header position for deferred backpatching of the two
1970    /// CExoArrayList pointers by [`write_anim_mesh_arrays`].
1971    fn write_anim_mesh_header(&mut self, anim: &super::MdlAnimMesh) -> Result<u64, MdlError> {
1972        let header_pos = self.buffer.position();
1973
1974        // Write 56 zero bytes as base, then backpatch typed fields.
1975        self.buffer.write_all(&[0u8; ANIM_MESH_EXTRA_SIZE])?;
1976        let save = self.buffer.position();
1977
1978        // sample_period at +0x00
1979        self.write_f32_at(
1980            header_pos,
1981            anim_mesh_offsets::SAMPLE_PERIOD,
1982            anim.sample_period,
1983        )?;
1984        // CExoArrayList pointers at +0x04 and +0x10 are backpatched by write_anim_mesh_arrays.
1985
1986        // Runtime-only fields at +0x1C..+0x37 (always zero in authored files,
1987        // preserved for roundtrip fidelity).
1988        self.write_u32_at(header_pos, anim_mesh_offsets::DATA_PTR_1, anim.data_ptr_1)?;
1989        self.write_u32_at(
1990            header_pos,
1991            anim_mesh_offsets::DATA_COUNT_1,
1992            anim.data_count_1,
1993        )?;
1994        self.write_u32_at(header_pos, anim_mesh_offsets::PADDING_24, anim.padding_24)?;
1995        self.write_u32_at(
1996            header_pos,
1997            anim_mesh_offsets::ANIM_VERTICES_PTR,
1998            anim.anim_vertices_ptr,
1999        )?;
2000        self.write_u32_at(
2001            header_pos,
2002            anim_mesh_offsets::ANIM_TEX_VERTICES_PTR,
2003            anim.anim_tex_vertices_ptr,
2004        )?;
2005        self.write_u32_at(
2006            header_pos,
2007            anim_mesh_offsets::ANIM_VERTICES_COUNT,
2008            anim.anim_vertices_count,
2009        )?;
2010        self.write_u32_at(
2011            header_pos,
2012            anim_mesh_offsets::ANIM_TEX_VERTICES_COUNT,
2013            anim.anim_tex_vertices_count,
2014        )?;
2015
2016        self.buffer.set_position(save);
2017        Ok(header_pos)
2018    }
2019
2020    /// Writes the two animated vertex arrays and backpatches the anim header pointers.
2021    fn write_anim_mesh_arrays(
2022        &mut self,
2023        anim: &super::MdlAnimMesh,
2024        anim_header_pos: u64,
2025    ) -> Result<(), MdlError> {
2026        // Write anim_verts (Vector = 3 × f32 each)
2027        let anim_verts_count = count_u32(anim.anim_verts.len(), "anim_verts_count")?;
2028        let mut anim_verts_ptr = 0u32;
2029        if anim_verts_count > 0 {
2030            anim_verts_ptr = self.content_position()?;
2031            for v in &anim.anim_verts {
2032                write_f32(&mut self.buffer, v[0])?;
2033                write_f32(&mut self.buffer, v[1])?;
2034                write_f32(&mut self.buffer, v[2])?;
2035            }
2036        }
2037
2038        // Write anim_t_verts (Vector = 3 × f32 each)
2039        let anim_t_verts_count = count_u32(anim.anim_t_verts.len(), "anim_t_verts_count")?;
2040        let mut anim_t_verts_ptr = 0u32;
2041        if anim_t_verts_count > 0 {
2042            anim_t_verts_ptr = self.content_position()?;
2043            for v in &anim.anim_t_verts {
2044                write_f32(&mut self.buffer, v[0])?;
2045                write_f32(&mut self.buffer, v[1])?;
2046                write_f32(&mut self.buffer, v[2])?;
2047            }
2048        }
2049
2050        // Backpatch CExoArrayList headers: ptr, count, alloc
2051        let save_end = self.buffer.position();
2052
2053        // anim_verts at anim extra +0x04
2054        self.seek_to(anim_header_pos, anim_mesh_offsets::ANIM_VERTS_PTR);
2055        write_u32(&mut self.buffer, anim_verts_ptr)?;
2056        write_u32(&mut self.buffer, anim_verts_count)?;
2057        write_u32(&mut self.buffer, anim_verts_count)?; // alloc = count on disk
2058
2059        // anim_t_verts at anim extra +0x10
2060        self.seek_to(anim_header_pos, anim_mesh_offsets::ANIM_T_VERTS_PTR);
2061        write_u32(&mut self.buffer, anim_t_verts_ptr)?;
2062        write_u32(&mut self.buffer, anim_t_verts_count)?;
2063        write_u32(&mut self.buffer, anim_t_verts_count)?; // alloc = count on disk
2064
2065        self.buffer.set_position(save_end);
2066        Ok(())
2067    }
2068
2069    fn write_controllers(
2070        &mut self,
2071        controllers: &[MdlController],
2072        orphan_data: &[f32],
2073    ) -> Result<(u32, u32, u32, u32), MdlError> {
2074        if controllers.is_empty() && orphan_data.is_empty() {
2075            return Ok((0, 0, 0, 0));
2076        }
2077
2078        // Orphan data: key_count=0 but data_count>0. Write the data array
2079        // only so the engine sees the same data_ptr/data_count header values.
2080        if controllers.is_empty() {
2081            let data_ptr = self.content_position()?;
2082            for &val in orphan_data {
2083                write_f32(&mut self.buffer, val)?;
2084            }
2085            return Ok((
2086                0,
2087                0,
2088                data_ptr,
2089                count_u32(orphan_data.len(), "orphan_data_count")?,
2090            ));
2091        }
2092
2093        let key_ptr = self.content_position()?;
2094        let key_count = count_u32(controllers.len(), "controller_count")?;
2095
2096        // Reserve headers
2097        let headers_start = self.buffer.position();
2098        self.buffer.write_all(&vec![
2099            0u8;
2100            usize::try_from(key_count)
2101                .expect("key_count fits in usize")
2102                * 16
2103        ])?;
2104
2105        // Build flat data array
2106        let mut master_float_data = Vec::new();
2107        let mut controller_indices = Vec::new(); // (time_idx, data_idx)
2108
2109        for c in controllers {
2110            let time_idx = master_float_data.len();
2111            for key in &c.keys {
2112                master_float_data.push(key.time);
2113            }
2114
2115            let data_idx = master_float_data.len();
2116            for key in &c.keys {
2117                master_float_data.extend_from_slice(&key.values);
2118            }
2119
2120            controller_indices.push((time_idx, data_idx));
2121        }
2122
2123        // Write Float Data
2124        let data_start_pos = headers_start + u64::from(key_count) * 16;
2125        self.buffer.set_position(data_start_pos);
2126
2127        let real_data_ptr = u32::try_from(
2128            data_start_pos
2129                .checked_sub(MDL_WRAPPER_SIZE)
2130                .ok_or(MdlError::ValueOverflow("ctrl_data_position underflow"))?,
2131        )
2132        .map_err(|_| MdlError::ValueOverflow("ctrl_data_position"))?;
2133
2134        for val in &master_float_data {
2135            write_f32(&mut self.buffer, *val)?;
2136        }
2137        let real_data_count = count_u32(master_float_data.len(), "controller_data_count")?;
2138
2139        // Fill Headers
2140        let final_end = self.buffer.position();
2141        self.buffer.set_position(headers_start);
2142
2143        for (i, c) in controllers.iter().enumerate() {
2144            let row_count = count_u16(c.keys.len(), "key_row_count")?;
2145            let (t_idx, d_idx) = controller_indices[i];
2146
2147            write_u32(&mut self.buffer, c.controller_type.raw())?; // 0x00
2148
2149            // 0x04: Unknown(2) + RowCount(2)
2150            self.buffer.write_all(&c.key_unknown_04)?;
2151            write_u16(&mut self.buffer, row_count)?;
2152
2153            // 0x08: TimeIndex(2) + DataIndex(2)
2154            write_u16(
2155                &mut self.buffer,
2156                u16::try_from(t_idx).map_err(|_| MdlError::ValueOverflow("time_index"))?,
2157            )?;
2158            write_u16(
2159                &mut self.buffer,
2160                u16::try_from(d_idx).map_err(|_| MdlError::ValueOverflow("data_index"))?,
2161            )?;
2162
2163            // 0x0C: ColumnCount(1) + Unknown(3)
2164            // Use the raw column_count byte (preserves Bezier flag and
2165            // integral orientation encoding) rather than deriving from
2166            // values.len(), which would lose the flag bits.
2167            self.buffer.write_all(&[c.raw_column_count])?;
2168            self.buffer.write_all(&c.key_unknown_0d)?;
2169        }
2170
2171        self.buffer.set_position(final_end);
2172
2173        Ok((key_ptr, key_count, real_data_ptr, real_data_count))
2174    }
2175
2176    /// Writes a Light node header (92 bytes) with placeholder CExoArray pointers.
2177    ///
2178    /// Returns the header start position for backpatching by `write_light_arrays`.
2179    /// The 5 CExoArrayList headers at +0x04..+0x3F are written as zeros here;
2180    /// the actual data pointers are backpatched after array payloads are emitted.
2181    fn write_light_header(&mut self, light: &super::MdlLight) -> Result<u64, MdlError> {
2182        let header_pos = self.buffer.position();
2183
2184        // Write 92 bytes of zeros as base, then backpatch typed fields.
2185        self.buffer.write_all(&[0u8; LIGHT_EXTRA_SIZE])?;
2186
2187        // flare_radius: f32 at +0x00
2188        self.write_f32_at(header_pos, light_offsets::FLARE_RADIUS, light.flare_radius)?;
2189
2190        // texture_safe_ptrs: 3×u32 at +0x04 (runtime-only, preserved for roundtrip)
2191        let sp_off = light_offsets::TEXTURE_SAFE_PTRS_PTR;
2192        self.write_u32_at(header_pos, sp_off, light.texture_safe_ptrs[0])?;
2193        self.write_u32_at(header_pos, sp_off + 4, light.texture_safe_ptrs[1])?;
2194        self.write_u32_at(header_pos, sp_off + 8, light.texture_safe_ptrs[2])?;
2195
2196        // CExoArrayList headers at +0x10..+0x3F left as zeros (backpatched in write_light_arrays)
2197
2198        // Scalar fields at +0x40..+0x5C
2199        self.write_i32_at(header_pos, light_offsets::PRIORITY, light.priority)?;
2200        self.write_i32_at(
2201            header_pos,
2202            light_offsets::NUM_DYNAMIC_TYPES,
2203            light.num_dynamic_types,
2204        )?;
2205        self.write_i32_at(
2206            header_pos,
2207            light_offsets::AFFECTDYNAMIC,
2208            light.affectdynamic,
2209        )?;
2210        self.write_i32_at(header_pos, light_offsets::SHADOW, light.shadow)?;
2211        self.write_i32_at(header_pos, light_offsets::AMBIENTONLY, light.ambientonly)?;
2212        self.write_i32_at(
2213            header_pos,
2214            light_offsets::GENERATEFLARE,
2215            light.generateflare,
2216        )?;
2217        self.write_i32_at(header_pos, light_offsets::FADING_LIGHT, light.fading_light)?;
2218
2219        Ok(header_pos)
2220    }
2221
2222    /// Writes Light flare data arrays and backpatches CExoArrayList pointers.
2223    ///
2224    /// Four CExoArrayList payloads are written:
2225    /// - flare_sizes: `Vec<f32>` (4 bytes each)
2226    /// - flare_positions: `Vec<f32>` (4 bytes each)
2227    /// - flare_color_shifts: `Vec<[f32; 3]>` (12 bytes each)
2228    /// - flare_texture_names: `Vec<String>` as pointer-to-pointer chain
2229    ///
2230    /// Each payload writes data, then backpatches the CExoArrayList header
2231    /// (ptr, count, alloc=count) in the light extra header.
2232    fn write_light_arrays(
2233        &mut self,
2234        light: &super::MdlLight,
2235        header_pos: u64,
2236    ) -> Result<(), MdlError> {
2237        // Helper: write a CExoArrayList header (ptr + count + alloc) into the light header.
2238        let write_cexo_header = |writer: &mut Self,
2239                                 ptr_offset: usize,
2240                                 data_ptr: u32,
2241                                 count: u32|
2242         -> Result<(), MdlError> {
2243            writer.write_u32_at(header_pos, ptr_offset, data_ptr)?;
2244            writer.write_u32_at(header_pos, ptr_offset + 4, count)?;
2245            // alloc = count (ptr_offset + 8)
2246            writer.write_u32_at(header_pos, ptr_offset + 8, count)?;
2247            Ok(())
2248        };
2249
2250        // Flare sizes: CExoArrayList<float> at +0x10
2251        let count = count_u32(light.flare_sizes.len(), "flare_sizes_count")?;
2252        if count > 0 {
2253            let data_ptr = self.content_position()?; // content-relative
2254            for &size in &light.flare_sizes {
2255                write_f32(&mut self.buffer, size)?;
2256            }
2257            write_cexo_header(self, light_offsets::FLARE_SIZES_PTR, data_ptr, count)?;
2258        }
2259
2260        // Flare positions: CExoArrayList<float> at +0x1C
2261        let count = count_u32(light.flare_positions.len(), "flare_positions_count")?;
2262        if count > 0 {
2263            let data_ptr = self.content_position()?; // content-relative
2264            for &pos in &light.flare_positions {
2265                write_f32(&mut self.buffer, pos)?;
2266            }
2267            write_cexo_header(self, light_offsets::FLARE_POSITIONS_PTR, data_ptr, count)?;
2268        }
2269
2270        // Flare color shifts: CExoArrayList<Vector> at +0x28
2271        let count = count_u32(light.flare_color_shifts.len(), "flare_color_shifts_count")?;
2272        if count > 0 {
2273            let data_ptr = self.content_position()?; // content-relative
2274            for shift in &light.flare_color_shifts {
2275                write_f32(&mut self.buffer, shift[0])?;
2276                write_f32(&mut self.buffer, shift[1])?;
2277                write_f32(&mut self.buffer, shift[2])?;
2278            }
2279            write_cexo_header(self, light_offsets::FLARE_COLOR_SHIFTS_PTR, data_ptr, count)?;
2280        }
2281
2282        // Flare texture names: CExoArrayList<char*> at +0x34
2283        // Each entry is a u32 content-relative pointer to a null-terminated string.
2284        let count = count_u32(light.flare_texture_names.len(), "flare_tex_names_count")?;
2285        if count > 0 {
2286            // Phase 1: Write the pointer array (u32 per name)
2287            let ptr_array_pos = self.buffer.position();
2288            let ptr_array_content_rel = self.content_position()?;
2289            // Write placeholder pointers
2290            for _ in 0..count {
2291                write_u32(&mut self.buffer, 0)?;
2292            }
2293
2294            // Phase 2: Write each string and backpatch its pointer
2295            for (i, name) in light.flare_texture_names.iter().enumerate() {
2296                let str_ptr = self.content_position()?; // content-relative
2297                                                        // Write null-terminated string
2298                self.buffer.write_all(name.as_bytes())?;
2299                self.buffer.write_all(&[0])?; // null terminator
2300
2301                // Backpatch the pointer
2302                let save = self.buffer.position();
2303                self.seek_to(ptr_array_pos, i * 4);
2304                write_u32(&mut self.buffer, str_ptr)?;
2305                self.buffer.set_position(save);
2306            }
2307
2308            write_cexo_header(
2309                self,
2310                light_offsets::FLARE_TEX_NAMES_PTR,
2311                ptr_array_content_rel,
2312                count,
2313            )?;
2314        }
2315
2316        Ok(())
2317    }
2318
2319    /// Writes a Reference node header (36 bytes).
2320    fn write_reference_header(&mut self, reference: &super::MdlReference) -> Result<(), MdlError> {
2321        // ref_model: char[32] at +0x00
2322        self.write_fixed_string(&reference.ref_model, 32)?;
2323        // reattachable: i32 at +0x20
2324        write_i32(&mut self.buffer, reference.reattachable)?;
2325        Ok(())
2326    }
2327
2328    /// Writes an Emitter node header (224 bytes).
2329    fn write_emitter_header(&mut self, emitter: &super::MdlEmitter) -> Result<(), MdlError> {
2330        // Scalars: +0x00..+0x20 (8 fields × 4 bytes = 32 bytes)
2331        write_f32(&mut self.buffer, emitter.deadspace)?;
2332        write_f32(&mut self.buffer, emitter.blast_radius)?;
2333        write_f32(&mut self.buffer, emitter.blast_length)?;
2334        write_i32(&mut self.buffer, emitter.num_branches)?;
2335        write_i32(&mut self.buffer, emitter.control_pt_smoothing)?;
2336        write_i32(&mut self.buffer, emitter.x_grid)?;
2337        write_i32(&mut self.buffer, emitter.y_grid)?;
2338        write_i32(&mut self.buffer, emitter.spawn_type)?;
2339
2340        // Strings: 4 × char[32] + 1 × char[16] = 144 bytes
2341        self.write_fixed_string(&emitter.update, 32)?; // +0x20
2342        self.write_fixed_string(&emitter.render, 32)?; // +0x40
2343        self.write_fixed_string(&emitter.blend, 32)?; // +0x60
2344        self.write_fixed_string(&emitter.texture, 32)?; // +0x80
2345        self.write_fixed_string(&emitter.chunk_name, 16)?; // +0xA0
2346
2347        // Trailing scalars: +0xB0..+0xBB (10 bytes)
2348        write_i32(&mut self.buffer, emitter.two_sided_tex)?;
2349        write_i32(&mut self.buffer, emitter.loop_emitter)?;
2350        write_u16(&mut self.buffer, emitter.render_order)?;
2351        self.buffer
2352            .write_all(&[if emitter.frame_blending { 1 } else { 0 }])?;
2353
2354        // depth_texture_name: char[16] at +0xBB
2355        self.write_fixed_string(&emitter.depth_texture_name, 16)?;
2356
2357        // Reserved: 21 bytes at +0xCB..+0xE0 - write verbatim
2358        self.buffer.write_all(&emitter.reserved)?;
2359
2360        Ok(())
2361    }
2362
2363    /// Writes the 28-byte DanglyMesh extra header (inline scalars only).
2364    ///
2365    /// Returns the header position for deferred backpatching of the constraint
2366    /// array pointer by [`write_dangly_arrays`].
2367    fn write_dangly_header(&mut self, dangly: &super::MdlDangly) -> Result<u64, MdlError> {
2368        let header_pos = self.buffer.position();
2369
2370        // Write 28 zero bytes as base, then fill typed fields.
2371        // CExoArrayList (ptr/count/alloc at +0x00) and data pointer (+0x18)
2372        // are backpatched by write_dangly_arrays and write_dangly_pre_mesh_payload.
2373        self.buffer.write_all(&[0u8; DANGLY_EXTRA_SIZE])?;
2374
2375        let save = self.buffer.position();
2376        self.seek_to(header_pos, dangly_offsets::DISPLACEMENT);
2377        write_f32(&mut self.buffer, dangly.displacement)?;
2378        write_f32(&mut self.buffer, dangly.tightness)?;
2379        write_f32(&mut self.buffer, dangly.period)?;
2380        self.buffer.set_position(save);
2381
2382        Ok(header_pos)
2383    }
2384
2385    /// Writes the dangly per-vertex positions (+0x18 pointer) before TriMesh arrays.
2386    ///
2387    /// Emits `vertex_count` vec3 positions (12 bytes each), then backpatches the
2388    /// content-relative pointer at dangly extra +0x18.
2389    fn write_dangly_pre_mesh_payload(
2390        &mut self,
2391        dangly: &super::MdlDangly,
2392        dangly_header_pos: u64,
2393    ) -> Result<(), MdlError> {
2394        let ptr = if !dangly.dangly_vertices.is_empty() {
2395            let p = self.content_position()?;
2396            for v in &dangly.dangly_vertices {
2397                write_f32(&mut self.buffer, v[0])?;
2398                write_f32(&mut self.buffer, v[1])?;
2399                write_f32(&mut self.buffer, v[2])?;
2400            }
2401            p
2402        } else {
2403            0
2404        };
2405
2406        self.write_u32_at(dangly_header_pos, dangly_offsets::DATA_PTR, ptr)?;
2407        Ok(())
2408    }
2409
2410    /// Writes the constraint float array and backpatches the dangly header pointer.
2411    fn write_dangly_arrays(
2412        &mut self,
2413        dangly: &super::MdlDangly,
2414        dangly_header_pos: u64,
2415    ) -> Result<(), MdlError> {
2416        let constraints_count = count_u32(dangly.constraints.len(), "constraints_count")?;
2417        let mut constraints_ptr = 0u32;
2418
2419        if constraints_count > 0 {
2420            constraints_ptr = self.content_position()?;
2421            for &val in &dangly.constraints {
2422                write_f32(&mut self.buffer, val)?;
2423            }
2424        }
2425
2426        // Backpatch CExoArrayList: ptr, count, alloc
2427        let save_end = self.buffer.position();
2428        self.seek_to(dangly_header_pos, dangly_offsets::CONSTRAINTS_PTR);
2429        write_u32(&mut self.buffer, constraints_ptr)?;
2430        write_u32(&mut self.buffer, constraints_count)?;
2431        write_u32(&mut self.buffer, constraints_count)?; // alloc = count on disk
2432        self.buffer.set_position(save_end);
2433
2434        Ok(())
2435    }
2436
2437    /// Writes the 4-byte AABB extra header (placeholder for tree root pointer).
2438    ///
2439    /// The root pointer is backpatched by [`write_aabb_arrays`] after the tree
2440    /// payload has been emitted.
2441    fn write_aabb_header(&mut self) -> Result<u64, MdlError> {
2442        let header_pos = self.buffer.position();
2443        self.buffer.write_all(&[0u8; AABB_EXTRA_SIZE])?;
2444        Ok(header_pos)
2445    }
2446
2447    /// Writes the AABB tree payload and backpatches the root pointer in the header.
2448    fn write_aabb_arrays(
2449        &mut self,
2450        aabb: &super::MdlAabb,
2451        aabb_header_pos: u64,
2452    ) -> Result<(), MdlError> {
2453        let ptr = if let Some(tree) = &aabb.aabb_tree {
2454            self.write_aabb_tree(tree)?
2455        } else {
2456            0
2457        };
2458
2459        let save = self.buffer.position();
2460        self.seek_to(aabb_header_pos, aabb_offsets::TREE_PTR);
2461        write_u32(&mut self.buffer, ptr)?;
2462        self.buffer.set_position(save);
2463        Ok(())
2464    }
2465
2466    /// Recursively writes an AABB tree node in DFS preorder (matching mdledit).
2467    ///
2468    /// Returns the content-relative offset of the written node. Child pointers
2469    /// are backpatched after each subtree is written.
2470    fn write_aabb_tree(&mut self, node: &super::AabbNode) -> Result<u32, MdlError> {
2471        let node_ptr = self.content_position()?; // content-relative
2472
2473        // Bounding box (24 bytes)
2474        for &v in &node.box_min {
2475            write_f32(&mut self.buffer, v)?;
2476        }
2477        for &v in &node.box_max {
2478            write_f32(&mut self.buffer, v)?;
2479        }
2480
2481        // Placeholder child pointers: right at +0x18, left at +0x1C
2482        let right_ph = self.buffer.position();
2483        write_u32(&mut self.buffer, 0)?;
2484        let left_ph = self.buffer.position();
2485        write_u32(&mut self.buffer, 0)?;
2486
2487        // Face index and split direction flags
2488        write_i32(&mut self.buffer, node.face_index)?;
2489        write_u32(&mut self.buffer, node.split_direction_flags)?;
2490
2491        // Recurse left child, backpatch pointer
2492        if let Some(left) = &node.left {
2493            let left_ptr = self.write_aabb_tree(left)?;
2494            let save = self.buffer.position();
2495            self.buffer.set_position(left_ph);
2496            write_u32(&mut self.buffer, left_ptr)?;
2497            self.buffer.set_position(save);
2498        }
2499
2500        // Recurse right child, backpatch pointer
2501        if let Some(right) = &node.right {
2502            let right_ptr = self.write_aabb_tree(right)?;
2503            let save = self.buffer.position();
2504            self.buffer.set_position(right_ph);
2505            write_u32(&mut self.buffer, right_ptr)?;
2506            self.buffer.set_position(save);
2507        }
2508
2509        Ok(node_ptr)
2510    }
2511
2512    /// Writes the 20-byte Saber extra header from typed fields.
2513    ///
2514    /// Data pointers are placeholders (backpatched by [`write_saber_arrays`]).
2515    /// GL pool IDs are preserved for roundtrip fidelity (runtime-only values).
2516    fn write_saber_header(&mut self, saber: &super::MdlSaber) -> Result<u64, MdlError> {
2517        let header_pos = self.buffer.position();
2518
2519        // Write 20 zero bytes as base, then backpatch GL pool IDs.
2520        self.buffer.write_all(&[0u8; SABER_EXTRA_SIZE])?;
2521        let save = self.buffer.position();
2522
2523        // Data pointers at +0x00, +0x04, +0x08 are backpatched by write_saber_arrays.
2524        self.write_u32_at(header_pos, saber_offsets::GL_POOL_VERT, saber.gl_pool_vert)?;
2525        self.write_u32_at(
2526            header_pos,
2527            saber_offsets::GL_POOL_INDEX,
2528            saber.gl_pool_index,
2529        )?;
2530
2531        self.buffer.set_position(save);
2532        Ok(header_pos)
2533    }
2534
2535    /// Writes the three saber vertex arrays and backpatches header pointers.
2536    fn write_saber_arrays(
2537        &mut self,
2538        saber: &super::MdlSaber,
2539        saber_header_pos: u64,
2540    ) -> Result<(), MdlError> {
2541        // Write saber_verts (vec3 × N)
2542        let verts_ptr = if !saber.saber_verts.is_empty() {
2543            let p = self.content_position()?;
2544            for v in &saber.saber_verts {
2545                write_f32(&mut self.buffer, v[0])?;
2546                write_f32(&mut self.buffer, v[1])?;
2547                write_f32(&mut self.buffer, v[2])?;
2548            }
2549            p
2550        } else {
2551            0
2552        };
2553
2554        // Write saber_uvs (vec2 × N)
2555        let uvs_ptr = if !saber.saber_uvs.is_empty() {
2556            let p = self.content_position()?;
2557            for v in &saber.saber_uvs {
2558                write_f32(&mut self.buffer, v[0])?;
2559                write_f32(&mut self.buffer, v[1])?;
2560            }
2561            p
2562        } else {
2563            0
2564        };
2565
2566        // Write saber_normals (vec3 × N)
2567        let normals_ptr = if !saber.saber_normals.is_empty() {
2568            let p = self.content_position()?;
2569            for v in &saber.saber_normals {
2570                write_f32(&mut self.buffer, v[0])?;
2571                write_f32(&mut self.buffer, v[1])?;
2572                write_f32(&mut self.buffer, v[2])?;
2573            }
2574            p
2575        } else {
2576            0
2577        };
2578
2579        // Backpatch data pointers in the saber header.
2580        self.write_u32_at(saber_header_pos, saber_offsets::VERTS_PTR, verts_ptr)?;
2581        self.write_u32_at(saber_header_pos, saber_offsets::UVS_PTR, uvs_ptr)?;
2582        self.write_u32_at(saber_header_pos, saber_offsets::NORMALS_PTR, normals_ptr)?;
2583
2584        Ok(())
2585    }
2586
2587    /// Writes a null-terminated string into a fixed-size field, zero-padded.
2588    fn write_fixed_string(&mut self, s: &str, field_size: usize) -> Result<(), MdlError> {
2589        crate::binary::write_fixed_c_string(&mut self.buffer, s, field_size)?;
2590        Ok(())
2591    }
2592}