1use std::collections::HashMap;
13use std::io::BufRead;
14
15use super::ascii_names::{
16 classification_from_ascii, controller_from_ascii_name, is_non_controller_keyword,
17 node_data_from_ascii_name, node_type_context, NodeTypeContext,
18};
19use super::ascii_writer::MdlAsciiError;
20use super::controllers::{MdlController, MdlControllerType, MdlKey, CTRL_FLAG_BEZIER};
21use super::orientation::axis_angle_to_quat;
22use super::types::{
23 AabbNode, MdlAabb, MdlAnimMesh, MdlDangly, MdlEmitter, MdlFace, MdlLight, MdlMesh, MdlNodeData,
24 MdlReference, MdlSkin,
25};
26use super::{
27 collect_geo_positions, count_anim_nodes, count_nodes, Mdl, MdlAnimEvent, MdlAnimNode,
28 MdlAnimation, MdlNode,
29};
30
31#[cfg_attr(
44 feature = "tracing",
45 tracing::instrument(level = "debug", skip(reader))
46)]
47pub fn read_mdl_ascii<R: BufRead>(reader: R) -> Result<Mdl, MdlAsciiError> {
48 let mut raw_lines = Vec::new();
49 for (i, line) in reader.lines().enumerate() {
50 let line = line.map_err(MdlAsciiError::Io)?;
51 let trimmed = line.trim();
52 if !trimmed.is_empty() && !trimmed.starts_with('#') {
53 raw_lines.push((i + 1, trimmed.to_string()));
54 }
55 }
56 let mut parser = AsciiParser {
57 lines: raw_lines,
58 pos: 0,
59 };
60 parse_model(&mut parser)
61}
62
63#[cfg_attr(
70 feature = "tracing",
71 tracing::instrument(level = "debug", skip(s), fields(bytes_len = s.len()))
72)]
73pub fn read_mdl_ascii_from_str(s: &str) -> Result<Mdl, MdlAsciiError> {
74 read_mdl_ascii(std::io::Cursor::new(s))
75}
76
77struct AsciiParser {
82 lines: Vec<(usize, String)>,
83 pos: usize,
84}
85
86impl AsciiParser {
87 fn next_line(&mut self) -> Option<(usize, String)> {
92 if self.pos < self.lines.len() {
93 let (ln, ref mut s) = self.lines[self.pos];
94 self.pos += 1;
95 Some((ln, std::mem::take(s)))
96 } else {
97 None
98 }
99 }
100
101 fn peek_line(&self) -> Option<(usize, &str)> {
102 if self.pos < self.lines.len() {
103 let (ln, ref s) = self.lines[self.pos];
104 Some((ln, s.as_str()))
105 } else {
106 None
107 }
108 }
109
110 fn parse_err(&self, line: usize, msg: impl Into<String>) -> MdlAsciiError {
111 MdlAsciiError::Parse {
112 line,
113 message: msg.into(),
114 }
115 }
116}
117
118fn tokens(line: &str) -> Vec<&str> {
119 line.split_whitespace().collect()
120}
121
122fn parse_f32(s: &str, line: usize) -> Result<f32, MdlAsciiError> {
123 s.parse::<f32>().map_err(|_| MdlAsciiError::Parse {
124 line,
125 message: format!("invalid float: {s}"),
126 })
127}
128
129fn parse_u32(s: &str, line: usize) -> Result<u32, MdlAsciiError> {
130 s.parse::<u32>().map_err(|_| MdlAsciiError::Parse {
131 line,
132 message: format!("invalid u32: {s}"),
133 })
134}
135
136fn parse_i32(s: &str, line: usize) -> Result<i32, MdlAsciiError> {
137 s.parse::<i32>().map_err(|_| MdlAsciiError::Parse {
138 line,
139 message: format!("invalid i32: {s}"),
140 })
141}
142
143fn parse_u16(s: &str, line: usize) -> Result<u16, MdlAsciiError> {
144 s.parse::<u16>().map_err(|_| MdlAsciiError::Parse {
145 line,
146 message: format!("invalid u16: {s}"),
147 })
148}
149
150fn parse_u8(s: &str, line: usize) -> Result<u8, MdlAsciiError> {
151 s.parse::<u8>().map_err(|_| MdlAsciiError::Parse {
152 line,
153 message: format!("invalid u8: {s}"),
154 })
155}
156
157fn eq_ci(a: &str, b: &str) -> bool {
158 a.eq_ignore_ascii_case(b)
159}
160
161struct FlatNode {
166 name: String,
167 parent_name: String, position: [f32; 3],
169 rotation: [f32; 4], node_data: MdlNodeData,
171 controllers: Vec<MdlController>,
172}
173
174struct FlatAnimNode {
175 name: String,
176 parent_name: String,
177 controllers: Vec<MdlController>,
178}
179
180fn parse_model(p: &mut AsciiParser) -> Result<Mdl, MdlAsciiError> {
185 let mut _model_name = String::new();
186 let mut supermodel_name = String::new();
187 let mut classification: u8 = 0;
188 let mut subclassification: u8 = 0;
189 let mut affected_by_fog: u8 = 1;
190 let mut animation_scale: f32 = 1.0;
191 let mut headlink = false;
192 let mut bounding_box = [0.0f32; 6];
193 let mut radius: f32 = 0.0;
194 let mut geo_nodes: Vec<FlatNode> = Vec::new();
195 let mut animations: Vec<MdlAnimation> = Vec::new();
196
197 while let Some((ln, line)) = p.next_line() {
198 let toks = tokens(&line);
199 if toks.is_empty() {
200 continue;
201 }
202 let kw = toks[0];
203
204 if eq_ci(kw, "newmodel") {
205 if toks.len() >= 2 {
206 _model_name = toks[1].to_string();
207 }
208 } else if eq_ci(kw, "setsupermodel") {
209 if toks.len() >= 3 {
210 supermodel_name = toks[2].to_string();
211 }
212 } else if eq_ci(kw, "classification") && toks.len() >= 2 {
213 classification = classification_from_ascii(toks[1]).unwrap_or(0);
214 } else if eq_ci(kw, "classification_unk1") && toks.len() >= 2 {
215 subclassification = parse_u8(toks[1], ln)?;
216 } else if eq_ci(kw, "ignorefog") && toks.len() >= 2 {
217 let v = parse_i32(toks[1], ln)?;
218 affected_by_fog = if v != 0 { 0 } else { 1 };
219 } else if eq_ci(kw, "setanimationscale") && toks.len() >= 2 {
220 animation_scale = parse_f32(toks[1], ln)?;
221 } else if eq_ci(kw, "compress_quaternions") {
222 } else if eq_ci(kw, "headlink") && toks.len() >= 2 {
224 headlink = parse_i32(toks[1], ln)? != 0;
225 } else if eq_ci(kw, "beginmodelgeom") {
226 parse_geometry_block(p, &mut bounding_box, &mut radius, &mut geo_nodes)?;
228 } else if eq_ci(kw, "newanim") && toks.len() >= 3 {
229 let anim = parse_animation(p, toks[1], toks[2], ln)?;
230 animations.push(anim);
231 } else if eq_ci(kw, "donemodel") {
232 break;
233 }
234 }
236
237 let root_node = assemble_node_tree(geo_nodes)?;
239 let mut node_count = count_nodes(&root_node);
240
241 let geo_positions = collect_geo_positions(&root_node);
243
244 let name_to_index = build_name_index_map(&root_node);
246
247 for anim in &mut animations {
251 subtract_geo_positions_from_anim(&mut anim.root_node, &geo_positions);
252 assign_anim_node_numbers(&mut anim.root_node, &name_to_index);
253 }
254
255 for anim in &animations {
258 node_count += count_anim_nodes(&anim.root_node);
259 }
260
261 let anim_root_node = if headlink {
263 animations
264 .first()
265 .map(|a| a.anim_root.clone())
266 .filter(|s| !s.is_empty())
267 } else {
268 None
269 };
270
271 Ok(Mdl {
272 root_node,
273 geometry_fn_ptr1: 0,
274 geometry_fn_ptr2: 0,
275 model_type: 2,
276 classification,
277 subclassification,
278 affected_by_fog,
279 supermodel_name,
280 node_count,
281 bounding_box,
282 radius,
283 animation_scale,
284 animations,
285 anim_root_node,
286 })
287}
288
289fn parse_geometry_block(
294 p: &mut AsciiParser,
295 bbox: &mut [f32; 6],
296 radius: &mut f32,
297 nodes: &mut Vec<FlatNode>,
298) -> Result<(), MdlAsciiError> {
299 while let Some((ln, line)) = p.next_line() {
300 let toks = tokens(&line);
301 if toks.is_empty() {
302 continue;
303 }
304 let kw = toks[0];
305
306 if eq_ci(kw, "bmin") && toks.len() >= 4 {
307 bbox[0] = parse_f32(toks[1], ln)?;
308 bbox[1] = parse_f32(toks[2], ln)?;
309 bbox[2] = parse_f32(toks[3], ln)?;
310 } else if eq_ci(kw, "bmax") && toks.len() >= 4 {
311 bbox[3] = parse_f32(toks[1], ln)?;
312 bbox[4] = parse_f32(toks[2], ln)?;
313 bbox[5] = parse_f32(toks[3], ln)?;
314 } else if eq_ci(kw, "radius") && toks.len() >= 2 {
315 *radius = parse_f32(toks[1], ln)?;
316 } else if eq_ci(kw, "node") && toks.len() >= 3 {
317 let flat = parse_geometry_node(p, toks[1], toks[2], ln)?;
318 nodes.push(flat);
319 } else if eq_ci(kw, "endmodelgeom") {
320 break;
321 }
322 }
323 Ok(())
324}
325
326fn parse_geometry_node(
331 p: &mut AsciiParser,
332 type_str: &str,
333 name: &str,
334 _node_line: usize,
335) -> Result<FlatNode, MdlAsciiError> {
336 let mut flat = FlatNode {
337 name: name.to_string(),
338 parent_name: "NULL".into(),
339 position: [0.0; 3],
340 rotation: [1.0, 0.0, 0.0, 0.0],
341 node_data: node_data_from_type_str(type_str),
342 controllers: Vec::new(),
343 };
344
345 let ctx = node_type_context(&flat.node_data);
346
347 while let Some((ln, line)) = p.next_line() {
348 let toks = tokens(&line);
349 if toks.is_empty() {
350 continue;
351 }
352 let kw = toks[0];
353
354 if eq_ci(kw, "endnode") {
355 break;
356 } else if eq_ci(kw, "parent") && toks.len() >= 2 {
357 flat.parent_name = toks[1].to_string();
358 } else if eq_ci(kw, "position") && toks.len() >= 4 {
359 flat.position = [
363 parse_f32(toks[1], ln)?,
364 parse_f32(toks[2], ln)?,
365 parse_f32(toks[3], ln)?,
366 ];
367 } else if eq_ci(kw, "orientation") && toks.len() >= 5 {
368 let aa = [
369 parse_f32(toks[1], ln)?,
370 parse_f32(toks[2], ln)?,
371 parse_f32(toks[3], ln)?,
372 parse_f32(toks[4], ln)?,
373 ];
374 flat.rotation = axis_angle_to_quat(aa);
375 } else if try_parse_controller_line(p, &toks, ln, ctx, &mut flat.controllers)? {
376 } else {
378 parse_node_field(&toks, ln, p, &mut flat.node_data)?;
380 }
381 }
382
383 if let Some(mesh) = flat.node_data.mesh_mut() {
385 mesh.vertex_count = u16::try_from(mesh.positions.len())
386 .map_err(|_| MdlAsciiError::InvalidData("vertex count exceeds u16".into()))?;
387 mesh.indices_per_face = 3;
388 let mut tc: u16 = 0;
390 if !mesh.uv1.is_empty() {
391 tc += 1;
392 }
393 if !mesh.uv2.is_empty() {
394 tc += 1;
395 }
396 if !mesh.uv3.is_empty() {
397 tc += 1;
398 }
399 if !mesh.uv4.is_empty() {
400 tc += 1;
401 }
402 mesh.texture_channel_count = tc;
403 mesh.recompute_derived_fields();
404 }
405
406 Ok(flat)
407}
408
409fn node_data_from_type_str(s: &str) -> MdlNodeData {
410 node_data_from_ascii_name(s)
411}
412
413fn try_parse_controller_line(
420 p: &mut AsciiParser,
421 toks: &[&str],
422 ln: usize,
423 ctx: NodeTypeContext,
424 controllers: &mut Vec<MdlController>,
425) -> Result<bool, MdlAsciiError> {
426 if toks.is_empty() {
427 return Ok(false);
428 }
429
430 let kw = toks[0];
431
432 let (base_name, is_bezier) = if let Some(base) = strip_suffix_ci(kw, "bezierkey") {
434 (base, true)
435 } else if let Some(base) = strip_suffix_ci(kw, "key") {
436 (base, false)
437 } else {
438 return try_parse_inline_controller(toks, ln, ctx, controllers);
440 };
441
442 let ctrl_type = resolve_controller_type(base_name, ctx)?;
444
445 let is_orientation = ctrl_type == MdlControllerType::ORIENTATION;
447 let mut keys = Vec::new();
448
449 while let Some((kln, kline)) = p.next_line() {
450 let ktoks = tokens(&kline);
451 if ktoks.is_empty() {
452 continue;
453 }
454 if eq_ci(ktoks[0], "endlist") {
455 break;
456 }
457 if ktoks.len() < 2 {
458 continue;
459 }
460
461 let time = parse_f32(ktoks[0], kln)?;
462 let mut values: Vec<f32> = Vec::new();
463 for t in &ktoks[1..] {
464 values.push(parse_f32(t, kln)?);
465 }
466
467 if is_orientation && values.len() >= 4 {
469 let aa = [values[0], values[1], values[2], values[3]];
470 let q = axis_angle_to_quat(aa); values[0] = q[1];
473 values[1] = q[2];
474 values[2] = q[3];
475 values[3] = q[0];
476 }
477
478 keys.push(MdlKey { time, values });
479 }
480
481 let col_count = if let Some(first_key) = keys.first() {
482 u8::try_from(first_key.values.len()).map_err(|_| MdlAsciiError::Parse {
483 line: ln,
484 message: "column count exceeds u8".into(),
485 })?
486 } else {
487 0
488 };
489 let raw_column_count = if is_bezier {
490 col_count | CTRL_FLAG_BEZIER
491 } else {
492 col_count
493 };
494
495 controllers.push(MdlController {
496 controller_type: ctrl_type,
497 raw_column_count,
498 key_unknown_04: [0; 2],
499 key_unknown_0d: [0; 3],
500 keys,
501 });
502
503 Ok(true)
504}
505
506fn try_parse_inline_controller(
508 toks: &[&str],
509 ln: usize,
510 ctx: NodeTypeContext,
511 controllers: &mut Vec<MdlController>,
512) -> Result<bool, MdlAsciiError> {
513 if toks.len() < 2 {
514 return Ok(false);
515 }
516
517 let name = toks[0];
518
519 if is_non_controller_keyword(name) {
522 return Ok(false);
523 }
524
525 let ctrl_type = match resolve_controller_type_optional(name, ctx) {
527 Some(ct) => ct,
528 None => return Ok(false),
529 };
530
531 let is_orientation = ctrl_type == MdlControllerType::ORIENTATION;
533 let mut values: Vec<f32> = Vec::new();
534 for t in &toks[1..] {
535 match t.parse::<f32>() {
536 Ok(v) => values.push(v),
537 Err(_) => return Ok(false), }
539 }
540
541 if values.is_empty() {
542 return Ok(false);
543 }
544
545 if is_orientation && values.len() >= 4 {
547 let aa = [values[0], values[1], values[2], values[3]];
548 let q = axis_angle_to_quat(aa);
549 values[0] = q[1];
550 values[1] = q[2];
551 values[2] = q[3];
552 values[3] = q[0];
553 }
554
555 let raw_column_count = u8::try_from(values.len()).map_err(|_| MdlAsciiError::Parse {
556 line: ln,
557 message: "column count exceeds u8".into(),
558 })?;
559
560 controllers.push(MdlController {
561 controller_type: ctrl_type,
562 raw_column_count,
563 key_unknown_04: [0; 2],
564 key_unknown_0d: [0; 3],
565 keys: vec![MdlKey { time: 0.0, values }],
566 });
567
568 Ok(true)
569}
570
571fn resolve_controller_type(
574 name: &str,
575 ctx: NodeTypeContext,
576) -> Result<MdlControllerType, MdlAsciiError> {
577 resolve_controller_type_optional(name, ctx)
578 .ok_or_else(|| MdlAsciiError::InvalidData(format!("unknown controller: {name}")))
579}
580
581fn resolve_controller_type_optional(name: &str, ctx: NodeTypeContext) -> Option<MdlControllerType> {
582 if let Some(ct) = controller_from_ascii_name(name, ctx) {
584 return Some(ct);
585 }
586 for alt_ctx in &[
588 NodeTypeContext::Base,
589 NodeTypeContext::Mesh,
590 NodeTypeContext::Light,
591 NodeTypeContext::Emitter,
592 ] {
593 if let Some(ct) = controller_from_ascii_name(name, *alt_ctx) {
594 return Some(ct);
595 }
596 }
597 let lower = name.to_ascii_lowercase();
599 if let Some(num_str) = lower.strip_prefix("controller_") {
600 if let Ok(code) = num_str.parse::<u32>() {
601 return Some(MdlControllerType::from_raw(code));
602 }
603 }
604 None
605}
606
607fn parse_node_field(
612 toks: &[&str],
613 ln: usize,
614 p: &mut AsciiParser,
615 data: &mut MdlNodeData,
616) -> Result<(), MdlAsciiError> {
617 if let Some(mesh) = data.mesh_mut() {
619 if parse_mesh_field(toks, ln, p, mesh)? {
620 return Ok(());
621 }
622 }
623
624 let handled = match data {
626 MdlNodeData::Skin(skin) => parse_skin_field(toks, ln, p, skin)?,
627 MdlNodeData::Dangly(dangly) => parse_dangly_field(toks, ln, p, dangly)?,
628 MdlNodeData::Aabb(aabb) => parse_aabb_field(toks, ln, p, aabb)?,
629 MdlNodeData::Light(light) => parse_light_field(toks, ln, p, light)?,
630 MdlNodeData::Emitter(emitter) => parse_emitter_field(toks, ln, p, emitter)?,
631 MdlNodeData::Reference(reference) => parse_reference_field(toks, ln, reference)?,
632 MdlNodeData::AnimMesh(animmesh) => parse_animmesh_field(toks, ln, p, animmesh)?,
633 _ => false,
634 };
635
636 if handled {
637 return Ok(());
638 }
639
640 Ok(())
642}
643
644fn parse_mesh_field(
649 toks: &[&str],
650 ln: usize,
651 p: &mut AsciiParser,
652 mesh: &mut MdlMesh,
653) -> Result<bool, MdlAsciiError> {
654 let kw = toks[0];
655
656 if eq_ci(kw, "diffuse") && toks.len() >= 4 {
657 mesh.diffuse_color = [
658 parse_f32(toks[1], ln)?,
659 parse_f32(toks[2], ln)?,
660 parse_f32(toks[3], ln)?,
661 ];
662 } else if eq_ci(kw, "ambient") && toks.len() >= 4 {
663 mesh.ambient_color = [
664 parse_f32(toks[1], ln)?,
665 parse_f32(toks[2], ln)?,
666 parse_f32(toks[3], ln)?,
667 ];
668 } else if eq_ci(kw, "transparencyhint") && toks.len() >= 2 {
669 mesh.transparency_hint = parse_i32(toks[1], ln)?;
670 } else if eq_ci(kw, "animateuv") && toks.len() >= 2 {
671 mesh.animate_uv = parse_i32(toks[1], ln)?;
672 } else if eq_ci(kw, "uvdirectionx") && toks.len() >= 2 {
673 mesh.uv_direction_x = parse_f32(toks[1], ln)?;
674 } else if eq_ci(kw, "uvdirectiony") && toks.len() >= 2 {
675 mesh.uv_direction_y = parse_f32(toks[1], ln)?;
676 } else if eq_ci(kw, "uvjitter") && toks.len() >= 2 {
677 mesh.uv_jitter = parse_f32(toks[1], ln)?;
678 } else if eq_ci(kw, "uvjitterspeed") && toks.len() >= 2 {
679 mesh.uv_jitter_speed = parse_f32(toks[1], ln)?;
680 } else if eq_ci(kw, "lightmapped") && toks.len() >= 2 {
681 mesh.light_mapped = parse_i32(toks[1], ln)? != 0;
682 } else if eq_ci(kw, "rotatetexture") && toks.len() >= 2 {
683 mesh.rotate_texture = parse_i32(toks[1], ln)? != 0;
684 } else if eq_ci(kw, "m_bIsBackgroundGeometry") && toks.len() >= 2 {
685 mesh.is_background_geometry = parse_i32(toks[1], ln)? != 0;
686 } else if eq_ci(kw, "shadow") && toks.len() >= 2 {
687 mesh.shadow = parse_i32(toks[1], ln)? != 0;
688 } else if eq_ci(kw, "beaming") && toks.len() >= 2 {
689 mesh.beaming = parse_i32(toks[1], ln)? != 0;
690 } else if eq_ci(kw, "render") && toks.len() >= 2 {
691 mesh.render = parse_i32(toks[1], ln)? != 0;
692 } else if (eq_ci(kw, "bitmap") || eq_ci(kw, "texture0")) && toks.len() >= 2 {
693 let val = toks[1];
694 mesh.texture_0 = if eq_ci(val, "NULL") {
695 String::new()
696 } else {
697 val.to_string()
698 };
699 } else if (eq_ci(kw, "bitmap2") || eq_ci(kw, "texture1")) && toks.len() >= 2 {
700 let val = toks[1];
701 mesh.texture_1 = if eq_ci(val, "NULL") {
702 String::new()
703 } else {
704 val.to_string()
705 };
706 } else if eq_ci(kw, "inv_count") && toks.len() >= 2 {
707 mesh.inverted_counter = parse_u32(toks[1], ln)?;
708 } else if eq_ci(kw, "verts") && toks.len() >= 2 {
709 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
710 mesh.positions = parse_vec3_block(p, count)?;
711 } else if eq_ci(kw, "faces") && toks.len() >= 2 {
712 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
713 mesh.faces = parse_face_block(p, count)?;
714 } else if (eq_ci(kw, "tverts") || eq_ci(kw, "tverts0")) && toks.len() >= 2 {
715 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
716 mesh.uv1 = parse_uv_block(p, count)?;
717 } else if eq_ci(kw, "tverts1") && toks.len() >= 2 {
718 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
719 mesh.uv2 = parse_uv_block(p, count)?;
720 } else if eq_ci(kw, "tverts2") && toks.len() >= 2 {
721 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
722 mesh.uv3 = parse_uv_block(p, count)?;
723 } else if eq_ci(kw, "tverts3") && toks.len() >= 2 {
724 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
725 mesh.uv4 = parse_uv_block(p, count)?;
726 } else if eq_ci(kw, "colors") && toks.len() >= 2 {
727 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
728 mesh.vertex_colors = parse_color_block(p, count)?;
729 } else if eq_ci(kw, "tangentspace")
730 || eq_ci(kw, "dirt_enabled")
731 || eq_ci(kw, "dirt_texture")
732 || eq_ci(kw, "dirt_worldspace")
733 || eq_ci(kw, "hologram_donotdraw")
734 {
735 } else {
737 return Ok(false);
738 }
739
740 Ok(true)
741}
742
743fn parse_vec3_block(p: &mut AsciiParser, count: usize) -> Result<Vec<[f32; 3]>, MdlAsciiError> {
744 let mut result = Vec::with_capacity(count);
745 for _ in 0..count {
746 let (ln, line) = p
747 .next_line()
748 .ok_or_else(|| MdlAsciiError::InvalidData("unexpected EOF in vec3 block".into()))?;
749 let toks = tokens(&line);
750 if toks.len() < 3 {
751 return Err(p.parse_err(ln, "expected 3 floats"));
752 }
753 result.push([
754 parse_f32(toks[0], ln)?,
755 parse_f32(toks[1], ln)?,
756 parse_f32(toks[2], ln)?,
757 ]);
758 }
759 Ok(result)
760}
761
762fn parse_face_block(p: &mut AsciiParser, count: usize) -> Result<Vec<MdlFace>, MdlAsciiError> {
763 let mut faces = Vec::with_capacity(count);
764 for _ in 0..count {
765 let (ln, line) = p
766 .next_line()
767 .ok_or_else(|| MdlAsciiError::InvalidData("unexpected EOF in face block".into()))?;
768 let toks = tokens(&line);
769 if toks.len() < 8 {
770 return Err(p.parse_err(ln, "expected 8 values in face line"));
771 }
772 let v0 = parse_u16(toks[0], ln)?;
773 let v1 = parse_u16(toks[1], ln)?;
774 let v2 = parse_u16(toks[2], ln)?;
775 let surface_id = parse_u32(toks[7], ln)?;
778
779 faces.push(MdlFace {
780 plane_normal: [0.0; 3], plane_distance: 0.0, surface_id,
783 adjacent: [0xFFFF; 3], vertex_indices: [v0, v1, v2],
785 });
786 }
787 Ok(faces)
788}
789
790fn parse_uv_block(p: &mut AsciiParser, count: usize) -> Result<Vec<[f32; 2]>, MdlAsciiError> {
791 let mut uvs = Vec::with_capacity(count);
792 for _ in 0..count {
793 let (ln, line) = p
794 .next_line()
795 .ok_or_else(|| MdlAsciiError::InvalidData("unexpected EOF in UV block".into()))?;
796 let toks = tokens(&line);
797 if toks.len() < 2 {
798 return Err(p.parse_err(ln, "expected at least 2 floats in UV line"));
799 }
800 uvs.push([parse_f32(toks[0], ln)?, parse_f32(toks[1], ln)?]);
802 }
803 Ok(uvs)
804}
805
806fn parse_color_block(p: &mut AsciiParser, count: usize) -> Result<Vec<[u8; 4]>, MdlAsciiError> {
807 let mut colors = Vec::with_capacity(count);
808 for _ in 0..count {
809 let (ln, line) = p
810 .next_line()
811 .ok_or_else(|| MdlAsciiError::InvalidData("unexpected EOF in color block".into()))?;
812 let toks = tokens(&line);
813 if toks.len() < 3 {
814 return Err(p.parse_err(ln, "expected 3 floats in color line"));
815 }
816 #[allow(
818 clippy::cast_possible_truncation,
819 clippy::cast_sign_loss,
820 clippy::as_conversions
821 )]
822 let r = (parse_f32(toks[0], ln)? * 255.0).round().clamp(0.0, 255.0) as u8;
823 #[allow(
824 clippy::cast_possible_truncation,
825 clippy::cast_sign_loss,
826 clippy::as_conversions
827 )]
828 let g = (parse_f32(toks[1], ln)? * 255.0).round().clamp(0.0, 255.0) as u8;
829 #[allow(
830 clippy::cast_possible_truncation,
831 clippy::cast_sign_loss,
832 clippy::as_conversions
833 )]
834 let b = (parse_f32(toks[2], ln)? * 255.0).round().clamp(0.0, 255.0) as u8;
835 colors.push([r, g, b, 255]);
836 }
837 Ok(colors)
838}
839
840fn parse_skin_field(
845 toks: &[&str],
846 ln: usize,
847 p: &mut AsciiParser,
848 skin: &mut MdlSkin,
849) -> Result<bool, MdlAsciiError> {
850 let kw = toks[0];
851 if (eq_ci(kw, "weights") || eq_ci(kw, "skinweights")) && toks.len() >= 2 {
852 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
853 parse_skin_weights(p, count, skin)?;
854 Ok(true)
855 } else {
856 Ok(false)
857 }
858}
859
860struct VertexWeights {
862 pairs: Vec<(String, f32)>,
864}
865
866fn parse_skin_weights(
867 p: &mut AsciiParser,
868 count: usize,
869 skin: &mut MdlSkin,
870) -> Result<(), MdlAsciiError> {
871 let mut vertex_weights: Vec<VertexWeights> = Vec::with_capacity(count);
872
873 for _ in 0..count {
874 let (ln, line) = p
875 .next_line()
876 .ok_or_else(|| MdlAsciiError::InvalidData("unexpected EOF in weights block".into()))?;
877 let toks = tokens(&line);
878 let mut pairs = Vec::new();
879 let mut i = 0;
880 while i + 1 < toks.len() && pairs.len() < 4 {
881 let bone_name = toks[i].to_string();
882 let weight = parse_f32(toks[i + 1], ln)?;
883 if weight > 0.0 {
884 pairs.push((bone_name, weight));
885 }
886 i += 2;
887 }
888 vertex_weights.push(VertexWeights { pairs });
889 }
890
891 let mut bone_name_to_idx: HashMap<String, usize> = HashMap::new();
893 for vw in &vertex_weights {
894 for (name, _) in &vw.pairs {
895 let next_idx = bone_name_to_idx.len();
896 bone_name_to_idx.entry(name.clone()).or_insert(next_idx);
897 }
898 }
899
900 skin.mdx_bone_weights_offset = 0;
904 skin.mdx_bone_indices_offset = 0;
905
906 let mut bone_weights_vec = Vec::with_capacity(count);
907 let mut bone_indices_vec = Vec::with_capacity(count);
908 for vw in &vertex_weights {
909 let mut weights = [0.0f32; 4];
910 let mut indices = [0.0f32; 4];
911 for (j, (name, weight)) in vw.pairs.iter().enumerate().take(4) {
912 let idx = *bone_name_to_idx.get(name).unwrap_or(&0);
913 weights[j] = *weight;
914 #[allow(clippy::cast_precision_loss, clippy::as_conversions)]
917 let idx_f32 = idx as f32;
918 indices[j] = idx_f32;
919 }
920 bone_weights_vec.push(weights);
921 bone_indices_vec.push(indices);
922 }
923 skin.bone_weights = bone_weights_vec;
924 skin.bone_indices = bone_indices_vec;
925
926 skin.bonemap = Vec::new();
929
930 Ok(())
931}
932
933fn parse_dangly_field(
938 toks: &[&str],
939 ln: usize,
940 p: &mut AsciiParser,
941 dangly: &mut MdlDangly,
942) -> Result<bool, MdlAsciiError> {
943 let kw = toks[0];
944
945 if eq_ci(kw, "displacement") && toks.len() >= 2 {
946 dangly.displacement = parse_f32(toks[1], ln)?;
947 } else if eq_ci(kw, "tightness") && toks.len() >= 2 {
948 dangly.tightness = parse_f32(toks[1], ln)?;
949 } else if eq_ci(kw, "period") && toks.len() >= 2 {
950 dangly.period = parse_f32(toks[1], ln)?;
951 } else if eq_ci(kw, "constraints") && toks.len() >= 2 {
952 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
953 dangly.constraints = parse_float_block(p, count)?;
954 } else {
955 return Ok(false);
956 }
957 Ok(true)
958}
959
960fn parse_float_block(p: &mut AsciiParser, count: usize) -> Result<Vec<f32>, MdlAsciiError> {
961 let mut result = Vec::with_capacity(count);
962 for _ in 0..count {
963 let (ln, line) = p
964 .next_line()
965 .ok_or_else(|| MdlAsciiError::InvalidData("unexpected EOF in float block".into()))?;
966 let toks = tokens(&line);
967 if toks.is_empty() {
968 return Err(p.parse_err(ln, "expected float value"));
969 }
970 result.push(parse_f32(toks[0], ln)?);
971 }
972 Ok(result)
973}
974
975fn parse_aabb_field(
980 toks: &[&str],
981 _ln: usize,
982 p: &mut AsciiParser,
983 aabb: &mut MdlAabb,
984) -> Result<bool, MdlAsciiError> {
985 if !eq_ci(toks[0], "aabb") {
986 return Ok(false);
987 }
988
989 let mut leaves: Vec<AabbLeaf> = Vec::new();
991 while let Some((ln, line)) = p.peek_line() {
992 let toks = tokens(line);
993 if toks.len() < 7 {
994 break;
995 }
996 let vals: Result<Vec<f32>, _> = toks[..7].iter().map(|t| parse_f32(t, ln)).collect();
998 match vals {
999 Ok(v) => {
1000 p.next_line(); leaves.push(AabbLeaf {
1002 box_min: [v[0], v[1], v[2]],
1003 box_max: [v[3], v[4], v[5]],
1004 #[allow(clippy::cast_possible_truncation, clippy::as_conversions)]
1007 face_index: v[6] as i32,
1008 });
1009 }
1010 Err(_) => break,
1011 }
1012 }
1013
1014 if !leaves.is_empty() {
1015 aabb.aabb_tree = Some(Box::new(build_aabb_tree(&leaves)));
1016 }
1017
1018 Ok(true)
1019}
1020
1021struct AabbLeaf {
1022 box_min: [f32; 3],
1023 box_max: [f32; 3],
1024 face_index: i32,
1025}
1026
1027fn build_aabb_tree(leaves: &[AabbLeaf]) -> AabbNode {
1029 if leaves.len() == 1 {
1030 return AabbNode {
1031 box_min: leaves[0].box_min,
1032 box_max: leaves[0].box_max,
1033 face_index: leaves[0].face_index,
1034 split_direction_flags: 0,
1035 left: None,
1036 right: None,
1037 };
1038 }
1039
1040 let mut combined_min = [f32::MAX; 3];
1042 let mut combined_max = [f32::MIN; 3];
1043 for leaf in leaves {
1044 for i in 0..3 {
1045 combined_min[i] = combined_min[i].min(leaf.box_min[i]);
1046 combined_max[i] = combined_max[i].max(leaf.box_max[i]);
1047 }
1048 }
1049
1050 let extents = [
1052 combined_max[0] - combined_min[0],
1053 combined_max[1] - combined_min[1],
1054 combined_max[2] - combined_min[2],
1055 ];
1056 let axis = if extents[0] >= extents[1] && extents[0] >= extents[2] {
1057 0
1058 } else if extents[1] >= extents[2] {
1059 1
1060 } else {
1061 2
1062 };
1063
1064 let mut sorted: Vec<usize> = (0..leaves.len()).collect();
1066 sorted.sort_by(|&a, &b| {
1067 let ca = (leaves[a].box_min[axis] + leaves[a].box_max[axis]) * 0.5;
1068 let cb = (leaves[b].box_min[axis] + leaves[b].box_max[axis]) * 0.5;
1069 ca.partial_cmp(&cb).unwrap_or(std::cmp::Ordering::Equal)
1070 });
1071
1072 let mid = sorted.len() / 2;
1074 let left_leaves: Vec<AabbLeaf> = sorted[..mid]
1075 .iter()
1076 .map(|&i| AabbLeaf {
1077 box_min: leaves[i].box_min,
1078 box_max: leaves[i].box_max,
1079 face_index: leaves[i].face_index,
1080 })
1081 .collect();
1082 let right_leaves: Vec<AabbLeaf> = sorted[mid..]
1083 .iter()
1084 .map(|&i| AabbLeaf {
1085 box_min: leaves[i].box_min,
1086 box_max: leaves[i].box_max,
1087 face_index: leaves[i].face_index,
1088 })
1089 .collect();
1090
1091 let left = build_aabb_tree(&left_leaves);
1092 let right = build_aabb_tree(&right_leaves);
1093
1094 let split_flags = 1u32 << axis;
1096
1097 AabbNode {
1098 box_min: combined_min,
1099 box_max: combined_max,
1100 face_index: -1,
1101 split_direction_flags: split_flags,
1102 left: Some(Box::new(left)),
1103 right: Some(Box::new(right)),
1104 }
1105}
1106
1107fn parse_light_field(
1112 toks: &[&str],
1113 ln: usize,
1114 p: &mut AsciiParser,
1115 light: &mut MdlLight,
1116) -> Result<bool, MdlAsciiError> {
1117 let kw = toks[0];
1118
1119 if eq_ci(kw, "lightpriority") && toks.len() >= 2 {
1120 light.priority = parse_i32(toks[1], ln)?;
1121 } else if eq_ci(kw, "ambientonly") && toks.len() >= 2 {
1122 light.ambientonly = parse_i32(toks[1], ln)?;
1123 } else if eq_ci(kw, "ndynamictype") && toks.len() >= 2 {
1124 light.num_dynamic_types = parse_i32(toks[1], ln)?;
1125 } else if eq_ci(kw, "affectdynamic") && toks.len() >= 2 {
1126 light.affectdynamic = parse_i32(toks[1], ln)?;
1127 } else if eq_ci(kw, "shadow") && toks.len() >= 2 {
1128 light.shadow = parse_i32(toks[1], ln)?;
1129 } else if eq_ci(kw, "generateflare") && toks.len() >= 2 {
1130 light.generateflare = parse_i32(toks[1], ln)?;
1131 } else if eq_ci(kw, "fadingLight") && toks.len() >= 2 {
1132 light.fading_light = parse_i32(toks[1], ln)?;
1133 } else if eq_ci(kw, "flareradius") && toks.len() >= 2 {
1134 light.flare_radius = parse_f32(toks[1], ln)?;
1135 } else if eq_ci(kw, "lensflares") {
1136 } else if eq_ci(kw, "texturenames") && toks.len() >= 2 {
1138 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
1139 light.flare_texture_names = parse_string_block(p, count)?;
1140 } else if eq_ci(kw, "flarepositions") && toks.len() >= 2 {
1141 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
1142 light.flare_positions = parse_float_block(p, count)?;
1143 } else if eq_ci(kw, "flaresizes") && toks.len() >= 2 {
1144 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
1145 light.flare_sizes = parse_float_block(p, count)?;
1146 } else if eq_ci(kw, "flarecolorshifts") && toks.len() >= 2 {
1147 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
1148 light.flare_color_shifts = parse_vec3_block(p, count)?;
1149 } else {
1150 return Ok(false);
1151 }
1152 Ok(true)
1153}
1154
1155fn parse_string_block(p: &mut AsciiParser, count: usize) -> Result<Vec<String>, MdlAsciiError> {
1156 let mut result = Vec::with_capacity(count);
1157 for _ in 0..count {
1158 let (_ln, line) = p
1159 .next_line()
1160 .ok_or_else(|| MdlAsciiError::InvalidData("unexpected EOF in string block".into()))?;
1161 result.push(line.trim().to_string());
1162 }
1163 Ok(result)
1164}
1165
1166fn parse_emitter_field(
1171 toks: &[&str],
1172 ln: usize,
1173 _p: &mut AsciiParser,
1174 em: &mut MdlEmitter,
1175) -> Result<bool, MdlAsciiError> {
1176 let kw = toks[0];
1177
1178 if eq_ci(kw, "deadspace") && toks.len() >= 2 {
1179 em.deadspace = parse_f32(toks[1], ln)?;
1180 } else if eq_ci(kw, "blastRadius") && toks.len() >= 2 {
1181 em.blast_radius = parse_f32(toks[1], ln)?;
1182 } else if eq_ci(kw, "blastLength") && toks.len() >= 2 {
1183 em.blast_length = parse_f32(toks[1], ln)?;
1184 } else if eq_ci(kw, "numBranches") && toks.len() >= 2 {
1185 em.num_branches = parse_i32(toks[1], ln)?;
1186 } else if eq_ci(kw, "controlptsmoothing") && toks.len() >= 2 {
1187 em.control_pt_smoothing = parse_i32(toks[1], ln)?;
1188 } else if eq_ci(kw, "xgrid") && toks.len() >= 2 {
1189 em.x_grid = parse_i32(toks[1], ln)?;
1190 } else if eq_ci(kw, "ygrid") && toks.len() >= 2 {
1191 em.y_grid = parse_i32(toks[1], ln)?;
1192 } else if eq_ci(kw, "spawntype") && toks.len() >= 2 {
1193 em.spawn_type = parse_i32(toks[1], ln)?;
1194 } else if eq_ci(kw, "update") && toks.len() >= 2 {
1195 em.update = toks[1].to_string();
1196 } else if eq_ci(kw, "render") && toks.len() >= 2 {
1197 em.render = toks[1].to_string();
1198 } else if eq_ci(kw, "blend") && toks.len() >= 2 {
1199 em.blend = toks[1].to_string();
1200 } else if eq_ci(kw, "texture") && toks.len() >= 2 {
1201 em.texture = toks[1].to_string();
1202 } else if eq_ci(kw, "chunkName") && toks.len() >= 2 {
1203 em.chunk_name = toks[1].to_string();
1204 } else if eq_ci(kw, "twosidedtex") && toks.len() >= 2 {
1205 em.two_sided_tex = parse_i32(toks[1], ln)?;
1206 } else if eq_ci(kw, "loop") && toks.len() >= 2 {
1207 em.loop_emitter = parse_i32(toks[1], ln)?;
1208 } else if eq_ci(kw, "renderorder") && toks.len() >= 2 {
1209 em.render_order = parse_u16(toks[1], ln)?;
1210 } else if eq_ci(kw, "m_bFrameBlending") && toks.len() >= 2 {
1211 em.frame_blending = parse_i32(toks[1], ln)? != 0;
1212 } else if eq_ci(kw, "m_sDepthTextureName") && toks.len() >= 2 {
1213 em.depth_texture_name = toks[1].to_string();
1214 } else if eq_ci(kw, "p2p")
1215 || eq_ci(kw, "p2p_sel")
1216 || eq_ci(kw, "affectedByWind")
1217 || eq_ci(kw, "m_isTinted")
1218 || eq_ci(kw, "bounce")
1219 || eq_ci(kw, "random")
1220 || eq_ci(kw, "inherit")
1221 || eq_ci(kw, "inheritvel")
1222 || eq_ci(kw, "inherit_local")
1223 || eq_ci(kw, "splat")
1224 || eq_ci(kw, "inherit_part")
1225 || eq_ci(kw, "depth_texture")
1226 {
1227 } else {
1230 return Ok(false);
1231 }
1232 Ok(true)
1233}
1234
1235fn parse_reference_field(
1240 toks: &[&str],
1241 ln: usize,
1242 reference: &mut MdlReference,
1243) -> Result<bool, MdlAsciiError> {
1244 let kw = toks[0];
1245
1246 if eq_ci(kw, "refModel") && toks.len() >= 2 {
1247 reference.ref_model = toks[1].to_string();
1248 } else if eq_ci(kw, "reattachable") && toks.len() >= 2 {
1249 reference.reattachable = parse_i32(toks[1], ln)?;
1250 } else {
1251 return Ok(false);
1252 }
1253 Ok(true)
1254}
1255
1256fn parse_animmesh_field(
1261 toks: &[&str],
1262 ln: usize,
1263 p: &mut AsciiParser,
1264 am: &mut MdlAnimMesh,
1265) -> Result<bool, MdlAsciiError> {
1266 let kw = toks[0];
1267
1268 if eq_ci(kw, "sampleperiod") && toks.len() >= 2 {
1269 am.sample_period = parse_f32(toks[1], ln)?;
1270 } else if eq_ci(kw, "animverts") && toks.len() >= 2 {
1271 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
1272 am.anim_verts = parse_vec3_block(p, count)?;
1273 } else if eq_ci(kw, "animtverts") && toks.len() >= 2 {
1274 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
1275 am.anim_t_verts = parse_vec3_block(p, count)?;
1276 } else {
1277 return Ok(false);
1278 }
1279 Ok(true)
1280}
1281
1282fn parse_animation(
1287 p: &mut AsciiParser,
1288 anim_name: &str,
1289 _model_name: &str,
1290 _start_line: usize,
1291) -> Result<MdlAnimation, MdlAsciiError> {
1292 let mut length: f32 = 0.0;
1293 let mut transition_time: f32 = 0.0;
1294 let mut anim_root = String::new();
1295 let mut events: Vec<MdlAnimEvent> = Vec::new();
1296 let mut flat_nodes: Vec<FlatAnimNode> = Vec::new();
1297
1298 while let Some((ln, line)) = p.next_line() {
1299 let toks = tokens(&line);
1300 if toks.is_empty() {
1301 continue;
1302 }
1303 let kw = toks[0];
1304
1305 if eq_ci(kw, "length") && toks.len() >= 2 {
1306 length = parse_f32(toks[1], ln)?;
1307 } else if eq_ci(kw, "transtime") && toks.len() >= 2 {
1308 transition_time = parse_f32(toks[1], ln)?;
1309 } else if eq_ci(kw, "animroot") && toks.len() >= 2 {
1310 anim_root = toks[1].to_string();
1311 } else if eq_ci(kw, "event") && toks.len() >= 3 {
1312 let time = parse_f32(toks[1], ln)?;
1313 let name = toks[2].to_string();
1314 events.push(MdlAnimEvent { time, name });
1315 } else if eq_ci(kw, "node") && toks.len() >= 3 {
1316 let flat = parse_anim_node(p, toks[2], ln)?;
1317 flat_nodes.push(flat);
1318 } else if eq_ci(kw, "doneanim") {
1319 break;
1320 }
1321 }
1322
1323 let root_node = assemble_anim_node_tree(flat_nodes)?;
1324
1325 Ok(MdlAnimation {
1326 name: anim_name.to_string(),
1327 length,
1328 transition_time,
1329 anim_root,
1330 events,
1331 root_node,
1332 fn_ptr1: 0,
1333 fn_ptr2: 0,
1334 })
1335}
1336
1337fn parse_anim_node(
1338 p: &mut AsciiParser,
1339 name: &str,
1340 _node_line: usize,
1341) -> Result<FlatAnimNode, MdlAsciiError> {
1342 let mut parent_name = "NULL".into();
1343 let mut controllers = Vec::new();
1344
1345 while let Some((ln, line)) = p.next_line() {
1346 let toks = tokens(&line);
1347 if toks.is_empty() {
1348 continue;
1349 }
1350 let kw = toks[0];
1351
1352 if eq_ci(kw, "endnode") {
1353 break;
1354 } else if eq_ci(kw, "parent") && toks.len() >= 2 {
1355 parent_name = toks[1].to_string();
1356 } else {
1357 try_parse_controller_line(p, &toks, ln, NodeTypeContext::Base, &mut controllers)?;
1359 }
1360 }
1361
1362 Ok(FlatAnimNode {
1363 name: name.to_string(),
1364 parent_name,
1365 controllers,
1366 })
1367}
1368
1369fn assemble_node_tree(flat_nodes: Vec<FlatNode>) -> Result<MdlNode, MdlAsciiError> {
1374 if flat_nodes.is_empty() {
1375 return Err(MdlAsciiError::InvalidData("no geometry nodes found".into()));
1376 }
1377
1378 let root_idx = flat_nodes
1380 .iter()
1381 .position(|n| eq_ci(&n.parent_name, "NULL"))
1382 .ok_or_else(|| MdlAsciiError::InvalidData("no root node (parent NULL) found".into()))?;
1383
1384 let mut children_of_idx: HashMap<usize, Vec<usize>> = HashMap::new();
1388 let mut stack: Vec<(usize, String)> = Vec::new();
1389
1390 for (i, node) in flat_nodes.iter().enumerate() {
1391 if i == root_idx {
1392 stack.push((i, node.name.clone()));
1393 continue;
1394 }
1395
1396 let parent_pos = stack.iter().rposition(|(_, n)| eq_ci(n, &node.parent_name));
1399 if let Some(pos) = parent_pos {
1400 stack.truncate(pos + 1);
1402 let parent_idx = stack[pos].0;
1403 children_of_idx.entry(parent_idx).or_default().push(i);
1404 }
1405 stack.push((i, node.name.clone()));
1407 }
1408
1409 fn build_node(
1410 idx: usize,
1411 flat: &[FlatNode],
1412 children_of_idx: &HashMap<usize, Vec<usize>>,
1413 ) -> MdlNode {
1414 let f = &flat[idx];
1415 let children: Vec<MdlNode> = children_of_idx
1416 .get(&idx)
1417 .cloned()
1418 .unwrap_or_default()
1419 .iter()
1420 .map(|&ci| build_node(ci, flat, children_of_idx))
1421 .collect();
1422
1423 MdlNode {
1424 name: f.name.clone(),
1425 parent_index: None, children,
1427 position: f.position,
1428 rotation: f.rotation,
1429 node_data: f.node_data.clone(),
1430 controllers: f.controllers.clone(),
1431 orphan_controller_data: Vec::new(),
1432 header_padding_02: [0, 0],
1433 header_padding_06: [0, 0],
1434 }
1435 }
1436
1437 Ok(build_node(root_idx, &flat_nodes, &children_of_idx))
1438}
1439
1440fn assemble_anim_node_tree(flat_nodes: Vec<FlatAnimNode>) -> Result<MdlAnimNode, MdlAsciiError> {
1441 if flat_nodes.is_empty() {
1442 return Ok(MdlAnimNode {
1444 name: String::new(),
1445 node_number: 0,
1446 controllers: Vec::new(),
1447 orphan_controller_data: Vec::new(),
1448 children: Vec::new(),
1449 });
1450 }
1451
1452 let root_idx = flat_nodes
1453 .iter()
1454 .position(|n| eq_ci(&n.parent_name, "NULL"))
1455 .unwrap_or(0);
1456
1457 let mut children_of_idx: HashMap<usize, Vec<usize>> = HashMap::new();
1459 let mut stack: Vec<(usize, String)> = Vec::new();
1460 for (i, node) in flat_nodes.iter().enumerate() {
1461 if i == root_idx {
1462 stack.push((i, node.name.clone()));
1463 continue;
1464 }
1465 let parent_pos = stack.iter().rposition(|(_, n)| eq_ci(n, &node.parent_name));
1466 if let Some(pos) = parent_pos {
1467 stack.truncate(pos + 1);
1468 let parent_idx = stack[pos].0;
1469 children_of_idx.entry(parent_idx).or_default().push(i);
1470 }
1471 stack.push((i, node.name.clone()));
1472 }
1473
1474 fn build_anim(
1475 idx: usize,
1476 flat: &[FlatAnimNode],
1477 children_of_idx: &HashMap<usize, Vec<usize>>,
1478 ) -> MdlAnimNode {
1479 let f = &flat[idx];
1480 let children: Vec<MdlAnimNode> = children_of_idx
1481 .get(&idx)
1482 .cloned()
1483 .unwrap_or_default()
1484 .iter()
1485 .map(|&ci| build_anim(ci, flat, children_of_idx))
1486 .collect();
1487
1488 MdlAnimNode {
1489 name: f.name.clone(),
1490 node_number: 0, controllers: f.controllers.clone(),
1492 orphan_controller_data: Vec::new(),
1493 children,
1494 }
1495 }
1496
1497 Ok(build_anim(root_idx, &flat_nodes, &children_of_idx))
1498}
1499
1500fn build_name_index_map(node: &MdlNode) -> HashMap<String, u16> {
1505 let mut map = HashMap::new();
1506 let mut idx = 0u16;
1507 build_name_idx_recursive(node, &mut map, &mut idx);
1508 map
1509}
1510
1511fn build_name_idx_recursive(node: &MdlNode, map: &mut HashMap<String, u16>, idx: &mut u16) {
1512 map.insert(node.name.clone(), *idx);
1513 *idx += 1;
1514 for child in &node.children {
1515 build_name_idx_recursive(child, map, idx);
1516 }
1517}
1518
1519fn subtract_geo_positions_from_anim(
1520 node: &mut MdlAnimNode,
1521 geo_positions: &HashMap<&str, [f32; 3]>,
1522) {
1523 if let Some(geo_pos) = geo_positions.get(node.name.as_str()) {
1524 for ctrl in &mut node.controllers {
1525 if ctrl.controller_type == MdlControllerType::POSITION {
1526 for key in &mut ctrl.keys {
1527 if key.values.len() >= 3 {
1528 key.values[0] -= geo_pos[0];
1529 key.values[1] -= geo_pos[1];
1530 key.values[2] -= geo_pos[2];
1531 }
1532 }
1533 }
1534 }
1535 }
1536 for child in &mut node.children {
1537 subtract_geo_positions_from_anim(child, geo_positions);
1538 }
1539}
1540
1541fn assign_anim_node_numbers(node: &mut MdlAnimNode, name_to_index: &HashMap<String, u16>) {
1542 node.node_number = name_to_index.get(&node.name).copied().unwrap_or(0);
1543 for child in &mut node.children {
1544 assign_anim_node_numbers(child, name_to_index);
1545 }
1546}
1547
1548fn strip_suffix_ci<'a>(s: &'a str, suffix: &str) -> Option<&'a str> {
1553 let s_lower = s.to_ascii_lowercase();
1554 let suffix_lower = suffix.to_ascii_lowercase();
1555 if s_lower.ends_with(&suffix_lower) {
1556 Some(&s[..s.len() - suffix.len()])
1557 } else {
1558 None
1559 }
1560}
1561
1562#[cfg(test)]
1567mod tests {
1568 use super::*;
1569 use crate::mdl::ascii_writer::write_mdl_ascii_to_string;
1570
1571 #[test]
1572 fn minimal_model_parse() {
1573 let input = "\
1574newmodel test
1575setsupermodel test NULL
1576classification other
1577classification_unk1 0
1578ignorefog 0
1579setanimationscale 1.0
1580compress_quaternions 0
1581headlink 0
1582beginmodelgeom test
1583 bmin -1.0 -1.0 -1.0
1584 bmax 1.0 1.0 1.0
1585 radius 1.73
1586 node dummy test
1587 parent NULL
1588 endnode
1589endmodelgeom test
1590donemodel test
1591";
1592 let mdl = read_mdl_ascii_from_str(input).unwrap();
1593 assert_eq!(mdl.root_node.name, "test");
1594 assert_eq!(mdl.supermodel_name, "NULL");
1595 assert_eq!(mdl.classification, 0);
1596 assert_eq!(mdl.affected_by_fog, 1);
1597 assert_eq!(mdl.node_count, 1);
1598 }
1599
1600 #[test]
1601 fn mesh_node_parse() {
1602 let input = "\
1603newmodel m
1604setsupermodel m NULL
1605classification other
1606beginmodelgeom m
1607 node trimesh mesh1
1608 parent NULL
1609 diffuse 0.8 0.8 0.8
1610 ambient 0.2 0.2 0.2
1611 bitmap texture_a
1612 render 1
1613 verts 3
1614 0.0 0.0 0.0
1615 1.0 0.0 0.0
1616 0.0 1.0 0.0
1617 faces 1
1618 0 1 2 1 0 1 2 0
1619 tverts 3
1620 0.0 0.0
1621 1.0 0.0
1622 0.0 1.0
1623 endnode
1624endmodelgeom m
1625donemodel m
1626";
1627 let mdl = read_mdl_ascii_from_str(input).unwrap();
1628 let mesh = mdl.root_node.node_data.mesh().unwrap();
1629 assert_eq!(mesh.positions.len(), 3);
1630 assert_eq!(mesh.faces.len(), 1);
1631 assert_eq!(mesh.uv1.len(), 3);
1632 assert_eq!(mesh.texture_0, "texture_a");
1633 assert_eq!(mesh.faces[0].vertex_indices, [0, 1, 2]);
1634 assert_eq!(mesh.vertex_count, 3);
1635 }
1636
1637 #[test]
1638 fn controller_keyed_parse() {
1639 let input = "\
1640newmodel m
1641setsupermodel m NULL
1642classification other
1643beginmodelgeom m
1644 node dummy root
1645 parent NULL
1646 positionkey
1647 0.0 1.0 2.0 3.0
1648 0.5 4.0 5.0 6.0
1649 endlist
1650 endnode
1651endmodelgeom m
1652donemodel m
1653";
1654 let mdl = read_mdl_ascii_from_str(input).unwrap();
1655 assert_eq!(mdl.root_node.controllers.len(), 1);
1656 let ctrl = &mdl.root_node.controllers[0];
1657 assert_eq!(ctrl.controller_type, MdlControllerType::POSITION);
1658 assert_eq!(ctrl.keys.len(), 2);
1659 assert_eq!(ctrl.keys[0].time, 0.0);
1660 assert_eq!(ctrl.keys[0].values, vec![1.0, 2.0, 3.0]);
1661 assert_eq!(ctrl.keys[1].time, 0.5);
1662 assert_eq!(ctrl.keys[1].values, vec![4.0, 5.0, 6.0]);
1663 }
1664
1665 #[test]
1666 fn orientation_conversion() {
1667 let input = "\
1668newmodel m
1669setsupermodel m NULL
1670classification other
1671beginmodelgeom m
1672 node dummy root
1673 parent NULL
1674 orientation 0.0 0.0 1.0 1.5707963
1675 endnode
1676endmodelgeom m
1677donemodel m
1678";
1679 let mdl = read_mdl_ascii_from_str(input).unwrap();
1680 let q = mdl.root_node.rotation;
1682 let expected_half = std::f32::consts::FRAC_1_SQRT_2;
1684 assert!((q[0] - expected_half).abs() < 0.01, "w = {}", q[0]);
1685 assert!(q[1].abs() < 0.01, "x = {}", q[1]);
1686 assert!(q[2].abs() < 0.01, "y = {}", q[2]);
1687 assert!((q[3] - expected_half).abs() < 0.01, "z = {}", q[3]);
1688 }
1689
1690 #[test]
1691 fn animation_parse() {
1692 let input = "\
1693newmodel m
1694setsupermodel m NULL
1695classification other
1696beginmodelgeom m
1697 node dummy root
1698 parent NULL
1699 position 10.0 20.0 30.0
1700 endnode
1701endmodelgeom m
1702newanim walk m
1703 length 1.0
1704 transtime 0.25
1705 animroot root
1706 event 0.5 footstep
1707 node dummy root
1708 parent NULL
1709 positionkey
1710 0.0 10.0 20.0 30.0
1711 1.0 11.0 21.0 31.0
1712 endlist
1713 endnode
1714doneanim walk m
1715donemodel m
1716";
1717 let mdl = read_mdl_ascii_from_str(input).unwrap();
1718 assert_eq!(mdl.animations.len(), 1);
1719 let anim = &mdl.animations[0];
1720 assert_eq!(anim.name, "walk");
1721 assert_eq!(anim.length, 1.0);
1722 assert_eq!(anim.anim_root, "root");
1723 assert_eq!(anim.events.len(), 1);
1724 assert_eq!(anim.events[0].name, "footstep");
1725
1726 let ctrl = &anim.root_node.controllers[0];
1730 assert_eq!(ctrl.controller_type, MdlControllerType::POSITION);
1731 assert!((ctrl.keys[0].values[0]).abs() < 0.001);
1732 assert!((ctrl.keys[0].values[1]).abs() < 0.001);
1733 assert!((ctrl.keys[0].values[2]).abs() < 0.001);
1734 assert!((ctrl.keys[1].values[0] - 1.0).abs() < 0.001);
1735 assert!((ctrl.keys[1].values[1] - 1.0).abs() < 0.001);
1736 assert!((ctrl.keys[1].values[2] - 1.0).abs() < 0.001);
1737 }
1738
1739 #[test]
1740 fn aabb_tree_reconstruction() {
1741 let input = "\
1743newmodel m
1744setsupermodel m NULL
1745classification other
1746beginmodelgeom m
1747 node aabb walkmesh
1748 parent NULL
1749 verts 4
1750 0.0 0.0 0.0
1751 1.0 0.0 0.0
1752 1.0 1.0 0.0
1753 0.0 1.0 0.0
1754 faces 2
1755 0 1 2 1 0 1 2 0
1756 0 2 3 1 0 2 3 0
1757 aabb
1758 0.0 0.0 0.0 1.0 0.5 0.0 0
1759 0.0 0.5 0.0 1.0 1.0 0.0 1
1760 endnode
1761endmodelgeom m
1762donemodel m
1763";
1764 let mdl = read_mdl_ascii_from_str(input).unwrap();
1765 if let MdlNodeData::Aabb(ref aabb) = mdl.root_node.node_data {
1766 assert!(aabb.aabb_tree.is_some());
1767 let tree = aabb.aabb_tree.as_ref().unwrap();
1768 assert_eq!(tree.face_index, -1);
1770 assert!(tree.left.is_some());
1771 assert!(tree.right.is_some());
1772 } else {
1773 panic!("expected AABB node");
1774 }
1775 }
1776
1777 fn ascii_self_roundtrip(input: &str) {
1782 let mdl = read_mdl_ascii_from_str(input).unwrap();
1783 let ascii1 = write_mdl_ascii_to_string(&mdl).unwrap();
1784 let mdl2 = read_mdl_ascii_from_str(&ascii1).unwrap();
1785 let ascii2 = write_mdl_ascii_to_string(&mdl2).unwrap();
1786 if ascii1 != ascii2 {
1787 for (i, (a, b)) in ascii1.lines().zip(ascii2.lines()).enumerate() {
1789 if a != b {
1790 panic!(
1791 "ASCII self round-trip mismatch at line {}:\n pass 1: {}\n pass 2: {}",
1792 i + 1,
1793 a,
1794 b
1795 );
1796 }
1797 }
1798 let c1 = ascii1.lines().count();
1799 let c2 = ascii2.lines().count();
1800 if c1 != c2 {
1801 panic!("ASCII self round-trip: line count differs ({c1} vs {c2})");
1802 }
1803 }
1804 }
1805
1806 #[test]
1807 fn self_roundtrip_minimal() {
1808 let input = "\
1809newmodel test
1810setsupermodel test NULL
1811classification other
1812classification_unk1 0
1813ignorefog 0
1814setanimationscale 1.0
1815compress_quaternions 0
1816headlink 0
1817beginmodelgeom test
1818 bmin -1.0 -1.0 -1.0
1819 bmax 1.0 1.0 1.0
1820 radius 1.73
1821 node dummy test
1822 parent NULL
1823 endnode
1824endmodelgeom test
1825donemodel test
1826";
1827 ascii_self_roundtrip(input);
1828 }
1829
1830 #[test]
1831 fn self_roundtrip_mesh_with_controllers() {
1832 let input = "\
1833newmodel m
1834setsupermodel m NULL
1835classification other
1836classification_unk1 0
1837ignorefog 0
1838setanimationscale 1.0
1839compress_quaternions 0
1840headlink 0
1841beginmodelgeom m
1842 bmin -1.0 -1.0 -1.0
1843 bmax 1.0 1.0 1.0
1844 radius 1.73
1845 node trimesh mesh1
1846 parent NULL
1847 diffuse 0.8 0.8 0.8
1848 ambient 0.2 0.2 0.2
1849 bitmap texture_a
1850 render 1
1851 shadow 0
1852 verts 3
1853 0.0 0.0 0.0
1854 1.0 0.0 0.0
1855 0.0 1.0 0.0
1856 faces 1
1857 0 1 2 1 0 1 2 0
1858 tverts 3
1859 0.0 0.0
1860 1.0 0.0
1861 0.0 1.0
1862 endnode
1863endmodelgeom m
1864donemodel m
1865";
1866 ascii_self_roundtrip(input);
1867 }
1868
1869 #[test]
1870 fn self_roundtrip_animation() {
1871 let input = "\
1872newmodel m
1873setsupermodel m NULL
1874classification character
1875classification_unk1 0
1876ignorefog 0
1877setanimationscale 1.0
1878compress_quaternions 0
1879headlink 0
1880beginmodelgeom m
1881 bmin -1.0 -1.0 -1.0
1882 bmax 1.0 1.0 1.0
1883 radius 1.73
1884 node dummy root
1885 parent NULL
1886 position 10.0 20.0 30.0
1887 endnode
1888endmodelgeom m
1889newanim walk m
1890 length 1.0
1891 transtime 0.25
1892 animroot root
1893 event 0.5 footstep
1894 node dummy root
1895 parent NULL
1896 positionkey
1897 0.0 10.0 20.0 30.0
1898 1.0 11.0 21.0 31.0
1899 endlist
1900 endnode
1901doneanim walk m
1902donemodel m
1903";
1904 ascii_self_roundtrip(input);
1905 }
1906
1907 fn assert_nodes_equivalent(a: &super::super::MdlNode, b: &super::super::MdlNode, path: &str) {
1914 assert_eq!(a.name, b.name, "{path}: name mismatch");
1915 assert_eq!(
1916 std::mem::discriminant(&a.node_data),
1917 std::mem::discriminant(&b.node_data),
1918 "{path}: node type mismatch"
1919 );
1920 for i in 0..3 {
1922 assert!(
1923 (a.position[i] - b.position[i]).abs() < 1e-4,
1924 "{path}: position[{i}] {:.6} vs {:.6}",
1925 a.position[i],
1926 b.position[i]
1927 );
1928 }
1929 for i in 0..4 {
1932 assert!(
1933 (a.rotation[i] - b.rotation[i]).abs() < 2e-3,
1934 "{path}: rotation[{i}] {:.6} vs {:.6}",
1935 a.rotation[i],
1936 b.rotation[i]
1937 );
1938 }
1939 for cb in &b.controllers {
1943 if let Some(ca) = a
1944 .controllers
1945 .iter()
1946 .find(|c| c.controller_type == cb.controller_type)
1947 {
1948 assert_eq!(
1949 ca.keys.len(),
1950 cb.keys.len(),
1951 "{path}: controller {:?} key count ({} vs {})",
1952 cb.controller_type,
1953 ca.keys.len(),
1954 cb.keys.len()
1955 );
1956 } else {
1957 panic!(
1958 "{path}: roundtripped has controller {:?} not in original",
1959 cb.controller_type
1960 );
1961 }
1962 }
1963 if let (Some(ma), Some(mb)) = (a.node_data.mesh(), b.node_data.mesh()) {
1965 assert_eq!(
1966 ma.positions.len(),
1967 mb.positions.len(),
1968 "{path}: vertex count"
1969 );
1970 assert_eq!(ma.faces.len(), mb.faces.len(), "{path}: face count");
1971 assert_eq!(ma.uv1.len(), mb.uv1.len(), "{path}: uv1 count");
1972 }
1973 assert_eq!(
1975 a.children.len(),
1976 b.children.len(),
1977 "{path}: child count ({} vs {})",
1978 a.children.len(),
1979 b.children.len()
1980 );
1981 for (ca, cb) in a.children.iter().zip(b.children.iter()) {
1982 assert_nodes_equivalent(ca, cb, &format!("{path}/{}", ca.name));
1983 }
1984 }
1985
1986 fn assert_anims_equivalent(a: &[super::super::MdlAnimation], b: &[super::super::MdlAnimation]) {
1987 assert_eq!(a.len(), b.len(), "animation count");
1988 for (i, (aa, ab)) in a.iter().zip(b.iter()).enumerate() {
1989 assert_eq!(aa.name, ab.name, "anim[{i}] name");
1990 assert!((aa.length - ab.length).abs() < 1e-4, "anim[{i}] length");
1991 assert_eq!(aa.anim_root, ab.anim_root, "anim[{i}] anim_root");
1992 assert_eq!(aa.events.len(), ab.events.len(), "anim[{i}] event count");
1993 }
1994 }
1995
1996 fn k1_override_dir() -> Option<String> {
1999 std::env::var("KOTOR_GAME_DIR")
2000 .ok()
2001 .map(|d| format!("{d}/Override"))
2002 }
2003
2004 fn binary_ascii_roundtrip(mdl_path: &str, mdx_path: Option<&str>) {
2006 let mdl_data = match std::fs::read(mdl_path) {
2007 Ok(d) => d,
2008 Err(_) => return,
2009 };
2010 let mdx_data = mdx_path.and_then(|p| std::fs::read(p).ok());
2011 let original =
2012 super::super::reader::read_mdl_from_bytes(&mdl_data, mdx_data.as_deref()).unwrap();
2013
2014 let ascii = write_mdl_ascii_to_string(&original).unwrap();
2015 let roundtripped = read_mdl_ascii_from_str(&ascii).unwrap();
2016
2017 assert_eq!(
2019 original.root_node.name, roundtripped.root_node.name,
2020 "model_name"
2021 );
2022 assert_eq!(
2023 original.supermodel_name, roundtripped.supermodel_name,
2024 "supermodel_name"
2025 );
2026 assert_eq!(
2027 original.classification, roundtripped.classification,
2028 "classification"
2029 );
2030 assert_eq!(
2031 original.affected_by_fog, roundtripped.affected_by_fog,
2032 "affected_by_fog"
2033 );
2034 assert!(
2035 (original.animation_scale - roundtripped.animation_scale).abs() < 1e-4,
2036 "animation_scale"
2037 );
2038 assert!(roundtripped.node_count > 0, "node_count should be > 0");
2041
2042 assert_nodes_equivalent(
2044 &original.root_node,
2045 &roundtripped.root_node,
2046 &original.root_node.name,
2047 );
2048
2049 assert_anims_equivalent(&original.animations, &roundtripped.animations);
2051 }
2052
2053 #[test]
2054 fn roundtrip_vanilla_item() {
2055 let base = match k1_override_dir() {
2057 Some(d) => d,
2058 None => return,
2059 };
2060 binary_ascii_roundtrip(
2061 &format!("{base}/i_adrnaline_001.mdl"),
2062 Some(&format!("{base}/i_adrnaline_001.mdx")),
2063 );
2064 }
2065
2066 #[test]
2067 fn roundtrip_vanilla_placeable() {
2068 let base = match k1_override_dir() {
2070 Some(d) => d,
2071 None => return,
2072 };
2073 binary_ascii_roundtrip(&format!("{base}/3dgui.mdl"), None);
2074 }
2075
2076 #[test]
2077 fn roundtrip_vanilla_character() {
2078 let base = match k1_override_dir() {
2080 Some(d) => d,
2081 None => return,
2082 };
2083 binary_ascii_roundtrip(
2084 &format!("{base}/p_bastilabb.mdl"),
2085 Some(&format!("{base}/p_bastilabb.mdx")),
2086 );
2087 }
2088
2089 #[test]
2090 fn roundtrip_vanilla_supermodel() {
2091 let base = match k1_override_dir() {
2093 Some(d) => d,
2094 None => return,
2095 };
2096 binary_ascii_roundtrip(
2097 &format!("{base}/s_female03.mdl"),
2098 Some(&format!("{base}/s_female03.mdx")),
2099 );
2100 }
2101
2102 #[test]
2103 fn roundtrip_vanilla_effect() {
2104 let base = match k1_override_dir() {
2106 Some(d) => d,
2107 None => return,
2108 };
2109 binary_ascii_roundtrip(&format!("{base}/fx_carbref.mdl"), None);
2110 }
2111}