Skip to main content

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