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}