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