Skip to main content

rakata_formats/mdl/
ascii_names.rs

1//! ASCII MDL name registry for controllers, classifications, and node types.
2//!
3//! Provides bidirectional mappings between binary format codes and the ASCII
4//! field name strings used by the engine's text-mode MDL parser. Controller
5//! type codes are node-type-specific -- the same numeric code has different
6//! ASCII names on different node types (e.g., code 100 is `selfillumcolor`
7//! on mesh, `verticaldisplacement` on light, and `drag` on emitter).
8//!
9//! All 58 controller codes are independently traced against `swkotor.exe`:
10//! 3 base (`MdlNode::InternalParseField`, `0x00465560`), 48 emitter
11//! (`MdlNodeEmitter`, `0x004658b0`), 5 light (`MdlNodeLight`, `0x00469150`)
12//! and 2 mesh (`MdlNodeTriMesh`, `0x00469700`). The set is closed rather
13//! than sampled: every call site of the three controller-registration sinks
14//! in the whole binary falls inside those four functions, so no other node
15//! type has a controller vocabulary of its own. `MdlNodeSkin` tail-calls the
16//! mesh parser, which is why the mesh pair covers the whole mesh family.
17
18use super::controllers::MdlControllerType;
19use super::types::{
20    MdlAabb, MdlAnimMesh, MdlDangly, MdlEmitter, MdlLight, MdlNodeData, MdlReference, MdlSaber,
21    MdlSkin,
22};
23
24/// Node type context for disambiguating controller names.
25///
26/// The engine dispatches ASCII field parsing through type-specific
27/// `InternalParseField` functions. Base controllers (position, orientation,
28/// scale) are handled by the base dispatcher; type-specific controllers
29/// use separate lookup tables.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum NodeTypeContext {
32    /// Base node (dummy, camera) -- only base controllers.
33    Base,
34    /// Mesh node (trimesh, skin, dangly, aabb, saber, animmesh).
35    Mesh,
36    /// Light node.
37    Light,
38    /// Emitter node.
39    Emitter,
40}
41
42/// Base controller entries shared across all node types.
43///
44/// Traced from `MdlNode::InternalParseField` (`0x00465560`). Node types with
45/// no parser of their own, such as saber and camera, get only these three.
46const BASE_CONTROLLERS: &[(u32, &str)] = &[(8, "position"), (20, "orientation"), (36, "scale")];
47
48/// Mesh-specific controller entries.
49///
50/// Traced from `MdlNodeTriMesh::InternalParseField` (`0x00469700`). Applies
51/// to every mesh-family node: `MdlNodeSkin` tail-calls this parser rather
52/// than registering its own.
53const MESH_CONTROLLERS: &[(u32, &str)] = &[(100, "selfillumcolor"), (132, "alpha")];
54
55/// Light-specific controller entries.
56///
57/// Traced from `MdlNodeLight::InternalParseField` (`0x00469150`).
58const LIGHT_CONTROLLERS: &[(u32, &str)] = &[
59    (76, "color"),
60    (88, "radius"),
61    (96, "shadowradius"),
62    (100, "verticaldisplacement"),
63    (140, "multiplier"),
64];
65
66/// Emitter-specific controller entries.
67///
68/// All 48 codes and their name strings traced from
69/// `MdlNodeEmitter::InternalParseField` at `0x004658b0`.
70const EMITTER_CONTROLLERS: &[(u32, &str)] = &[
71    (80, "alphaEnd"),
72    (84, "alphaStart"),
73    (88, "birthrate"),
74    (92, "bounce_co"),
75    (96, "combinetime"),
76    (100, "drag"),
77    (104, "fps"),
78    (108, "frameEnd"),
79    (112, "frameStart"),
80    (116, "grav"),
81    (120, "lifeExp"),
82    (124, "mass"),
83    (128, "p2p_bezier2"),
84    (132, "p2p_bezier3"),
85    (136, "particleRot"),
86    (140, "randvel"),
87    (144, "sizeStart"),
88    (148, "sizeEnd"),
89    (152, "sizeStart_y"),
90    (156, "sizeEnd_y"),
91    (160, "spread"),
92    (164, "threshold"),
93    (168, "velocity"),
94    (172, "xsize"),
95    (176, "ysize"),
96    (180, "blurlength"),
97    (184, "lightningDelay"),
98    (188, "lightningRadius"),
99    (192, "lightningScale"),
100    (196, "lightningSubDiv"),
101    (200, "lightningZigzag"),
102    (216, "alphaMid"),
103    (220, "percentStart"),
104    (224, "percentMid"),
105    (228, "percentEnd"),
106    (232, "sizeMid"),
107    (236, "sizeMid_y"),
108    (240, "m_fRandomBirthRate"),
109    (252, "targetsize"),
110    (256, "numcontrolpts"),
111    (260, "controlptradius"),
112    (264, "controlptdelay"),
113    (268, "tangentspread"),
114    (272, "tangentlength"),
115    (284, "colorMid"),
116    (380, "colorEnd"),
117    (392, "colorStart"),
118    (502, "detonate"),
119];
120
121/// Returns the ASCII name for a controller type in a given node context.
122///
123/// Base controllers (position, orientation, scale) are checked first, then
124/// the type-specific table. Returns `None` for unknown codes -- callers
125/// should use a `controller_<code>` fallback.
126pub fn controller_ascii_name(
127    code: MdlControllerType,
128    ctx: NodeTypeContext,
129) -> Option<&'static str> {
130    let raw = code.raw();
131
132    // Base controllers apply to all node types.
133    for &(c, name) in BASE_CONTROLLERS {
134        if c == raw {
135            return Some(name);
136        }
137    }
138
139    // Type-specific lookup.
140    let table = match ctx {
141        NodeTypeContext::Base => return None,
142        NodeTypeContext::Mesh => MESH_CONTROLLERS,
143        NodeTypeContext::Light => LIGHT_CONTROLLERS,
144        NodeTypeContext::Emitter => EMITTER_CONTROLLERS,
145    };
146
147    for &(c, name) in table {
148        if c == raw {
149            return Some(name);
150        }
151    }
152
153    None
154}
155
156/// Returns the controller type code for an ASCII name in a given node context.
157///
158/// Case-insensitive lookup. Base controllers are checked first, then
159/// the type-specific table. Returns `None` for unrecognized names.
160pub fn controller_from_ascii_name(name: &str, ctx: NodeTypeContext) -> Option<MdlControllerType> {
161    // Base controllers apply to all node types.
162    for &(code, ascii) in BASE_CONTROLLERS {
163        if ascii.eq_ignore_ascii_case(name) {
164            return Some(MdlControllerType::from_raw(code));
165        }
166    }
167
168    // Type-specific lookup.
169    let table = match ctx {
170        NodeTypeContext::Base => return None,
171        NodeTypeContext::Mesh => MESH_CONTROLLERS,
172        NodeTypeContext::Light => LIGHT_CONTROLLERS,
173        NodeTypeContext::Emitter => EMITTER_CONTROLLERS,
174    };
175
176    for &(code, ascii) in table {
177        if ascii.eq_ignore_ascii_case(name) {
178            return Some(MdlControllerType::from_raw(code));
179        }
180    }
181
182    None
183}
184
185/// Classification byte-to-string mapping.
186///
187/// The eight codes are independently established: all eight are attested in
188/// models indexed by a retail `chitin.key`.
189///
190/// The **name strings** cannot be attested, and that is a finding rather than
191/// an outstanding task. `swkotor.exe` carries no ASCII name for any
192/// classification bit: no `classification` string anywhere, no adjacent table
193/// of eight names, and the individual words that do occur all belong to
194/// unrelated subsystems. The ASCII model parser recognises four keywords and
195/// none of them is this. So the names are tool-side vocabulary with no
196/// engine-side term to compare against, and re-derivation from this binary is
197/// not available. See `docs/src/legal.md`.
198const CLASSIFICATIONS: &[(u8, &str)] = &[
199    (0, "other"),
200    (1, "effect"),
201    (2, "tile"),
202    (4, "character"),
203    (8, "door"),
204    (16, "lightsaber"),
205    (32, "placeable"),
206    (64, "flyer"),
207];
208
209/// Returns the ASCII classification string for a classification byte.
210///
211/// Defaults to `"Other"` for unrecognized codes.
212pub fn classification_to_ascii(code: u8) -> &'static str {
213    for &(c, name) in CLASSIFICATIONS {
214        if c == code {
215            return name;
216        }
217    }
218    "Other"
219}
220
221/// Returns the classification byte for an ASCII classification string.
222///
223/// Case-insensitive. Returns `None` for unrecognized strings.
224pub fn classification_from_ascii(name: &str) -> Option<u8> {
225    for &(code, ascii) in CLASSIFICATIONS {
226        if ascii.eq_ignore_ascii_case(name) {
227            return Some(code);
228        }
229    }
230    None
231}
232
233/// Returns the ASCII node type string for a node data variant.
234///
235/// These strings appear after the `node` keyword in ASCII MDL files
236/// (e.g., `node trimesh torso_g`). Camera nodes use `"dummy"` since the
237/// engine has no camera-specific ASCII type keyword.
238pub fn node_type_ascii_name(data: &MdlNodeData) -> &'static str {
239    match data {
240        MdlNodeData::Base => "dummy",
241        MdlNodeData::Light(_) => "light",
242        MdlNodeData::Emitter(_) => "emitter",
243        MdlNodeData::Camera(_) => "dummy",
244        MdlNodeData::Reference(_) => "reference",
245        MdlNodeData::Mesh(_) => "trimesh",
246        MdlNodeData::Skin(_) => "skin",
247        MdlNodeData::AnimMesh(_) => "animmesh",
248        MdlNodeData::Dangly(_) => "danglymesh",
249        MdlNodeData::Aabb(_) => "aabb",
250        MdlNodeData::Saber(_) => "lightsaber",
251    }
252}
253
254/// Returns the [`NodeTypeContext`] for a node data variant.
255///
256/// Used to select the correct controller name lookup table.
257pub fn node_type_context(data: &MdlNodeData) -> NodeTypeContext {
258    match data {
259        MdlNodeData::Base | MdlNodeData::Camera(_) => NodeTypeContext::Base,
260        MdlNodeData::Light(_) => NodeTypeContext::Light,
261        MdlNodeData::Emitter(_) => NodeTypeContext::Emitter,
262        MdlNodeData::Mesh(_)
263        | MdlNodeData::Skin(_)
264        | MdlNodeData::AnimMesh(_)
265        | MdlNodeData::Dangly(_)
266        | MdlNodeData::Aabb(_)
267        | MdlNodeData::Saber(_)
268        | MdlNodeData::Reference(_) => NodeTypeContext::Mesh,
269    }
270}
271
272/// Returns the default [`MdlNodeData`] variant for an ASCII node type string.
273///
274/// Case-insensitive. Unrecognized strings (including `"dummy"`) produce
275/// [`MdlNodeData::Base`].
276pub fn node_data_from_ascii_name(name: &str) -> MdlNodeData {
277    if name.eq_ignore_ascii_case("trimesh") {
278        MdlNodeData::Mesh(Default::default())
279    } else if name.eq_ignore_ascii_case("skin") {
280        MdlNodeData::Skin(MdlSkin::default())
281    } else if name.eq_ignore_ascii_case("danglymesh") {
282        MdlNodeData::Dangly(MdlDangly::default())
283    } else if name.eq_ignore_ascii_case("aabb") {
284        MdlNodeData::Aabb(MdlAabb::default())
285    } else if name.eq_ignore_ascii_case("lightsaber") {
286        MdlNodeData::Saber(MdlSaber::default())
287    } else if name.eq_ignore_ascii_case("light") {
288        MdlNodeData::Light(MdlLight::default())
289    } else if name.eq_ignore_ascii_case("emitter") {
290        MdlNodeData::Emitter(MdlEmitter::default())
291    } else if name.eq_ignore_ascii_case("reference") {
292        MdlNodeData::Reference(MdlReference::default())
293    } else if name.eq_ignore_ascii_case("animmesh") {
294        MdlNodeData::AnimMesh(MdlAnimMesh::default())
295    } else {
296        MdlNodeData::Base
297    }
298}
299
300/// Returns true for ASCII MDL keywords that should NOT be interpreted as
301/// inline controllers, even if they could parse as float values.
302///
303/// This list covers all mesh, light, emitter, dangly, aabb, skin, animmesh,
304/// and reference fields, plus structural keywords (node/endnode, model
305/// header directives, animation directives).
306pub fn is_non_controller_keyword(name: &str) -> bool {
307    const KEYWORDS: &[&str] = &[
308        "parent",
309        "bitmap",
310        "bitmap2",
311        "texture0",
312        "texture1",
313        "diffuse",
314        "ambient",
315        "transparencyhint",
316        "animateuv",
317        "uvdirectionx",
318        "uvdirectiony",
319        "uvjitter",
320        "uvjitterspeed",
321        "lightmapped",
322        "rotatetexture",
323        "m_bisbackgroundgeometry",
324        "shadow",
325        "beaming",
326        "render",
327        "verts",
328        "faces",
329        "tverts",
330        "tverts0",
331        "tverts1",
332        "tverts2",
333        "tverts3",
334        "colors",
335        "tangentspace",
336        "dirt_enabled",
337        "dirt_texture",
338        "dirt_worldspace",
339        "hologram_donotdraw",
340        "inv_count",
341        "weights",
342        "skinweights",
343        "constraints",
344        "displacement",
345        "tightness",
346        "period",
347        "aabb",
348        "lightpriority",
349        "ambientonly",
350        "ndynamictype",
351        "affectdynamic",
352        "generateflare",
353        "fadinglight",
354        "flareradius",
355        "lensflares",
356        "texturenames",
357        "flarepositions",
358        "flaresizes",
359        "flarecolorshifts",
360        "deadspace",
361        "blastradius",
362        "blastlength",
363        "numbranches",
364        "controlptsmoothing",
365        "xgrid",
366        "ygrid",
367        "spawntype",
368        "update",
369        "blend",
370        "texture",
371        "chunkname",
372        "twosidedtex",
373        "loop",
374        "renderorder",
375        "m_bframeblending",
376        "m_sdepthtexturename",
377        "p2p",
378        "p2p_sel",
379        "affectedbywind",
380        "m_istinted",
381        "bounce",
382        "random",
383        "inherit",
384        "inheritvel",
385        "inherit_local",
386        "splat",
387        "inherit_part",
388        "depth_texture",
389        "refmodel",
390        "reattachable",
391        "sampleperiod",
392        "animverts",
393        "animtverts",
394        "bmin",
395        "bmax",
396        "endnode",
397        "endmodelgeom",
398        "donemodel",
399        "doneanim",
400        "beginmodelgeom",
401        "newmodel",
402        "newanim",
403        "node",
404        "endlist",
405        "compress_quaternions",
406        "headlink",
407        "setanimationscale",
408        "ignorefog",
409        "classification",
410        "classification_unk1",
411        "setsupermodel",
412        "length",
413        "transtime",
414        "animroot",
415        "event",
416    ];
417    let lower = name.to_ascii_lowercase();
418    KEYWORDS.contains(&lower.as_str())
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    #[test]
426    fn base_controllers_all_contexts() {
427        // Base controllers should resolve in every context.
428        for ctx in [
429            NodeTypeContext::Base,
430            NodeTypeContext::Mesh,
431            NodeTypeContext::Light,
432            NodeTypeContext::Emitter,
433        ] {
434            assert_eq!(
435                controller_ascii_name(MdlControllerType::POSITION, ctx),
436                Some("position")
437            );
438            assert_eq!(
439                controller_ascii_name(MdlControllerType::ORIENTATION, ctx),
440                Some("orientation")
441            );
442            assert_eq!(
443                controller_ascii_name(MdlControllerType::SCALE, ctx),
444                Some("scale")
445            );
446        }
447    }
448
449    #[test]
450    fn code_100_disambiguation() {
451        // Code 100 means different things per node type.
452        assert_eq!(
453            controller_ascii_name(MdlControllerType::SELFILLUMCOLOR, NodeTypeContext::Mesh),
454            Some("selfillumcolor")
455        );
456        assert_eq!(
457            controller_ascii_name(
458                MdlControllerType::VERTICAL_DISPLACEMENT,
459                NodeTypeContext::Light
460            ),
461            Some("verticaldisplacement")
462        );
463        assert_eq!(
464            controller_ascii_name(MdlControllerType::DRAG, NodeTypeContext::Emitter),
465            Some("drag")
466        );
467    }
468
469    #[test]
470    fn reverse_lookup_case_insensitive() {
471        assert_eq!(
472            controller_from_ascii_name("Position", NodeTypeContext::Base),
473            Some(MdlControllerType::POSITION)
474        );
475        assert_eq!(
476            controller_from_ascii_name("SELFILLUMCOLOR", NodeTypeContext::Mesh),
477            Some(MdlControllerType::SELFILLUMCOLOR)
478        );
479        assert_eq!(
480            controller_from_ascii_name("birthrate", NodeTypeContext::Emitter),
481            Some(MdlControllerType::BIRTHRATE)
482        );
483    }
484
485    #[test]
486    fn unknown_controller_returns_none() {
487        assert_eq!(
488            controller_ascii_name(MdlControllerType::from_raw(9999), NodeTypeContext::Mesh),
489            None
490        );
491    }
492
493    #[test]
494    fn classification_roundtrip() {
495        for &(code, name) in CLASSIFICATIONS {
496            assert_eq!(classification_to_ascii(code), name);
497            assert_eq!(classification_from_ascii(name), Some(code));
498        }
499    }
500
501    #[test]
502    fn classification_case_insensitive() {
503        assert_eq!(classification_from_ascii("character"), Some(4));
504        assert_eq!(classification_from_ascii("CHARACTER"), Some(4));
505    }
506
507    #[test]
508    fn node_type_names() {
509        assert_eq!(node_type_ascii_name(&MdlNodeData::Base), "dummy");
510        assert_eq!(
511            node_type_ascii_name(&MdlNodeData::Mesh(Default::default())),
512            "trimesh"
513        );
514    }
515}