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