rakata_formats/mdl/
mod.rs

1//! MDL binary model reader and writer.
2//!
3//! MDL is the 3D model format used by the Odyssey engine (KotOR/KotOR2).
4//! Each model consists of two files: an MDL file containing the node tree,
5//! controllers, and metadata, and a companion MDX file containing
6//! interleaved per-vertex attribute data.
7//!
8//! The binary variant uses a 12-byte wrapper followed by a memory-mapped
9//! content blob. All internal offsets are content-relative (byte 0 = wrapper
10//! end = on-disk byte 12).
11//!
12//! ## File Layout
13//! ```text
14//! MDL file:                             MDX file:
15//! +------------------------------+      +------------------------------+
16//! | Wrapper (12 bytes)           |      | Mesh 1 vertex data           |
17//! | - zero_marker (u32)          |      | (stride * vertex_count bytes)|
18//! | - mdl_content_size (u32)     |      | + terminator (1 stride row)  |
19//! | - mdx_file_size (u32)        |      +- - - - - - - - - - - - - - -+
20//! +------------------------------+      | padding to 16-byte boundary  |
21//! | Geometry Header (80 bytes)   |      +------------------------------+
22//! | Model Header (116 bytes)     |      | Mesh 2 vertex data           |
23//! +------------------------------+      | + terminator                 |
24//! | Name Offset Array (u32[])    |      +- - - - - - - - - - - - - - -+
25//! | Name Strings (null-term)     |      | ...                          |
26//! +------------------------------+      +------------------------------+
27//! | Animation Header Array       |      | Last mesh vertex data        |
28//! | (136 bytes * anim_count)     |      | + terminator (no final pad)  |
29//! +------------------------------+      +------------------------------+
30//! | Node Tree (recursive DFS)    |
31//! | - Node header (80 bytes)     |
32//! | - Type-specific extra data   |
33//! | - Controller keys + data     |
34//! | - Children (recurse)         |
35//! +------------------------------+
36//! | Animation Node Trees         |
37//! | (one tree per animation)     |
38//! +------------------------------+
39//! ```
40//!
41//! ## Wrapper (12 bytes)
42//! ```text
43//! 0x00..0x04  zero_marker      (u32, always 0)
44//! 0x04..0x08  mdl_content_size (u32, bytes after wrapper)
45//! 0x08..0x0C  mdx_file_size    (u32, companion MDX total size)
46//! ```
47//!
48//! ## Geometry Header (80 bytes, content +0x00..+0x4F)
49//! ```text
50//! 0x00..0x04  fn_ptr1          (u32, leaked toolset vtable pointer)
51//! 0x04..0x08  fn_ptr2          (u32, leaked toolset vtable pointer)
52//! 0x08..0x28  model_name       (char[32], null-terminated)
53//! 0x28..0x2C  root_node_ptr    (u32, content-relative)
54//! 0x2C..0x30  node_count       (u32)
55//! 0x30..0x3C  runtime_arr1     (12 bytes, zeros on disk)
56//! 0x3C..0x48  runtime_arr2     (12 bytes, zeros on disk)
57//! 0x48..0x4C  ref_count        (u32, zero on disk)
58//! 0x4C..0x4D  model_type       (u8, always 2 for geometry)
59//! 0x4D..0x50  padding          (3 bytes)
60//! ```
61//!
62//! ## Model Header (116 bytes, content +0x50..+0xC3)
63//! ```text
64//! 0x50        classification   (u8: 0=Other, 1=Effect, 2=Tile, 4=Character, 8=Door)
65//! 0x51        subclassification (u8)
66//! 0x52        unknown_52       (u8, always 0 in vanilla)
67//! 0x53        affected_by_fog  (u8, 0 or 1)
68//! 0x54..0x58  num_child_models (u32, always 0 in vanilla K1)
69//! 0x58..0x64  animation_arr    (CExoArrayList: ptr/count/alloc)
70//! 0x64..0x68  supermodel_ref   (u32, always 0 in vanilla K1)
71//! 0x68..0x74  bounding_min     (3x f32)
72//! 0x74..0x80  bounding_max     (3x f32)
73//! 0x80..0x84  radius           (f32, bounding sphere)
74//! 0x84..0x88  animation_scale  (f32, default 1.0)
75//! 0x88..0xA8  supermodel_name  (char[32], null-terminated)
76//! 0xA8..0xAC  off_anim_root    (u32, content-relative; usually = root_node_ptr)
77//! 0xAC..0xB0  padding          (4 bytes)
78//! 0xB0..0xB4  mdx_size         (u32, total MDX data size)
79//! 0xB4..0xB8  mdx_offset       (u32, always 0 in vanilla K1)
80//! 0xB8..0xBC  name_offsets_ptr (u32, content-relative -> u32[] name pointers)
81//! 0xBC..0xC0  name_count       (u32)
82//! ```
83//!
84//! ## Node Header (80 bytes)
85//! ```text
86//! 0x00..0x02  type_flags       (u16, bitfield: see node_flags module)
87//! 0x02..0x04  node_number      (u16, name_index for geometry / anim mapping for anims)
88//! 0x04..0x06  name_index       (u16, index into name table)
89//! 0x06..0x08  padding          (u16)
90//! 0x08..0x0C  off_root         (u32, always 0 in vanilla)
91//! 0x0C..0x10  off_parent       (u32, content-relative pointer to parent)
92//! 0x10..0x1C  position         (3x f32: x, y, z)
93//! 0x1C..0x2C  orientation      (4x f32: w, x, y, z quaternion)
94//! 0x2C..0x38  children_arr     (CExoArrayList: ptr/count/alloc)
95//! 0x38..0x44  ctrl_key_arr     (CExoArrayList: ptr/count/alloc)
96//! 0x44..0x50  ctrl_data_arr    (CExoArrayList: ptr/count/alloc)
97//! ```
98//!
99//! ## TriMesh Extra Header (332 bytes, node +0x50..+0x19B)
100//! ```text
101//! +0x00..+0x08  fn_ptr stubs       (2x u32, leaked toolset pointers)
102//! +0x08..+0x14  face_arr           (CExoArrayList -> MaxFace[32B] array)
103//! +0x14..+0x54  bounding/color     (bbox, bsphere, diffuse, ambient, transparency)
104//! +0x58..+0x98  texture names      (texture_0[32], texture_1[32])
105//! +0x98..+0xD4  CExoArrayList[5]   (index buffer submission system)
106//! +0xD4..+0xE8  shared index data  (offset, pool, size, indices_per_face, padding)
107//! +0xE8..+0xFC  UV animation       (animate_uv, direction, jitter, speed)
108//! +0xFC..+0x100 vertex_stride      (u32, bytes per MDX vertex)
109//! +0x100..+0x130 MDX layout        (flags + 12 attribute byte offsets)
110//! +0x130..+0x140 mesh properties   (vertex_count, channel_count, flags)
111//! +0x13C..+0x144 total_surface_area (f32)
112//! +0x144..+0x148 mdx_data_offset   (u32, byte offset into MDX file)
113//! +0x148..+0x14C vert_array_offset (u32, content-relative -> position data)
114//! ```
115//!
116//! ## Animation Header (136 bytes)
117//! ```text
118//! 0x00..0x08  fn_ptrs          (2x u32, leaked toolset pointers)
119//! 0x08..0x28  name             (char[32], null-terminated)
120//! 0x28..0x2C  root_node_ptr    (u32, content-relative)
121//! 0x2C..0x30  node_count       (u32)
122//! 0x30..0x48  runtime arrays   (24 bytes, zeros on disk)
123//! 0x48..0x4C  ref_count        (u32, zero on disk)
124//! 0x4C..0x50  model_type+pad   (u8=5 + 3 bytes padding)
125//! 0x50..0x54  length           (f32, duration in seconds)
126//! 0x54..0x58  transition       (f32, blend time in seconds)
127//! 0x58..0x78  anim_root        (char[32], geometry node name)
128//! 0x78..0x84  event_arr        (CExoArrayList: ptr/count/alloc)
129//! 0x84..0x88  padding          (4 bytes)
130//! ```
131//!
132//! Binary format offsets verified against `swkotor.exe` via Ghidra
133//! decompilation. See `docs/notes/mdl_mdx.md` for full details.
134
135/// ASCII name registry for controllers, classifications, and node types.
136pub mod ascii_names;
137/// ASCII MDL reader.
138pub mod ascii_reader;
139/// ASCII MDL writer.
140pub mod ascii_writer;
141/// MDL controller types and keyframe structures.
142pub mod controllers;
143/// Quaternion and axis-angle conversion utilities.
144pub mod orientation;
145/// MDL binary reader.
146pub mod reader;
147/// Node-specific data types.
148pub mod types;
149/// MDL binary writer.
150pub mod writer;
151
152pub use ascii_reader::{read_mdl_ascii, read_mdl_ascii_from_str};
153pub use ascii_writer::{write_mdl_ascii, write_mdl_ascii_to_string, MdlAsciiError};
154pub use controllers::{MdlController, MdlControllerType, MdlKey};
155pub use reader::{read_mdl, read_mdl_from_bytes};
156pub use types::{
157    AabbNode, MdlAabb, MdlAnimMesh, MdlCamera, MdlDangly, MdlEmitter, MdlFace, MdlLight, MdlMesh,
158    MdlNodeData, MdlReference, MdlSaber, MdlSkin,
159};
160pub use writer::{write_mdl, write_mdl_to_vec, write_mdl_with_mdx_to_vec};
161
162use std::collections::HashMap;
163
164use crate::binary::{DecodeBinary, EncodeBinary};
165
166// ---------------------------------------------------------------------------
167// Shared tree helpers (used by reader, writer, and ASCII modules)
168// ---------------------------------------------------------------------------
169
170/// Counts all nodes in a geometry node tree (DFS).
171pub(crate) fn count_nodes(node: &MdlNode) -> u32 {
172    1 + node.children.iter().map(count_nodes).sum::<u32>()
173}
174
175/// Counts all nodes in an animation node tree (DFS).
176pub(crate) fn count_anim_nodes(node: &MdlAnimNode) -> u32 {
177    1 + node.children.iter().map(count_anim_nodes).sum::<u32>()
178}
179
180/// Collects geometry node positions by name (DFS). Used for animation
181/// position delta conversion (ASCII stores absolute, binary stores deltas).
182pub(crate) fn collect_geo_positions(node: &MdlNode) -> HashMap<&str, [f32; 3]> {
183    let mut map = HashMap::new();
184    fn recurse<'a>(node: &'a MdlNode, map: &mut HashMap<&'a str, [f32; 3]>) {
185        map.insert(&node.name, node.position);
186        for child in &node.children {
187            recurse(child, map);
188        }
189    }
190    recurse(node, &mut map);
191    map
192}
193
194/// Animation header size in the binary format (136 bytes = 0x88).
195///
196/// Mirrors the geometry header structure: fn_ptrs (8) + name (32) +
197/// root_node_ptr (4) + node_count (4) + runtime_arrays (24) + ref_count (4) +
198/// model_type (4) + length (4) + transition (4) + anim_root (32) +
199/// event_arr (12) + padding (4).
200///
201/// Verified against kotorblender's `peek_animations` (136 bytes per header)
202/// and binary reader at `load_animation`.
203pub(crate) const ANIMATION_HEADER_SIZE: usize = 0x88;
204
205/// Event size in the binary format (36 bytes = 0x24).
206///
207/// Layout: time (f32, 4 bytes) + name (char[32], 32 bytes).
208pub(crate) const ANIMATION_EVENT_SIZE: usize = 0x24;
209
210/// Offsets within an animation header (relative to animation header start).
211pub(crate) mod anim_header_offsets {
212    /// Function pointer 1 (u32).
213    pub const FN_PTR1: usize = 0x00;
214    /// Function pointer 2 (u32).
215    pub const FN_PTR2: usize = 0x04;
216    /// Animation name (32-byte null-terminated string).
217    pub const NAME: usize = 0x08;
218    /// Name field size.
219    pub const NAME_SIZE: usize = 32;
220    /// Content-relative offset to animation root node.
221    pub const ROOT_NODE_PTR: usize = 0x28;
222    /// Total number of animation nodes.
223    #[allow(dead_code)]
224    pub const NODE_COUNT: usize = 0x2C;
225    /// Runtime array 1 (12 bytes, zeros on disk).
226    #[allow(dead_code)]
227    pub const RUNTIME_ARR1: usize = 0x30;
228    /// Runtime array 2 (12 bytes, zeros on disk).
229    #[allow(dead_code)]
230    pub const RUNTIME_ARR2: usize = 0x3C;
231    /// Reference count (zero on disk).
232    #[allow(dead_code)]
233    pub const REF_COUNT: usize = 0x48;
234    /// Model type byte (always 5 for animations).
235    #[allow(dead_code)]
236    pub const MODEL_TYPE: usize = 0x4C;
237    /// Animation duration in seconds (f32).
238    pub const LENGTH: usize = 0x50;
239    /// Transition time in seconds (f32).
240    pub const TRANSITION: usize = 0x54;
241    /// Animation root node name (32-byte null-terminated string).
242    pub const ANIM_ROOT: usize = 0x58;
243    /// Animation root name field size.
244    pub const ANIM_ROOT_SIZE: usize = 32;
245    /// Event array pointer (content-relative, u32).
246    pub const EVENT_ARR_PTR: usize = 0x78;
247    /// Event count (u32).
248    pub const EVENT_ARR_COUNT: usize = 0x7C;
249    /// Event array allocated count (u32, mirrors count on disk).
250    #[allow(dead_code)]
251    pub const EVENT_ARR_ALLOC: usize = 0x80;
252    /// Padding (4 bytes at +0x84).
253    #[allow(dead_code)]
254    pub const PADDING_84: usize = 0x84;
255}
256
257/// K1 PC animation function pointer 1 (`0x00413370`).
258#[allow(dead_code)]
259pub(crate) const ANIM_FN_PTR_1_K1_PC: u32 = 4_273_392;
260/// K1 PC animation function pointer 2 (`0x0043E1E0`).
261#[allow(dead_code)]
262pub(crate) const ANIM_FN_PTR_2_K1_PC: u32 = 4_451_552;
263
264/// MDL file wrapper size (12 bytes: zero_marker + mdl_size + mdx_size).
265pub(crate) const MDL_WRAPPER_SIZE: u64 = 12;
266
267/// Offsets within the Model Header (relative to wrapper end).
268///
269/// Verified against `InputBinary::Reset` (`0x004a1030`) and `Model::Model`
270/// (`0x0044aa70`) in `swkotor.exe`, cross-validated via hex dump of
271/// `c_dewback.mdl` (Character=4), `dor_lhr01.mdl` (Door=8),
272/// and `m01aa_01a.mdl` (Other=0).
273///
274/// See `docs/notes/mdl_mdx.md` Binary MDL Loading Pipeline.
275pub(crate) mod header_offsets {
276    // --- Geometry header (80 bytes, +0x00..+0x4F) ---
277
278    /// Function pointer 1 (u32). Used by kotorblender for K1/K2/Xbox detection.
279    /// Runtime vtable pointer leaked from the BioWare toolset.
280    pub const FN_PTR1: usize = 0x00;
281    /// Function pointer 2 (u32). Same provenance as fn_ptr1.
282    pub const FN_PTR2: usize = 0x04;
283    /// Model name (32-byte null-terminated string at +0x08).
284    pub const MODEL_NAME: usize = 0x08;
285    /// Size of the model name field.
286    pub const MODEL_NAME_SIZE: usize = 32;
287    /// Offset to the root node structure.
288    pub const ROOT_NODE_PTR: usize = 0x28;
289    /// Total number of nodes in the model.
290    pub const NODE_COUNT: usize = 0x2C;
291    /// Runtime array 1 (12 bytes: ptr/count/alloc). Zeroed on disk.
292    #[allow(dead_code)]
293    pub const RUNTIME_ARR1: usize = 0x30;
294    /// Runtime array 2 (12 bytes: ptr/count/alloc). Zeroed on disk.
295    #[allow(dead_code)]
296    pub const RUNTIME_ARR2: usize = 0x3C;
297    /// Reference count (u32). Runtime-only, zero on disk.
298    #[allow(dead_code)]
299    pub const REF_COUNT: usize = 0x48;
300    /// Model type (u8): 0=geometry, 5=animation. Always 2 for geometry in KotOR.
301    pub const MODEL_TYPE: usize = 0x4C;
302
303    // --- Model header (116 bytes, +0x50..+0xC3) ---
304
305    /// Model classification byte (0=Other, 1=Effect, 2=Tile, 4=Character, 8=Door).
306    ///
307    /// Verified via `Model::Model` constructor (`0x0044aa70`): field at +0x50,
308    /// default 0. Cross-validated against 3 vanilla K1 models.
309    pub const CLASSIFICATION: usize = 0x50;
310    /// Subclassification byte (+0x51). Non-zero in ~196 vanilla K1 models.
311    pub const SUBCLASSIFICATION: usize = 0x51;
312    /// Unknown byte (+0x52). Always 0 in vanilla.
313    #[allow(dead_code)]
314    pub const UNKNOWN_52: usize = 0x52;
315    /// Affected-by-fog flag (+0x53). 0 or 1.
316    pub const AFFECTED_BY_FOG: usize = 0x53;
317    /// Number of child models (+0x54). Always 0 in vanilla K1.
318    #[allow(dead_code)]
319    pub const NUM_CHILD_MODELS: usize = 0x54;
320    /// Animation offsets CExoArrayList (ptr/count/alloc, 12 bytes at +0x58).
321    pub const ANIMATION_ARR_PTR: usize = 0x58;
322    /// Animation count.
323    pub const ANIMATION_ARR_COUNT: usize = 0x5C;
324    /// Supermodel reference (u32 at +0x64). Always 0 in vanilla K1.
325    #[allow(dead_code)]
326    pub const SUPERMODEL_REF: usize = 0x64;
327    /// Model bounding box minimum (3 × f32, +0x68..+0x73).
328    pub const BOUNDING_BOX_MIN: usize = 0x68;
329    /// Model bounding box maximum (3 × f32, +0x74..+0x7F).
330    #[allow(dead_code)]
331    pub const BOUNDING_BOX_MAX: usize = 0x74;
332    /// Model bounding sphere radius (f32, +0x80).
333    pub const RADIUS: usize = 0x80;
334    /// Animation scale factor (f32, default 1.0).
335    ///
336    /// Verified via `Model::Model` constructor: field at +0x84, initialized to 1.0.
337    pub const ANIMATION_SCALE: usize = 0x84;
338    /// Supermodel name (null-terminated string, up to 32 chars within 32-byte field).
339    ///
340    /// Verified via `InputBinary::Reset`: `FindModel((char*)(buf+0x88))`.
341    /// Constructor initializes to `'\0'` (empty string = no supermodel).
342    pub const SUPERMODEL_NAME: usize = 0x88;
343    /// Size of the supermodel name field in the binary header.
344    pub const SUPERMODEL_NAME_SIZE: usize = 32;
345    /// Animation root node offset (+0xA8). Content-relative pointer.
346    pub const OFF_ANIM_ROOT: usize = 0xA8;
347    /// MDX total size (+0xB0). Writer derives from MDX buffer.
348    pub const MDX_SIZE: usize = 0xB0;
349    /// MDX offset (+0xB4). Always 0 in vanilla K1.
350    #[allow(dead_code)]
351    pub const MDX_OFFSET: usize = 0xB4;
352    /// Offset to the array of name string pointers.
353    pub const NAME_OFFSETS_PTR: usize = 0xB8;
354    /// Number of names in the name table.
355    pub const NAME_COUNT: usize = 0xBC;
356}
357
358/// Size of the base node header in the binary format (bytes 0x00–0x4F).
359///
360/// The binary format uses 3-field arrays (ptr, count_used, count_allocated)
361/// for children, controller keys, and controller data - verified empirically
362/// against `c_dewback.mdl` extracted from vanilla K1 via `vanilla-inspector`.
363pub(crate) const NODE_HEADER_SIZE: usize = 0x50;
364
365/// Extra header size for Light nodes (92 bytes).
366///
367/// Verified via Ghidra struct `MdlNodeLight` (172 total − 80 base = 92).
368/// See `docs/notes/mdl_mdx.md` Non-Mesh Node Type Structs.
369pub(crate) const LIGHT_EXTRA_SIZE: usize = 0x5C;
370
371/// Offsets within a Light extra header (relative to light extra start,
372/// i.e. byte 0x50 from the node base, immediately after the base header).
373///
374/// Verified via `InputBinary::ResetLight` (`0x004a05e0`) and
375/// `MdlNodeLight::InternalParseField` (`0x00469150`).
376/// See `docs/notes/mdl_mdx.md` §Non-Mesh Node Type Structs.
377pub(crate) mod light_offsets {
378    /// Flare radius (f32). Extra +0x00.
379    pub const FLARE_RADIUS: usize = 0x00;
380    /// Texture SafePointers CExoArrayList (12 bytes, runtime-only). Extra +0x04.
381    /// Zeroed on disk - populated at runtime by `AurTextureGetReference`.
382    pub const TEXTURE_SAFE_PTRS_PTR: usize = 0x04;
383    /// Flare sizes CExoArrayList pointer (u32, relocated). Extra +0x10.
384    pub const FLARE_SIZES_PTR: usize = 0x10;
385    /// Flare sizes count (u32). Extra +0x14.
386    pub const FLARE_SIZES_COUNT: usize = 0x14;
387    // 0x18: CExoArrayList allocated count (ignored)
388    /// Flare positions CExoArrayList pointer (u32, relocated). Extra +0x1C.
389    pub const FLARE_POSITIONS_PTR: usize = 0x1C;
390    /// Flare positions count (u32). Extra +0x20.
391    pub const FLARE_POSITIONS_COUNT: usize = 0x20;
392    // 0x24: CExoArrayList allocated count (ignored)
393    /// Flare color shifts CExoArrayList pointer (u32, relocated). Extra +0x28.
394    pub const FLARE_COLOR_SHIFTS_PTR: usize = 0x28;
395    /// Flare color shifts count (u32). Extra +0x2C.
396    pub const FLARE_COLOR_SHIFTS_COUNT: usize = 0x2C;
397    // 0x30: CExoArrayList allocated count (ignored)
398    /// Flare texture names CExoArrayList pointer (u32, relocated). Extra +0x34.
399    ///
400    /// Points to an array of u32 string offsets, each of which is also relocated.
401    /// The strings are null-terminated.
402    pub const FLARE_TEX_NAMES_PTR: usize = 0x34;
403    /// Flare texture names count (u32). Extra +0x38.
404    pub const FLARE_TEX_NAMES_COUNT: usize = 0x38;
405    // 0x3C: CExoArrayList allocated count (ignored)
406    /// Light priority (i32, default 5). Extra +0x40.
407    pub const PRIORITY: usize = 0x40;
408    /// Dynamic type count (i32, default 1). Extra +0x44.
409    pub const NUM_DYNAMIC_TYPES: usize = 0x44;
410    /// Affects dynamic objects (i32, default 1). Extra +0x48.
411    pub const AFFECTDYNAMIC: usize = 0x48;
412    /// Casts shadow (i32, default 1). Extra +0x4C.
413    pub const SHADOW: usize = 0x4C;
414    /// Ambient-only light (i32, default 0). Extra +0x50.
415    pub const AMBIENTONLY: usize = 0x50;
416    /// Generate flare effect (i32, default 0). Extra +0x54.
417    pub const GENERATEFLARE: usize = 0x54;
418    /// Fading light (i32, default 1). Extra +0x58.
419    pub const FADING_LIGHT: usize = 0x58;
420}
421
422/// Extra header size for Reference nodes (36 bytes = char[32] + i32).
423///
424/// Verified via Ghidra struct `MdlNodeReference` (116 total − 80 base = 36).
425/// See `docs/notes/mdl_mdx.md` Non-Mesh Node Type Structs.
426pub(crate) const REFERENCE_EXTRA_SIZE: usize = 0x24;
427
428/// Extra header size for Emitter nodes (224 bytes).
429///
430/// All inline fixed-size data - no pointer relocation needed.
431/// Verified via Ghidra struct `MdlNodeEmitter` (304 total − 80 base = 224).
432/// See `docs/notes/mdl_mdx.md` Non-Mesh Node Type Structs.
433pub(crate) const EMITTER_EXTRA_SIZE: usize = 0xE0;
434
435/// Offsets within a Node Header (0x50 = 80 bytes).
436///
437/// Array fields use 3 × u32 (ptr, count_used, count_allocated) - the allocated
438/// count mirrors used count on disk and is ignored during reading.
439///
440/// Empirically verified against `c_dewback.mdl` (102 nodes, vanilla K1).
441/// See `docs/notes/mdl_mdx.md` ResetMdlNodeParts.
442pub(crate) mod node_offsets {
443    /// Node type flags (u16).
444    pub const FLAGS: usize = 0x00;
445    /// Node index (u16) used for name lookup.
446    pub const NODE_ID: usize = 0x04;
447    /// Local position X coordinate (f32).
448    pub const POS_X: usize = 0x10;
449    /// Orientation quaternion W component (f32). Layout: w, x, y, z.
450    ///
451    /// Verified via `Quaternion` struct in Ghidra: field order is {w, x, y, z}.
452    pub const ORIENTATION_W: usize = 0x1C;
453    /// Offset to the array of child node pointers.
454    pub const CHILD_ARRAY_PTR: usize = 0x2C;
455    /// Number of child nodes (used count).
456    pub const CHILD_COUNT: usize = 0x30;
457    // 0x34: child array allocated count (ignored)
458    /// Offset to the array of controller key headers.
459    pub const CONTROLLER_KEY_PTR: usize = 0x38;
460    /// Number of controller keys (used count).
461    pub const CONTROLLER_KEY_COUNT: usize = 0x3C;
462    // 0x40: controller key array allocated count (ignored)
463    /// Offset to the array of controller data (floats).
464    pub const CONTROLLER_DATA_PTR: usize = 0x44;
465    /// Number of controller data elements (floats, used count).
466    pub const CONTROLLER_DATA_COUNT: usize = 0x48;
467    // 0x4C: controller data array allocated count (ignored)
468}
469
470/// Extra header size for TriMesh nodes (332 bytes = 0x14C).
471///
472/// MdlNodeTriMesh is 412 bytes total; the base MdlNode is 80 bytes.
473/// Verified via Ghidra struct `MdlNodeTriMesh` (412 bytes total).
474/// See `docs/notes/mdl_mdx.md` §MdlNodeTriMesh.
475pub(crate) const MESH_EXTRA_SIZE: usize = 0x14C;
476
477/// Extra header size for Skin nodes beyond TriMesh (100 bytes = 0x64).
478///
479/// MdlNodeSkin is 512 bytes total (412 TriMesh + 100 extra).
480/// Verified via Ghidra struct `MdlNodeSkin` and `InputBinary::ResetSkin`
481/// (`0x004a01b0`). See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
482pub(crate) const SKIN_EXTRA_SIZE: usize = 0x64;
483
484/// Extra header size for AnimMesh nodes beyond TriMesh (56 bytes = 0x38).
485///
486/// MdlNodeAnimMesh is 468 bytes total (412 TriMesh + 56 extra).
487/// Verified via Ghidra struct `MdlNodeAnimMesh` and `InputBinary::ResetAnim`
488/// (`0x004a0060`). See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
489pub(crate) const ANIM_MESH_EXTRA_SIZE: usize = 0x38;
490
491/// Extra header size for AABB nodes beyond TriMesh (4 bytes).
492///
493/// MdlNodeAABB is 416 bytes total (412 TriMesh + 4 extra).
494/// The 4-byte extra is a root pointer to the AABB binary search tree,
495/// which is stored inline in the MDL content blob as a flattened binary tree.
496///
497/// Verified via `ResetMdlNode` inline processing and `ResetAABBTree`
498/// (`0x004a0260`). See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
499pub(crate) const AABB_EXTRA_SIZE: usize = 0x04;
500
501/// Extra header size for Saber nodes beyond TriMesh (20 bytes = 0x14).
502///
503/// MdlNodeLightsaber is 432 bytes total (412 TriMesh + 20 extra).
504/// Contains 3 relocated data pointers and 2 runtime GL pool IDs.
505///
506/// Verified via `InputBinary::ResetLightsaber` (`0x004a0460`) and
507/// `ParseNode` allocation (`operator_new(0x1B0)`).
508/// See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
509pub(crate) const SABER_EXTRA_SIZE: usize = 0x14;
510
511/// Extra header size for DanglyMesh nodes beyond TriMesh (28 bytes = 0x1C).
512///
513/// MdlNodeDanglyMesh is 440 bytes total (412 TriMesh + 28 extra).
514/// Verified via Ghidra struct `MdlNodeDanglyMesh` and `InputBinary::ResetDangly`
515/// (`0x004a0100`). See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
516pub(crate) const DANGLY_EXTRA_SIZE: usize = 0x1C;
517
518/// Size of a single MaxFace entry in the face array (32 bytes).
519///
520/// Layout: plane_normal(3×f32) + plane_distance(f32) + surface_id(u32)
521/// + adjacent(3×u16) + vertex_indices(3×u16).
522///
523/// Verified via Ghidra struct `MaxFace` (32 bytes) and the `0x1A` vertex-index
524/// offset constant in `InternalGenVertices` functions.
525/// See `docs/notes/mdl_mdx.md` §MdlNodeTriMesh.
526pub(crate) const MAX_FACE_SIZE: usize = 32;
527
528/// Offsets within a TriMesh extra header (relative to mesh extra start, i.e.
529/// `MdlNodeTriMesh` absolute offset 0x50).
530///
531/// Verified against `InputBinary::ResetTriMeshParts` (`0x004a0c00`)
532/// and Ghidra struct `MdlNodeTriMesh` (412 bytes).
533/// See `docs/notes/mdl_mdx.md` §MdlNodeTriMesh.
534pub(crate) mod mesh_offsets {
535    /// `gen_vertices` function pointer stub (u32). Extra +0x00.
536    ///
537    /// Stale code address from BioWare's build toolset. Overwritten at runtime
538    /// by the engine constructor. Preserved for lossless roundtrip and as a
539    /// toolset version fingerprint.
540    pub const FN_PTR_GEN_VERTICES: usize = 0x00;
541    /// `remove_temporary_array` function pointer stub (u32). Extra +0x04.
542    pub const FN_PTR_REMOVE_TEMP_ARRAY: usize = 0x04;
543
544    /// Face CExoArrayList data pointer (u32, relocated). Extra +0x08.
545    pub const FACE_ARRAY_OFFSET: usize = 0x08;
546    /// Number of faces (u32, CExoArrayList.size). Extra +0x0C.
547    pub const FACE_COUNT: usize = 0x0C;
548
549    /// Bounding box minimum (3×f32). Extra +0x14.
550    pub const BOUNDING_MIN: usize = 0x14;
551    /// Bounding box maximum (3×f32). Extra +0x20.
552    pub const BOUNDING_MAX: usize = 0x20;
553    /// Bounding sphere radius (f32). Extra +0x2C.
554    pub const BSPHERE_RADIUS: usize = 0x2C;
555    /// Bounding sphere center (3×f32). Extra +0x30.
556    pub const BSPHERE_CENTER: usize = 0x30;
557    /// RGB diffuse color (3×f32). Extra +0x3C.
558    pub const DIFFUSE_COLOR: usize = 0x3C;
559    /// RGB ambient color (3×f32). Extra +0x48.
560    pub const AMBIENT_COLOR: usize = 0x48;
561    /// Transparency hint (i32, 0=opaque, 1=transparent). Extra +0x54.
562    pub const TRANSPARENCY_HINT: usize = 0x54;
563    /// Primary texture name (char[32], null-terminated). Extra +0x58.
564    pub const TEXTURE_0: usize = 0x58;
565    /// Size of each texture name field in bytes.
566    pub const TEXTURE_NAME_SIZE: usize = 32;
567    /// Secondary/lightmap texture name (char[32], null-terminated). Extra +0x78.
568    pub const TEXTURE_1: usize = 0x78;
569
570    /// `vertex_indices` CExoArrayList pointer (u32, relocated). Extra +0x98.
571    /// Dead field in KotOR - always zeros. Reader/writer skip it.
572    #[allow(dead_code)]
573    pub const VERTEX_INDICES_ARRAY_PTR: usize = 0x98;
574    /// `vertex_indices` CExoArrayList count (u32). Extra +0x9C.
575    #[allow(dead_code)]
576    pub const VERTEX_INDICES_ARRAY_COUNT: usize = 0x9C;
577    /// `vertex_indices` CExoArrayList allocated count (u32). Extra +0xA0.
578    #[allow(dead_code)]
579    pub const VERTEX_INDICES_ARRAY_ALLOC: usize = 0xA0;
580
581    /// `left_over_faces` CExoArrayList pointer (u32, relocated). Extra +0xA4.
582    pub const LEFT_OVER_FACES_ARRAY_PTR: usize = 0xA4;
583    /// `left_over_faces` CExoArrayList count (u32). Extra +0xA8.
584    #[allow(dead_code)]
585    pub const LEFT_OVER_FACES_ARRAY_COUNT: usize = 0xA8;
586    /// `left_over_faces` CExoArrayList allocated count (u32). Extra +0xAC.
587    #[allow(dead_code)]
588    pub const LEFT_OVER_FACES_ARRAY_ALLOC: usize = 0xAC;
589
590    /// `vertex_indices_count` CExoArrayList pointer (u32, relocated). Extra +0xB0.
591    pub const VERTEX_INDICES_COUNT_ARRAY_PTR: usize = 0xB0;
592    /// `vertex_indices_count` CExoArrayList count (u32). Extra +0xB4.
593    pub const VERTEX_INDICES_COUNT_ARRAY_COUNT: usize = 0xB4;
594    /// `vertex_indices_count` CExoArrayList allocated count (u32). Extra +0xB8.
595    pub const VERTEX_INDICES_COUNT_ARRAY_ALLOC: usize = 0xB8;
596
597    /// `mdx_offsets` CExoArrayList pointer (u32, relocated). Extra +0xBC.
598    pub const MDX_OFFSETS_ARRAY_PTR: usize = 0xBC;
599    /// `mdx_offsets` CExoArrayList count (u32). Extra +0xC0.
600    pub const MDX_OFFSETS_ARRAY_COUNT: usize = 0xC0;
601    /// `mdx_offsets` CExoArrayList allocated count (u32). Extra +0xC4.
602    pub const MDX_OFFSETS_ARRAY_ALLOC: usize = 0xC4;
603
604    /// `index_buffer_pools` CExoArrayList pointer (u32, relocated). Extra +0xC8.
605    pub const INDEX_BUFFER_POOLS_ARRAY_PTR: usize = 0xC8;
606    /// `index_buffer_pools` CExoArrayList count (u32). Extra +0xCC.
607    pub const INDEX_BUFFER_POOLS_ARRAY_COUNT: usize = 0xCC;
608    /// `index_buffer_pools` CExoArrayList allocated count (u32). Extra +0xD0.
609    pub const INDEX_BUFFER_POOLS_ARRAY_ALLOC: usize = 0xD0;
610
611    /// Shared index offset scalar (i32). Extra +0xD4.
612    pub const SHARED_INDEX_OFFSET: usize = 0xD4;
613    /// Shared index pool scalar (i32/pointer-sized on-disk value). Extra +0xD8.
614    pub const SHARED_INDEX_POOL: usize = 0xD8;
615    /// Shared index size scalar (i32). Extra +0xDC.
616    pub const SHARED_INDEX_SIZE: usize = 0xDC;
617    /// Indices-per-face scalar (u32). Extra +0xE0.
618    pub const INDICES_PER_FACE: usize = 0xE0;
619
620    /// UV animation enable flag (i32). Extra +0xE8.
621    pub const ANIMATE_UV: usize = 0xE8;
622    /// UV animation direction X (f32). Extra +0xEC.
623    pub const UV_DIRECTION_X: usize = 0xEC;
624    /// UV animation direction Y (f32). Extra +0xF0.
625    pub const UV_DIRECTION_Y: usize = 0xF0;
626    /// UV jitter amount (f32). Extra +0xF4.
627    pub const UV_JITTER: usize = 0xF4;
628    /// UV jitter speed (f32). Extra +0xF8.
629    pub const UV_JITTER_SPEED: usize = 0xF8;
630
631    /// Per-vertex stride in MDX data (u32).
632    ///
633    /// At MdlNodeTriMesh absolute +0x14C, mesh extra offset +0xFC.
634    /// Verified via `ResetTriMeshParts`: `field34_0x14c * vertex_count`
635    /// computes total vertex data size.
636    pub const VERTEX_STRUCT_SIZE: usize = 0xFC;
637
638    /// Number of vertices (u16).
639    ///
640    /// At MdlNodeTriMesh absolute +0x180, mesh extra offset +0x130.
641    /// Verified via `ResetTriMeshParts`: `field47_0x180` cast to `(short)`.
642    pub const VERTEX_COUNT: usize = 0x130;
643
644    /// Number of UV texture channels (u16). Extra +0x132.
645    pub const TEXTURE_CHANNEL_COUNT: usize = 0x132;
646    /// Lightmapped flag (bool/u8). Extra +0x134.
647    pub const LIGHT_MAPPED: usize = 0x134;
648    /// Rotate texture flag (bool/u8). Extra +0x135.
649    pub const ROTATE_TEXTURE: usize = 0x135;
650    /// Background geometry flag (bool/u8). Extra +0x136.
651    pub const IS_BACKGROUND_GEOMETRY: usize = 0x136;
652
653    /// Shadow flag (bool/u8). MdlNodeTriMesh absolute +0x187, extra +0x137.
654    pub const SHADOW: usize = 0x137;
655
656    /// Beaming flag (bool/u8). Extra +0x138.
657    pub const BEAMING: usize = 0x138;
658
659    /// Render flag (bool/u8). MdlNodeTriMesh absolute +0x189, extra +0x139.
660    pub const RENDER: usize = 0x139;
661
662    /// Total surface area (f32). Extra +0x13C.
663    ///
664    /// Computed by `ComputeLocalSurfaceArea` during ASCII->binary post-processing.
665    /// In binary MDL files, preserved as-is from the file.
666    pub const TOTAL_SURFACE_AREA: usize = 0x13C;
667
668    /// Per-mesh offset into the MDX file (u32). Extra +0x144.
669    ///
670    /// Stores the byte offset where this mesh's interleaved vertex data begins
671    /// in the companion MDX file. The engine (and community tools like
672    /// kotorblender) uses this to seek to the correct position in the MDX
673    /// buffer for each mesh's vertices.
674    ///
675    /// Confirmed via kotorblender reader (line 375): reads this field and uses
676    /// it as `mdx.seek(mdx_offset + i * stride + attr_offset)`.
677    pub const MDX_DATA_OFFSET: usize = 0x144;
678
679    /// Content-relative pointer to position-only vertex data (u32, relocated).
680    ///
681    /// At MdlNodeTriMesh absolute +0x198, mesh extra offset +0x148.
682    /// Verified via `ResetTriMeshParts`: `field60_0x198` relocated as
683    /// `param_2 + offset` where `param_2` is the MDL content base pointer
684    /// (NOT the MDX base pointer, which is unused and freed after Reset).
685    ///
686    /// Points to `vertex_count * 12` bytes (3x f32 position data) within the
687    /// MDL content blob. This is the embedded vertex position array, always
688    /// present in vanilla files even when MDX data exists.
689    ///
690    /// See `docs/notes/mdl_mdx.md` -- MDX Data Offset Semantics.
691    pub const VERT_ARRAY_OFFSET: usize = 0x148;
692
693    // --- MDX vertex attribute layout fields ---
694    // These fields control which vertex attributes are present in the MDX
695    // companion file and where each attribute sits within each vertex's stride.
696    //
697    // Verified via Ghidra struct `MdlNodeTriMesh` and vanilla K1 hex dumps.
698    // See `docs/notes/mdl_mdx.md` §MDX Vertex Layout.
699
700    /// MDX vertex attribute flags bitfield (u32). Extra +0x100.
701    ///
702    /// Bits: 0x01=position, 0x02=UV1, 0x04=UV2, 0x08=UV3, 0x10=UV4,
703    /// 0x20=normal, 0x80=tangent_space. Vertex colors have no flag bit
704    /// (presence determined by offset != -1).
705    pub const MDX_VERTEX_FLAGS: usize = 0x100;
706    /// Byte offset of position data within each MDX vertex (i32). Extra +0x104.
707    /// Value -1 (0xFFFFFFFF) means not present.
708    pub const MDX_POSITION_OFFSET: usize = 0x104;
709    /// Byte offset of normal data within each MDX vertex (i32). Extra +0x108.
710    pub const MDX_NORMAL_OFFSET: usize = 0x108;
711    /// Byte offset of vertex color data within each MDX vertex (i32). Extra +0x10C.
712    pub const MDX_COLOR_OFFSET: usize = 0x10C;
713    /// Byte offset of UV1 texture coordinates within each MDX vertex (i32). Extra +0x110.
714    pub const MDX_UV1_OFFSET: usize = 0x110;
715    /// Byte offset of UV2 texture coordinates within each MDX vertex (i32). Extra +0x114.
716    pub const MDX_UV2_OFFSET: usize = 0x114;
717    /// Byte offset of UV3 texture coordinates within each MDX vertex (i32). Extra +0x118.
718    pub const MDX_UV3_OFFSET: usize = 0x118;
719    /// Byte offset of UV4 texture coordinates within each MDX vertex (i32). Extra +0x11C.
720    pub const MDX_UV4_OFFSET: usize = 0x11C;
721    /// Byte offset of tangent space data within each MDX vertex (i32). Extra +0x120.
722    /// Tangent space is 3×3 floats (tangent, bitangent, cross product) = 36 bytes.
723    pub const MDX_TANGENT_SPACE_OFFSET: usize = 0x120;
724
725    /// Reserved MDX offset slot 8 (always -1 in vanilla K1). Extra +0x124.
726    #[allow(dead_code)]
727    pub const MDX_RESERVED_OFFSET_8: usize = 0x124;
728    /// Reserved MDX offset slot 9 (always -1 in vanilla K1). Extra +0x128.
729    #[allow(dead_code)]
730    pub const MDX_RESERVED_OFFSET_9: usize = 0x128;
731    /// Reserved MDX offset slot 10 (always -1 in vanilla K1). Extra +0x12C.
732    #[allow(dead_code)]
733    pub const MDX_RESERVED_OFFSET_10: usize = 0x12C;
734}
735
736/// Offsets within a Skin extra header (relative to skin extra start,
737/// i.e. byte 0x19C from the node base, immediately after the TriMesh header).
738///
739/// Verified via `InputBinary::ResetSkin` (`0x004a01b0`) and Ghidra struct
740/// `MdlNodeSkin` (512 bytes total).
741/// See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
742pub(crate) mod skin_offsets {
743    /// Weights CExoArrayList header start (12 bytes). Extra +0x00.
744    ///
745    /// Always zeros in vanilla binary files - the engine's `SkinVertexWeight`
746    /// array is only populated by the ASCII text parser.
747    #[allow(dead_code)]
748    pub const WEIGHTS_PTR: usize = 0x00;
749    // 0x04: CExoArrayList.size (always 0)
750    // 0x08: CExoArrayList.allocated (always 0)
751    /// MDX per-vertex bone weights byte offset (i32). Extra +0x0C.
752    ///
753    /// Byte offset within each MDX vertex stride to the 4-float bone weights.
754    /// Value -1 means not present.
755    pub const MDX_BONE_WEIGHTS_OFFSET: usize = 0x0C;
756    /// MDX per-vertex bone indices byte offset (i32). Extra +0x10.
757    ///
758    /// Byte offset within each MDX vertex stride to the 4-float bone indices.
759    /// Value -1 means not present.
760    pub const MDX_BONE_INDICES_OFFSET: usize = 0x10;
761    /// Bonemap data pointer (u32, relocated if count at +0x18 != 0). Extra +0x14.
762    ///
763    /// Points to an array of bone mapping entries in the MDL content blob.
764    pub const BONEMAP_PTR: usize = 0x14;
765    /// Bonemap entry count (u32, size guard for +0x14). Extra +0x18.
766    pub const BONEMAP_COUNT: usize = 0x18;
767    /// Inverse bind rotation CExoArrayList pointer (u32, relocated). Extra +0x1C.
768    ///
769    /// Points to an array of Quaternion (4 × f32 = 16 bytes each).
770    /// One entry per bone - transforms from bone space to model space.
771    pub const QBONE_REF_INV_PTR: usize = 0x1C;
772    /// Number of inverse bind rotations (u32, CExoArrayList.size). Extra +0x20.
773    pub const QBONE_REF_INV_COUNT: usize = 0x20;
774    // 0x24: CExoArrayList allocated count (ignored)
775    /// Inverse bind translation CExoArrayList pointer (u32, relocated). Extra +0x28.
776    ///
777    /// Points to an array of Vector (3 × f32 = 12 bytes each).
778    /// One entry per bone - translation from bone space to model space.
779    pub const TBONE_REF_INV_PTR: usize = 0x28;
780    /// Number of inverse bind translations (u32, CExoArrayList.size). Extra +0x2C.
781    pub const TBONE_REF_INV_COUNT: usize = 0x2C;
782    // 0x30: CExoArrayList allocated count (ignored)
783    /// Bone constant indices CExoArrayList pointer (u32, relocated). Extra +0x34.
784    ///
785    /// Maps local bone index to global skeleton node index.
786    pub const BONE_CONSTANT_INDICES_PTR: usize = 0x34;
787    /// Number of bone constant indices (u32, CExoArrayList.size). Extra +0x38.
788    pub const BONE_CONSTANT_INDICES_COUNT: usize = 0x38;
789    // 0x3C: CExoArrayList allocated count (ignored)
790    /// Bone node serial numbers array (16 × u16 = 32 bytes). Extra +0x40.
791    ///
792    /// Fixed-size array of 16 bone node indices. Unused slots are zero.
793    pub const BONE_NODE_NUMBERS: usize = 0x40;
794    // 0x40..0x5F: 16 × u16 bone node numbers (count encoded in [u16; 16] array type)
795    // 0x60..0x63: 4 bytes tail (usually 0, non-zero in ~74 vanilla models)
796}
797
798/// Offsets within an AnimMesh extra header (relative to anim extra start,
799/// i.e. byte 0x19C from the node base, immediately after the TriMesh header).
800///
801/// Verified via `InputBinary::ResetAnim` (`0x004a0060`) and Ghidra struct
802/// `MdlNodeAnimMesh` (468 bytes total).
803/// See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
804pub(crate) mod anim_mesh_offsets {
805    /// Animation sampling period (f32). Extra +0x00.
806    pub const SAMPLE_PERIOD: usize = 0x00;
807    /// Animated vertex positions CExoArrayList pointer (u32, relocated). Extra +0x04.
808    ///
809    /// Points to an array of Vector (3 × f32 = 12 bytes each).
810    pub const ANIM_VERTS_PTR: usize = 0x04;
811    /// Number of animated vertex positions (u32, CExoArrayList.size). Extra +0x08.
812    pub const ANIM_VERTS_COUNT: usize = 0x08;
813    // 0x0C: CExoArrayList allocated count (ignored)
814    /// Animated texture coordinates CExoArrayList pointer (u32, relocated). Extra +0x10.
815    ///
816    /// Points to an array of Vector (3 × f32 = 12 bytes each).
817    pub const ANIM_T_VERTS_PTR: usize = 0x10;
818    /// Number of animated texture coordinates (u32, CExoArrayList.size). Extra +0x14.
819    pub const ANIM_T_VERTS_COUNT: usize = 0x14;
820    // 0x18: CExoArrayList allocated count (ignored)
821
822    // +0x1C..+0x37: Runtime-only fields with no ASCII parser names.
823    // Verified via `InputBinary::ResetAnim` (`0x004a0060`) - relocated if
824    // their size-guard counts are non-zero. `InternalParseField` (`0x0046a240`)
825    // only exposes "sampleperiod", "animverts", "animtverts" - these 6 fields
826    // have NO ASCII names. xoreos NWN calls the last 4 `offAnimVertices`,
827    // `offAnimTextureVertices`, `verticesCount`, `textureVerticesCount`.
828    //
829    // Always zero in authored binary files; populated at runtime by
830    // `MdlNodeAnimMesh::InternalGenVertices`. Preserved for roundtrip fidelity.
831
832    /// Runtime data pointer 1 (u32, relocated if `data_count_1 != 0`). Extra +0x1C.
833    pub const DATA_PTR_1: usize = 0x1C;
834    /// Size guard for `data_ptr_1` (u32). Extra +0x20.
835    pub const DATA_COUNT_1: usize = 0x20;
836    /// Padding (4 bytes, untouched by Reset). Extra +0x24.
837    pub const PADDING_24: usize = 0x24;
838    /// Runtime animated vertices pointer (u32, relocated if count != 0). Extra +0x28.
839    ///
840    /// Called `offAnimVertices` by xoreos NWN.
841    pub const ANIM_VERTICES_PTR: usize = 0x28;
842    /// Runtime animated texture vertices pointer (u32, relocated if count != 0). Extra +0x2C.
843    ///
844    /// Called `offAnimTextureVertices` by xoreos NWN.
845    pub const ANIM_TEX_VERTICES_PTR: usize = 0x2C;
846    /// Count for `anim_vertices_ptr` (u32). Extra +0x30.
847    pub const ANIM_VERTICES_COUNT: usize = 0x30;
848    /// Count for `anim_tex_vertices_ptr` (u32). Extra +0x34.
849    pub const ANIM_TEX_VERTICES_COUNT: usize = 0x34;
850}
851
852/// Offsets within an AABB extra header (relative to AABB extra start,
853/// i.e. byte 0x19C from the node base, immediately after the TriMesh header).
854///
855/// Verified via `ResetMdlNode` inline processing and `ResetAABBTree`
856/// (`0x004a0260`). See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
857pub(crate) mod aabb_offsets {
858    /// Root AABB tree pointer (u32, relocated). Extra +0x00.
859    pub const TREE_PTR: usize = 0x00;
860}
861
862/// Offsets within a DanglyMesh extra header (relative to dangly extra start,
863/// i.e. byte 0x19C from the node base, immediately after the TriMesh header).
864///
865/// Verified via `InputBinary::ResetDangly` (`0x004a0100`) and Ghidra struct
866/// `MdlNodeDanglyMesh` (440 bytes total).
867/// See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
868pub(crate) mod dangly_offsets {
869    /// Per-vertex constraint values CExoArrayList pointer (u32, relocated). Extra +0x00.
870    pub const CONSTRAINTS_PTR: usize = 0x00;
871    /// Number of constraint values (u32, CExoArrayList.size). Extra +0x04.
872    pub const CONSTRAINTS_COUNT: usize = 0x04;
873    // 0x08: CExoArrayList allocated count (ignored on read)
874    /// Maximum displacement distance (f32). Extra +0x0C.
875    pub const DISPLACEMENT: usize = 0x0C;
876    /// Spring tightness factor (f32). Extra +0x10.
877    pub const TIGHTNESS: usize = 0x10;
878    /// Oscillation period (f32). Extra +0x14.
879    pub const PERIOD: usize = 0x14;
880    /// Per-vertex dangly positions pointer (u32, relocated if vertex_count > 0). Extra +0x18.
881    ///
882    /// Points to `vertex_count` × vec3 (12 bytes each) in the MDL content blob.
883    /// At runtime, `PartDanglyMesh` copies these into a GL vertex pool.
884    pub const DATA_PTR: usize = 0x18;
885}
886
887/// Offsets within a Saber extra header (relative to saber extra start,
888/// i.e. byte 0x19C from the node base, immediately after the TriMesh header).
889///
890/// Verified via `InputBinary::ResetLightsaber` (`0x004a0460`).
891/// Semantic names from kotorblender (`reader.py` saber reading path).
892/// See `docs/notes/mdl_mdx.md` §Mesh Subtype Structs.
893pub(crate) mod saber_offsets {
894    /// Saber vertex positions pointer (u32, relocated). Extra +0x00.
895    ///
896    /// Points to `NUM_SABER_VERTS` × vec3 (12 bytes each) in the MDL content blob.
897    pub const VERTS_PTR: usize = 0x00;
898    /// Saber texture coordinates pointer (u32, relocated). Extra +0x04.
899    ///
900    /// Points to `NUM_SABER_VERTS` × vec2 (8 bytes each) in the MDL content blob.
901    pub const UVS_PTR: usize = 0x04;
902    /// Saber normal vectors pointer (u32, relocated). Extra +0x08.
903    ///
904    /// Points to `NUM_SABER_VERTS` × vec3 (12 bytes each) in the MDL content blob.
905    pub const NORMALS_PTR: usize = 0x08;
906    /// GL vertex pool ID (runtime-only, allocated by `GLRender::RequestPool`). Extra +0x0C.
907    pub const GL_POOL_VERT: usize = 0x0C;
908    /// GL index pool ID (runtime-only, allocated by `GLRender::RequestPool`). Extra +0x10.
909    pub const GL_POOL_INDEX: usize = 0x10;
910}
911
912/// Fixed number of saber vertices per blade mesh.
913///
914/// All lightsaber models use exactly 176 vertices - confirmed by
915/// kotorblender (`NUM_SABER_VERTS = 176`), reone (`kNumSaberSegments = 20`,
916/// `kNumSaberSegmentVertices = 4`, computed layout), and vanilla K1 hex dumps.
917pub(crate) const NUM_SABER_VERTS: usize = 176;
918
919/// Bitflags for node type identification in the on-disk binary format.
920///
921/// Node type constants verified at `0x00740a18` in `swkotor.exe`:
922/// `0x01, 0x03, 0x05, 0x09, 0x11, 0x21, 0x61, 0xA1, 0x121, 0x221, 0x401, 0x821`.
923/// See `docs/notes/mdl_mdx.md` ResetMdlNode.
924pub mod node_flags {
925    /// Node has a header (always set).
926    pub const HEADER: u32 = 0x0001;
927    /// Node contains light data.
928    pub const LIGHT: u32 = 0x0002;
929    /// Node contains emitter data.
930    pub const EMITTER: u32 = 0x0004;
931    /// Node contains camera data.
932    pub const CAMERA: u32 = 0x0008;
933    /// Node is a reference to another model.
934    pub const REFERENCE: u32 = 0x0010;
935    /// Node contains mesh geometry.
936    pub const MESH: u32 = 0x0020;
937    /// Node contains skinning weights.
938    pub const SKIN: u32 = 0x0040;
939    /// Node contains animation data.
940    pub const ANIM: u32 = 0x0080;
941    /// Node contains dangly mesh physics.
942    pub const DANGLY: u32 = 0x0100;
943    /// Node contains an AABB tree (walkmesh).
944    pub const AABB: u32 = 0x0200;
945    /// Node contains saber blade data.
946    pub const SABER: u32 = 0x0800;
947}
948
949/// MDL binary parsing error type.
950#[derive(Debug, thiserror::Error)]
951pub enum MdlError {
952    /// An I/O error occurred.
953    #[error("io error: {0}")]
954    Io(#[from] std::io::Error),
955
956    /// A binary layout error occurred (e.g. out of bounds).
957    #[error("binary layout error: {0}")]
958    Binary(#[from] crate::binary::BinaryLayoutError),
959
960    /// The MDL header is structurally invalid.
961    #[error("invalid MDL header: {0}")]
962    InvalidHeader(String),
963
964    /// The MDL data is structurally invalid.
965    #[error("invalid MDL data: {0}")]
966    InvalidData(String),
967
968    /// A value exceeds the on-disk field width during writing.
969    #[error("value overflow while writing field `{0}`")]
970    ValueOverflow(&'static str),
971}
972
973/// A node in the MDL hierarchy.
974#[derive(Debug, Clone, PartialEq)]
975pub struct MdlNode {
976    /// Name of the node resolved from the model's name table.
977    pub name: String,
978    /// Parent index (if any).
979    pub parent_index: Option<u16>,
980    /// Child nodes attached to this node.
981    pub children: Vec<MdlNode>,
982    /// Local position (x, y, z).
983    pub position: [f32; 3],
984    /// Local orientation quaternion (w, x, y, z).
985    ///
986    /// Matches the engine's `Quaternion` struct field order (Ghidra-verified).
987    /// Identity quaternion is `[1.0, 0.0, 0.0, 0.0]`.
988    pub rotation: [f32; 4],
989    /// Type-specific node data (determines node type and carries type-specific fields).
990    ///
991    /// Use [`MdlNodeData::flags()`] to get the binary flags for serialization.
992    pub node_data: MdlNodeData,
993    /// Controllers attached to this node.
994    pub controllers: Vec<MdlController>,
995    /// Unreferenced controller data floats (key_count=0 but data_count>0).
996    ///
997    /// Some vanilla nodes (especially in supermodel combat animations) carry
998    /// a controller data array with no corresponding controller key headers.
999    /// The engine reads both arrays independently, so we must preserve the
1000    /// data bytes for roundtrip fidelity even though no keys reference them.
1001    pub orphan_controller_data: Vec<f32>,
1002    /// Padding bytes from node header +0x02..+0x03 (between flags and node_id).
1003    ///
1004    /// Preserved verbatim for roundtrip fidelity per the reserved field rule.
1005    /// Zero for newly constructed nodes.
1006    pub header_padding_02: [u8; 2],
1007    /// Struct alignment padding from node header +0x06..+0x07 (2 bytes).
1008    ///
1009    /// Always zero in vanilla files. Located between the u16 node_id duplicate
1010    /// and the u32 name pointer at +0x08.
1011    ///
1012    /// The fields at +0x08 (name pointer) and +0x0C (parent pointer) are
1013    /// relocated pointers handled by the writer; they are not stored here.
1014    /// See `docs/notes/mdl_mdx.md` §MdlNode Binary Layout.
1015    pub header_padding_06: [u8; 2],
1016}
1017
1018impl MdlNode {
1019    /// Checks if this node contains mesh geometry (Flag 0x0020).
1020    pub fn is_mesh(&self) -> bool {
1021        self.node_data.mesh().is_some()
1022    }
1023
1024    /// Checks if this node contains light data (Flag 0x0002).
1025    pub fn is_light(&self) -> bool {
1026        matches!(self.node_data, MdlNodeData::Light(_))
1027    }
1028
1029    /// Checks if this node contains emitter data (Flag 0x0004).
1030    pub fn is_emitter(&self) -> bool {
1031        matches!(self.node_data, MdlNodeData::Emitter(_))
1032    }
1033
1034    /// Checks if this node is a reference to another model (Flag 0x0010).
1035    pub fn is_reference(&self) -> bool {
1036        matches!(self.node_data, MdlNodeData::Reference(_))
1037    }
1038
1039    /// Checks if this node contains skinning weights (Flag 0x0040).
1040    pub fn is_skin(&self) -> bool {
1041        matches!(self.node_data, MdlNodeData::Skin(_))
1042    }
1043
1044    /// Checks if this node contains animation data (Flag 0x0080).
1045    pub fn is_anim(&self) -> bool {
1046        matches!(self.node_data, MdlNodeData::AnimMesh(_))
1047    }
1048
1049    /// Checks if this node contains dangly mesh physics (Flag 0x0100).
1050    pub fn is_dangly(&self) -> bool {
1051        matches!(self.node_data, MdlNodeData::Dangly(_))
1052    }
1053
1054    /// Checks if this node contains an AABB walkmesh tree (Flag 0x0200).
1055    pub fn is_aabb(&self) -> bool {
1056        matches!(self.node_data, MdlNodeData::Aabb(_))
1057    }
1058
1059    /// Checks if this node contains camera data (Flag 0x0008).
1060    pub fn is_camera(&self) -> bool {
1061        matches!(self.node_data, MdlNodeData::Camera(_))
1062    }
1063
1064    /// Checks if this node contains saber blade data (Flag 0x0800).
1065    pub fn is_saber(&self) -> bool {
1066        matches!(self.node_data, MdlNodeData::Saber(_))
1067    }
1068}
1069
1070/// A node in an animation's node tree.
1071///
1072/// Animation nodes mirror the geometry node hierarchy but carry only
1073/// base header data (name, node_number, controllers, children). They
1074/// do NOT have type-specific extra data (mesh, light, emitter, etc.).
1075///
1076/// The `node_number` field maps each animation node to its corresponding
1077/// geometry node so the engine can apply keyframes to the correct target.
1078#[derive(Debug, Clone, PartialEq)]
1079pub struct MdlAnimNode {
1080    /// Name of the node (matches a geometry node name).
1081    pub name: String,
1082    /// Node number matching the corresponding geometry node.
1083    pub node_number: u16,
1084    /// Controllers (keyframes) for this animation node.
1085    pub controllers: Vec<MdlController>,
1086    /// Unreferenced controller data floats (key_count=0 but data_count>0).
1087    ///
1088    /// See [`MdlNode::orphan_controller_data`] for rationale.
1089    pub orphan_controller_data: Vec<f32>,
1090    /// Child animation nodes.
1091    pub children: Vec<MdlAnimNode>,
1092}
1093
1094/// An event that fires at a specific time during an animation.
1095///
1096/// Events trigger game logic (footstep sounds, particle effects, etc.)
1097/// at precise moments in the animation timeline.
1098#[derive(Debug, Clone, PartialEq)]
1099pub struct MdlAnimEvent {
1100    /// Time in seconds when this event fires.
1101    pub time: f32,
1102    /// Event name (up to 32 bytes in binary, null-terminated).
1103    pub name: String,
1104}
1105
1106/// A named animation sequence.
1107///
1108/// Each animation has its own node tree that mirrors (a subset of) the
1109/// geometry node hierarchy. The nodes carry controller keyframes that
1110/// animate transforms, light colors, emitter parameters, etc. over time.
1111///
1112/// The binary layout uses a 136-byte header (mirroring the geometry header
1113/// structure) followed by events and the animation node tree.
1114#[derive(Debug, Clone, PartialEq)]
1115pub struct MdlAnimation {
1116    /// Animation name (e.g. "cpause1", "walk", "attack1").
1117    pub name: String,
1118    /// Duration of the animation in seconds.
1119    pub length: f32,
1120    /// Transition time in seconds for blending into this animation.
1121    pub transition_time: f32,
1122    /// Name of the geometry node that anchors this animation.
1123    ///
1124    /// Determines which subtree of the geometry hierarchy is affected.
1125    /// Usually the root node name or a specific bone (e.g. "rootdummy").
1126    pub anim_root: String,
1127    /// Events that fire at specific times during playback.
1128    pub events: Vec<MdlAnimEvent>,
1129    /// Root of the animation node tree.
1130    pub root_node: MdlAnimNode,
1131    /// Function pointer 1 from the animation header.
1132    ///
1133    /// Leaked vtable pointer from the BioWare toolset.
1134    /// K1 PC value: `0x00413370` (4273392).
1135    pub fn_ptr1: u32,
1136    /// Function pointer 2 from the animation header.
1137    ///
1138    /// K1 PC value: `0x0043E1E0` (4451552).
1139    pub fn_ptr2: u32,
1140}
1141
1142/// The high-level MDL container.
1143///
1144/// Every field is fully typed - the reader extracts all meaningful header
1145/// data and the writer produces correct binary output from these fields alone.
1146#[derive(Debug, Clone, PartialEq)]
1147pub struct Mdl {
1148    /// The root node of the model hierarchy.
1149    pub root_node: MdlNode,
1150
1151    // --- Geometry header fields ---
1152    /// Function pointer 1 from the geometry header (+0x00).
1153    ///
1154    /// This is a leaked runtime vtable pointer from the BioWare toolset.
1155    /// Used by kotorblender for K1/K2/Xbox game detection.
1156    /// K1 PC value: `0x00413470`.
1157    pub geometry_fn_ptr1: u32,
1158    /// Function pointer 2 from the geometry header (+0x04).
1159    ///
1160    /// K1 PC value: `0x00405580`.
1161    pub geometry_fn_ptr2: u32,
1162    /// Model type from geometry header (+0x4C). Always 2 for geometry models.
1163    pub model_type: u8,
1164
1165    // --- Model header fields ---
1166    /// Classification (0=Other, 1=Effect, 2=Tile, 4=Character, 8=Door).
1167    pub classification: u8,
1168    /// Subclassification byte (+0x51). Non-zero in ~196 vanilla K1 models.
1169    pub subclassification: u8,
1170    /// Affected-by-fog flag (+0x53). 0 or 1.
1171    pub affected_by_fog: u8,
1172    /// The supermodel name (from header, +0x88).
1173    pub supermodel_name: String,
1174    /// Total node count (from header, +0x2C).
1175    pub node_count: u32,
1176    /// Model bounding box (min_xyz, max_xyz) from header (+0x68..+0x7F).
1177    pub bounding_box: [f32; 6],
1178    /// Model bounding sphere radius from header (+0x80).
1179    pub radius: f32,
1180    /// Animation scale factor from header (+0x84). Default 1.0.
1181    pub animation_scale: f32,
1182    /// Animations attached to this model.
1183    pub animations: Vec<MdlAnimation>,
1184    /// Animation root node name, when different from the geometry root.
1185    ///
1186    /// Head models set this to `"neck_g"` so the engine applies head
1187    /// animations from the neck bone rather than the model root. When
1188    /// `None`, the writer uses the geometry root offset for +0xA8.
1189    pub anim_root_node: Option<String>,
1190}
1191
1192/// Result of writing an MDL model with its companion MDX vertex data.
1193///
1194/// The MDL and MDX files are always written as a pair - the MDL contains
1195/// header references to vertex data stored in the MDX buffer.
1196#[derive(Debug, Clone)]
1197pub struct MdlWriteResult {
1198    /// The MDL binary data.
1199    pub mdl_bytes: Vec<u8>,
1200    /// The MDX vertex data.
1201    pub mdx_bytes: Vec<u8>,
1202}
1203
1204impl DecodeBinary for Mdl {
1205    type Error = MdlError;
1206
1207    /// Decodes an MDL model from raw bytes without MDX vertex data.
1208    ///
1209    /// For models with companion MDX data, use [`read_mdl_from_bytes`] directly
1210    /// with the `mdx_bytes` parameter.
1211    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
1212        read_mdl_from_bytes(bytes, None)
1213    }
1214}
1215
1216impl EncodeBinary for Mdl {
1217    type Error = MdlError;
1218
1219    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
1220        write_mdl_to_vec(self)
1221    }
1222}
1223
1224/// Assign inverted counter values to all mesh nodes in DFS tree order.
1225///
1226/// The inverted counter is a mesh sequence value stored at CExoArrayList
1227/// offset +0xC8. It uses an "inverted" numbering scheme where values count
1228/// down within each group of 100. Saber nodes consume two increments (one
1229/// for each GL pool).
1230///
1231/// This function is for newly constructed models. Binary-parsed models
1232/// already have their `inverted_counter` fields populated from the file.
1233///
1234/// Formula (from mdledit `asciipostprocess.cpp:1024`):
1235/// ```text
1236/// quo = mesh_counter / 100
1237/// mod = mesh_counter % 100
1238/// inverted_counter = 2^quo * 100 - mesh_counter
1239///                  + (mod != 0 ? quo * 100 : 0)
1240///                  + (quo != 0 ? 0 : -1)
1241/// ```
1242///
1243/// See `docs/notes/mesh_derived_fields.md` §1.5 for full documentation.
1244pub fn assign_inverted_counters(root: &mut MdlNode) {
1245    fn compute_inverted(counter: u32) -> u32 {
1246        let quo = counter / 100;
1247        let modv = counter % 100;
1248        let base = (1u32 << quo).saturating_mul(100);
1249        let result = base.wrapping_sub(counter);
1250        let result = if modv != 0 {
1251            result.wrapping_add(quo.saturating_mul(100))
1252        } else {
1253            result
1254        };
1255        if quo != 0 {
1256            result
1257        } else {
1258            result.wrapping_sub(1)
1259        }
1260    }
1261
1262    fn walk(node: &mut MdlNode, counter: &mut u32) {
1263        if let Some(mesh) = node.node_data.mesh_mut() {
1264            *counter += 1;
1265            mesh.inverted_counter = compute_inverted(*counter);
1266
1267            // Saber nodes consume a second increment.
1268            if matches!(node.node_data, types::MdlNodeData::Saber(_)) {
1269                *counter += 1;
1270            }
1271        }
1272        for child in &mut node.children {
1273            walk(child, counter);
1274        }
1275    }
1276
1277    let mut counter = 0u32;
1278    walk(root, &mut counter);
1279}