1use super::controllers::MdlControllerType;
15use super::types::{
16 MdlAabb, MdlAnimMesh, MdlDangly, MdlEmitter, MdlLight, MdlNodeData, MdlReference, MdlSaber,
17 MdlSkin,
18};
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum NodeTypeContext {
28 Base,
30 Mesh,
32 Light,
34 Emitter,
36}
37
38const BASE_CONTROLLERS: &[(u32, &str)] = &[(8, "position"), (20, "orientation"), (36, "scale")];
40
41const MESH_CONTROLLERS: &[(u32, &str)] = &[(100, "selfillumcolor"), (132, "alpha")];
43
44const LIGHT_CONTROLLERS: &[(u32, &str)] = &[
46 (76, "color"),
47 (88, "radius"),
48 (96, "shadowradius"),
49 (100, "verticaldisplacement"),
50 (140, "multiplier"),
51];
52
53const 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
109pub fn controller_ascii_name(
115 code: MdlControllerType,
116 ctx: NodeTypeContext,
117) -> Option<&'static str> {
118 let raw = code.raw();
119
120 for &(c, name) in BASE_CONTROLLERS {
122 if c == raw {
123 return Some(name);
124 }
125 }
126
127 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
144pub fn controller_from_ascii_name(name: &str, ctx: NodeTypeContext) -> Option<MdlControllerType> {
149 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 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
173const 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
188pub 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
200pub 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
212pub 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
233pub 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
251pub 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
279pub 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 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 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}