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}