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 match data {
614 MdlNodeData::Skin(skin) => {
615 if parse_skin_field(toks, ln, p, skin)? {
616 return Ok(());
617 }
618 }
619 MdlNodeData::Dangly(dangly) => {
620 if parse_dangly_field(toks, ln, p, dangly)? {
621 return Ok(());
622 }
623 }
624 MdlNodeData::Aabb(aabb) => {
625 if parse_aabb_field(toks, ln, p, aabb)? {
626 return Ok(());
627 }
628 }
629 MdlNodeData::Light(light) => {
630 if parse_light_field(toks, ln, p, light)? {
631 return Ok(());
632 }
633 }
634 MdlNodeData::Emitter(emitter) => {
635 if parse_emitter_field(toks, ln, p, emitter)? {
636 return Ok(());
637 }
638 }
639 MdlNodeData::Reference(reference) => {
640 if parse_reference_field(toks, ln, reference)? {
641 return Ok(());
642 }
643 }
644 MdlNodeData::AnimMesh(animmesh) => {
645 if parse_animmesh_field(toks, ln, p, animmesh)? {
646 return Ok(());
647 }
648 }
649 _ => {}
650 }
651
652 Ok(())
654}
655
656fn parse_mesh_field(
661 toks: &[&str],
662 ln: usize,
663 p: &mut AsciiParser,
664 mesh: &mut MdlMesh,
665) -> Result<bool, MdlAsciiError> {
666 let kw = toks[0];
667
668 if eq_ci(kw, "diffuse") && toks.len() >= 4 {
669 mesh.diffuse_color = [
670 parse_f32(toks[1], ln)?,
671 parse_f32(toks[2], ln)?,
672 parse_f32(toks[3], ln)?,
673 ];
674 } else if eq_ci(kw, "ambient") && toks.len() >= 4 {
675 mesh.ambient_color = [
676 parse_f32(toks[1], ln)?,
677 parse_f32(toks[2], ln)?,
678 parse_f32(toks[3], ln)?,
679 ];
680 } else if eq_ci(kw, "transparencyhint") && toks.len() >= 2 {
681 mesh.transparency_hint = parse_i32(toks[1], ln)?;
682 } else if eq_ci(kw, "animateuv") && toks.len() >= 2 {
683 mesh.animate_uv = parse_i32(toks[1], ln)?;
684 } else if eq_ci(kw, "uvdirectionx") && toks.len() >= 2 {
685 mesh.uv_direction_x = parse_f32(toks[1], ln)?;
686 } else if eq_ci(kw, "uvdirectiony") && toks.len() >= 2 {
687 mesh.uv_direction_y = parse_f32(toks[1], ln)?;
688 } else if eq_ci(kw, "uvjitter") && toks.len() >= 2 {
689 mesh.uv_jitter = parse_f32(toks[1], ln)?;
690 } else if eq_ci(kw, "uvjitterspeed") && toks.len() >= 2 {
691 mesh.uv_jitter_speed = parse_f32(toks[1], ln)?;
692 } else if eq_ci(kw, "lightmapped") && toks.len() >= 2 {
693 mesh.light_mapped = parse_i32(toks[1], ln)? != 0;
694 } else if eq_ci(kw, "rotatetexture") && toks.len() >= 2 {
695 mesh.rotate_texture = parse_i32(toks[1], ln)? != 0;
696 } else if eq_ci(kw, "m_bIsBackgroundGeometry") && toks.len() >= 2 {
697 mesh.is_background_geometry = parse_i32(toks[1], ln)? != 0;
698 } else if eq_ci(kw, "shadow") && toks.len() >= 2 {
699 mesh.shadow = parse_i32(toks[1], ln)? != 0;
700 } else if eq_ci(kw, "beaming") && toks.len() >= 2 {
701 mesh.beaming = parse_i32(toks[1], ln)? != 0;
702 } else if eq_ci(kw, "render") && toks.len() >= 2 {
703 mesh.render = parse_i32(toks[1], ln)? != 0;
704 } else if (eq_ci(kw, "bitmap") || eq_ci(kw, "texture0")) && toks.len() >= 2 {
705 let val = toks[1];
706 mesh.texture_0 = if eq_ci(val, "NULL") {
707 String::new()
708 } else {
709 val.to_string()
710 };
711 } else if (eq_ci(kw, "bitmap2") || eq_ci(kw, "texture1")) && toks.len() >= 2 {
712 let val = toks[1];
713 mesh.texture_1 = if eq_ci(val, "NULL") {
714 String::new()
715 } else {
716 val.to_string()
717 };
718 } else if eq_ci(kw, "inv_count") && toks.len() >= 2 {
719 mesh.inverted_counter = parse_u32(toks[1], ln)?;
720 } else if eq_ci(kw, "verts") && toks.len() >= 2 {
721 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
722 mesh.positions = parse_vec3_block(p, count)?;
723 } else if eq_ci(kw, "faces") && toks.len() >= 2 {
724 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
725 mesh.faces = parse_face_block(p, count)?;
726 } else if (eq_ci(kw, "tverts") || eq_ci(kw, "tverts0")) && toks.len() >= 2 {
727 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
728 mesh.uv1 = parse_uv_block(p, count)?;
729 } else if eq_ci(kw, "tverts1") && toks.len() >= 2 {
730 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
731 mesh.uv2 = parse_uv_block(p, count)?;
732 } else if eq_ci(kw, "tverts2") && toks.len() >= 2 {
733 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
734 mesh.uv3 = parse_uv_block(p, count)?;
735 } else if eq_ci(kw, "tverts3") && toks.len() >= 2 {
736 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
737 mesh.uv4 = parse_uv_block(p, count)?;
738 } else if eq_ci(kw, "colors") && toks.len() >= 2 {
739 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
740 mesh.vertex_colors = parse_color_block(p, count)?;
741 } else if eq_ci(kw, "tangentspace")
742 || eq_ci(kw, "dirt_enabled")
743 || eq_ci(kw, "dirt_texture")
744 || eq_ci(kw, "dirt_worldspace")
745 || eq_ci(kw, "hologram_donotdraw")
746 {
747 } else {
749 return Ok(false);
750 }
751
752 Ok(true)
753}
754
755fn parse_vec3_block(p: &mut AsciiParser, count: usize) -> Result<Vec<[f32; 3]>, MdlAsciiError> {
756 let mut result = Vec::with_capacity(count);
757 for _ in 0..count {
758 let (ln, line) = p
759 .next_line()
760 .ok_or_else(|| MdlAsciiError::InvalidData("unexpected EOF in vec3 block".into()))?;
761 let toks = tokens(&line);
762 if toks.len() < 3 {
763 return Err(p.parse_err(ln, "expected 3 floats"));
764 }
765 result.push([
766 parse_f32(toks[0], ln)?,
767 parse_f32(toks[1], ln)?,
768 parse_f32(toks[2], ln)?,
769 ]);
770 }
771 Ok(result)
772}
773
774fn parse_face_block(p: &mut AsciiParser, count: usize) -> Result<Vec<MdlFace>, MdlAsciiError> {
775 let mut faces = Vec::with_capacity(count);
776 for _ in 0..count {
777 let (ln, line) = p
778 .next_line()
779 .ok_or_else(|| MdlAsciiError::InvalidData("unexpected EOF in face block".into()))?;
780 let toks = tokens(&line);
781 if toks.len() < 8 {
782 return Err(p.parse_err(ln, "expected 8 values in face line"));
783 }
784 let v0 = parse_u16(toks[0], ln)?;
785 let v1 = parse_u16(toks[1], ln)?;
786 let v2 = parse_u16(toks[2], ln)?;
787 let surface_id = parse_u32(toks[7], ln)?;
790
791 faces.push(MdlFace {
792 plane_normal: [0.0; 3], plane_distance: 0.0, surface_id,
795 adjacent: [0xFFFF; 3], vertex_indices: [v0, v1, v2],
797 });
798 }
799 Ok(faces)
800}
801
802fn parse_uv_block(p: &mut AsciiParser, count: usize) -> Result<Vec<[f32; 2]>, MdlAsciiError> {
803 let mut uvs = Vec::with_capacity(count);
804 for _ in 0..count {
805 let (ln, line) = p
806 .next_line()
807 .ok_or_else(|| MdlAsciiError::InvalidData("unexpected EOF in UV block".into()))?;
808 let toks = tokens(&line);
809 if toks.len() < 2 {
810 return Err(p.parse_err(ln, "expected at least 2 floats in UV line"));
811 }
812 uvs.push([parse_f32(toks[0], ln)?, parse_f32(toks[1], ln)?]);
814 }
815 Ok(uvs)
816}
817
818fn parse_color_block(p: &mut AsciiParser, count: usize) -> Result<Vec<[u8; 4]>, MdlAsciiError> {
819 let mut colors = Vec::with_capacity(count);
820 for _ in 0..count {
821 let (ln, line) = p
822 .next_line()
823 .ok_or_else(|| MdlAsciiError::InvalidData("unexpected EOF in color block".into()))?;
824 let toks = tokens(&line);
825 if toks.len() < 3 {
826 return Err(p.parse_err(ln, "expected 3 floats in color line"));
827 }
828 #[allow(
830 clippy::cast_possible_truncation,
831 clippy::cast_sign_loss,
832 clippy::as_conversions
833 )]
834 let r = (parse_f32(toks[0], ln)? * 255.0).round().clamp(0.0, 255.0) as u8;
835 #[allow(
836 clippy::cast_possible_truncation,
837 clippy::cast_sign_loss,
838 clippy::as_conversions
839 )]
840 let g = (parse_f32(toks[1], ln)? * 255.0).round().clamp(0.0, 255.0) as u8;
841 #[allow(
842 clippy::cast_possible_truncation,
843 clippy::cast_sign_loss,
844 clippy::as_conversions
845 )]
846 let b = (parse_f32(toks[2], ln)? * 255.0).round().clamp(0.0, 255.0) as u8;
847 colors.push([r, g, b, 255]);
848 }
849 Ok(colors)
850}
851
852fn parse_skin_field(
857 toks: &[&str],
858 ln: usize,
859 p: &mut AsciiParser,
860 skin: &mut MdlSkin,
861) -> Result<bool, MdlAsciiError> {
862 let kw = toks[0];
863 if (eq_ci(kw, "weights") || eq_ci(kw, "skinweights")) && toks.len() >= 2 {
864 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
865 parse_skin_weights(p, count, skin)?;
866 Ok(true)
867 } else {
868 Ok(false)
869 }
870}
871
872struct VertexWeights {
874 pairs: Vec<(String, f32)>,
876}
877
878fn parse_skin_weights(
879 p: &mut AsciiParser,
880 count: usize,
881 skin: &mut MdlSkin,
882) -> Result<(), MdlAsciiError> {
883 let mut vertex_weights: Vec<VertexWeights> = Vec::with_capacity(count);
884
885 for _ in 0..count {
886 let (ln, line) = p
887 .next_line()
888 .ok_or_else(|| MdlAsciiError::InvalidData("unexpected EOF in weights block".into()))?;
889 let toks = tokens(&line);
890 let mut pairs = Vec::new();
891 let mut i = 0;
892 while i + 1 < toks.len() && pairs.len() < 4 {
893 let bone_name = toks[i].to_string();
894 let weight = parse_f32(toks[i + 1], ln)?;
895 if weight > 0.0 {
896 pairs.push((bone_name, weight));
897 }
898 i += 2;
899 }
900 vertex_weights.push(VertexWeights { pairs });
901 }
902
903 let mut bone_name_to_idx: HashMap<String, usize> = HashMap::new();
905 for vw in &vertex_weights {
906 for (name, _) in &vw.pairs {
907 let next_idx = bone_name_to_idx.len();
908 bone_name_to_idx.entry(name.clone()).or_insert(next_idx);
909 }
910 }
911
912 skin.mdx_bone_weights_offset = 0;
916 skin.mdx_bone_indices_offset = 0;
917
918 let mut bone_weights_vec = Vec::with_capacity(count);
919 let mut bone_indices_vec = Vec::with_capacity(count);
920 for vw in &vertex_weights {
921 let mut weights = [0.0f32; 4];
922 let mut indices = [0.0f32; 4];
923 for (j, (name, weight)) in vw.pairs.iter().enumerate().take(4) {
924 let idx = *bone_name_to_idx.get(name).unwrap_or(&0);
925 weights[j] = *weight;
926 #[allow(clippy::cast_precision_loss, clippy::as_conversions)]
929 let idx_f32 = idx as f32;
930 indices[j] = idx_f32;
931 }
932 bone_weights_vec.push(weights);
933 bone_indices_vec.push(indices);
934 }
935 skin.bone_weights = bone_weights_vec;
936 skin.bone_indices = bone_indices_vec;
937
938 skin.bonemap = Vec::new();
941
942 Ok(())
943}
944
945fn parse_dangly_field(
950 toks: &[&str],
951 ln: usize,
952 p: &mut AsciiParser,
953 dangly: &mut MdlDangly,
954) -> Result<bool, MdlAsciiError> {
955 let kw = toks[0];
956
957 if eq_ci(kw, "displacement") && toks.len() >= 2 {
958 dangly.displacement = parse_f32(toks[1], ln)?;
959 } else if eq_ci(kw, "tightness") && toks.len() >= 2 {
960 dangly.tightness = parse_f32(toks[1], ln)?;
961 } else if eq_ci(kw, "period") && toks.len() >= 2 {
962 dangly.period = parse_f32(toks[1], ln)?;
963 } else if eq_ci(kw, "constraints") && toks.len() >= 2 {
964 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
965 dangly.constraints = parse_float_block(p, count)?;
966 } else {
967 return Ok(false);
968 }
969 Ok(true)
970}
971
972fn parse_float_block(p: &mut AsciiParser, count: usize) -> Result<Vec<f32>, MdlAsciiError> {
973 let mut result = Vec::with_capacity(count);
974 for _ in 0..count {
975 let (ln, line) = p
976 .next_line()
977 .ok_or_else(|| MdlAsciiError::InvalidData("unexpected EOF in float block".into()))?;
978 let toks = tokens(&line);
979 if toks.is_empty() {
980 return Err(p.parse_err(ln, "expected float value"));
981 }
982 result.push(parse_f32(toks[0], ln)?);
983 }
984 Ok(result)
985}
986
987fn parse_aabb_field(
992 toks: &[&str],
993 _ln: usize,
994 p: &mut AsciiParser,
995 aabb: &mut MdlAabb,
996) -> Result<bool, MdlAsciiError> {
997 if !eq_ci(toks[0], "aabb") {
998 return Ok(false);
999 }
1000
1001 let mut leaves: Vec<AabbLeaf> = Vec::new();
1003 while let Some((ln, line)) = p.peek_line() {
1004 let toks = tokens(line);
1005 if toks.len() < 7 {
1006 break;
1007 }
1008 let vals: Result<Vec<f32>, _> = toks[..7].iter().map(|t| parse_f32(t, ln)).collect();
1010 match vals {
1011 Ok(v) => {
1012 p.next_line(); leaves.push(AabbLeaf {
1014 box_min: [v[0], v[1], v[2]],
1015 box_max: [v[3], v[4], v[5]],
1016 #[allow(clippy::cast_possible_truncation, clippy::as_conversions)]
1019 face_index: v[6] as i32,
1020 });
1021 }
1022 Err(_) => break,
1023 }
1024 }
1025
1026 if !leaves.is_empty() {
1027 aabb.aabb_tree = Some(Box::new(build_aabb_tree(&leaves)));
1028 }
1029
1030 Ok(true)
1031}
1032
1033struct AabbLeaf {
1034 box_min: [f32; 3],
1035 box_max: [f32; 3],
1036 face_index: i32,
1037}
1038
1039fn build_aabb_tree(leaves: &[AabbLeaf]) -> AabbNode {
1041 if leaves.len() == 1 {
1042 return AabbNode {
1043 box_min: leaves[0].box_min,
1044 box_max: leaves[0].box_max,
1045 face_index: leaves[0].face_index,
1046 split_direction_flags: 0,
1047 left: None,
1048 right: None,
1049 };
1050 }
1051
1052 let mut combined_min = [f32::MAX; 3];
1054 let mut combined_max = [f32::MIN; 3];
1055 for leaf in leaves {
1056 for i in 0..3 {
1057 combined_min[i] = combined_min[i].min(leaf.box_min[i]);
1058 combined_max[i] = combined_max[i].max(leaf.box_max[i]);
1059 }
1060 }
1061
1062 let extents = [
1064 combined_max[0] - combined_min[0],
1065 combined_max[1] - combined_min[1],
1066 combined_max[2] - combined_min[2],
1067 ];
1068 let axis = if extents[0] >= extents[1] && extents[0] >= extents[2] {
1069 0
1070 } else if extents[1] >= extents[2] {
1071 1
1072 } else {
1073 2
1074 };
1075
1076 let mut sorted: Vec<usize> = (0..leaves.len()).collect();
1078 sorted.sort_by(|&a, &b| {
1079 let ca = (leaves[a].box_min[axis] + leaves[a].box_max[axis]) * 0.5;
1080 let cb = (leaves[b].box_min[axis] + leaves[b].box_max[axis]) * 0.5;
1081 ca.partial_cmp(&cb).unwrap_or(std::cmp::Ordering::Equal)
1082 });
1083
1084 let mid = sorted.len() / 2;
1086 let left_leaves: Vec<AabbLeaf> = sorted[..mid]
1087 .iter()
1088 .map(|&i| AabbLeaf {
1089 box_min: leaves[i].box_min,
1090 box_max: leaves[i].box_max,
1091 face_index: leaves[i].face_index,
1092 })
1093 .collect();
1094 let right_leaves: Vec<AabbLeaf> = sorted[mid..]
1095 .iter()
1096 .map(|&i| AabbLeaf {
1097 box_min: leaves[i].box_min,
1098 box_max: leaves[i].box_max,
1099 face_index: leaves[i].face_index,
1100 })
1101 .collect();
1102
1103 let left = build_aabb_tree(&left_leaves);
1104 let right = build_aabb_tree(&right_leaves);
1105
1106 let split_flags = 1u32 << axis;
1108
1109 AabbNode {
1110 box_min: combined_min,
1111 box_max: combined_max,
1112 face_index: -1,
1113 split_direction_flags: split_flags,
1114 left: Some(Box::new(left)),
1115 right: Some(Box::new(right)),
1116 }
1117}
1118
1119fn parse_light_field(
1124 toks: &[&str],
1125 ln: usize,
1126 p: &mut AsciiParser,
1127 light: &mut MdlLight,
1128) -> Result<bool, MdlAsciiError> {
1129 let kw = toks[0];
1130
1131 if eq_ci(kw, "lightpriority") && toks.len() >= 2 {
1132 light.priority = parse_i32(toks[1], ln)?;
1133 } else if eq_ci(kw, "ambientonly") && toks.len() >= 2 {
1134 light.ambientonly = parse_i32(toks[1], ln)?;
1135 } else if eq_ci(kw, "ndynamictype") && toks.len() >= 2 {
1136 light.num_dynamic_types = parse_i32(toks[1], ln)?;
1137 } else if eq_ci(kw, "affectdynamic") && toks.len() >= 2 {
1138 light.affectdynamic = parse_i32(toks[1], ln)?;
1139 } else if eq_ci(kw, "shadow") && toks.len() >= 2 {
1140 light.shadow = parse_i32(toks[1], ln)?;
1141 } else if eq_ci(kw, "generateflare") && toks.len() >= 2 {
1142 light.generateflare = parse_i32(toks[1], ln)?;
1143 } else if eq_ci(kw, "fadingLight") && toks.len() >= 2 {
1144 light.fading_light = parse_i32(toks[1], ln)?;
1145 } else if eq_ci(kw, "flareradius") && toks.len() >= 2 {
1146 light.flare_radius = parse_f32(toks[1], ln)?;
1147 } else if eq_ci(kw, "lensflares") {
1148 } else if eq_ci(kw, "texturenames") && toks.len() >= 2 {
1150 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
1151 light.flare_texture_names = parse_string_block(p, count)?;
1152 } else if eq_ci(kw, "flarepositions") && toks.len() >= 2 {
1153 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
1154 light.flare_positions = parse_float_block(p, count)?;
1155 } else if eq_ci(kw, "flaresizes") && toks.len() >= 2 {
1156 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
1157 light.flare_sizes = parse_float_block(p, count)?;
1158 } else if eq_ci(kw, "flarecolorshifts") && toks.len() >= 2 {
1159 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
1160 light.flare_color_shifts = parse_vec3_block(p, count)?;
1161 } else {
1162 return Ok(false);
1163 }
1164 Ok(true)
1165}
1166
1167fn parse_string_block(p: &mut AsciiParser, count: usize) -> Result<Vec<String>, MdlAsciiError> {
1168 let mut result = Vec::with_capacity(count);
1169 for _ in 0..count {
1170 let (_ln, line) = p
1171 .next_line()
1172 .ok_or_else(|| MdlAsciiError::InvalidData("unexpected EOF in string block".into()))?;
1173 result.push(line.trim().to_string());
1174 }
1175 Ok(result)
1176}
1177
1178fn parse_emitter_field(
1183 toks: &[&str],
1184 ln: usize,
1185 _p: &mut AsciiParser,
1186 em: &mut MdlEmitter,
1187) -> Result<bool, MdlAsciiError> {
1188 let kw = toks[0];
1189
1190 if eq_ci(kw, "deadspace") && toks.len() >= 2 {
1191 em.deadspace = parse_f32(toks[1], ln)?;
1192 } else if eq_ci(kw, "blastRadius") && toks.len() >= 2 {
1193 em.blast_radius = parse_f32(toks[1], ln)?;
1194 } else if eq_ci(kw, "blastLength") && toks.len() >= 2 {
1195 em.blast_length = parse_f32(toks[1], ln)?;
1196 } else if eq_ci(kw, "numBranches") && toks.len() >= 2 {
1197 em.num_branches = parse_i32(toks[1], ln)?;
1198 } else if eq_ci(kw, "controlptsmoothing") && toks.len() >= 2 {
1199 em.control_pt_smoothing = parse_i32(toks[1], ln)?;
1200 } else if eq_ci(kw, "xgrid") && toks.len() >= 2 {
1201 em.x_grid = parse_i32(toks[1], ln)?;
1202 } else if eq_ci(kw, "ygrid") && toks.len() >= 2 {
1203 em.y_grid = parse_i32(toks[1], ln)?;
1204 } else if eq_ci(kw, "spawntype") && toks.len() >= 2 {
1205 em.spawn_type = parse_i32(toks[1], ln)?;
1206 } else if eq_ci(kw, "update") && toks.len() >= 2 {
1207 em.update = toks[1].to_string();
1208 } else if eq_ci(kw, "render") && toks.len() >= 2 {
1209 em.render = toks[1].to_string();
1210 } else if eq_ci(kw, "blend") && toks.len() >= 2 {
1211 em.blend = toks[1].to_string();
1212 } else if eq_ci(kw, "texture") && toks.len() >= 2 {
1213 em.texture = toks[1].to_string();
1214 } else if eq_ci(kw, "chunkName") && toks.len() >= 2 {
1215 em.chunk_name = toks[1].to_string();
1216 } else if eq_ci(kw, "twosidedtex") && toks.len() >= 2 {
1217 em.two_sided_tex = parse_i32(toks[1], ln)?;
1218 } else if eq_ci(kw, "loop") && toks.len() >= 2 {
1219 em.loop_emitter = parse_i32(toks[1], ln)?;
1220 } else if eq_ci(kw, "renderorder") && toks.len() >= 2 {
1221 em.render_order = parse_u16(toks[1], ln)?;
1222 } else if eq_ci(kw, "m_bFrameBlending") && toks.len() >= 2 {
1223 em.frame_blending = parse_i32(toks[1], ln)? != 0;
1224 } else if eq_ci(kw, "m_sDepthTextureName") && toks.len() >= 2 {
1225 em.depth_texture_name = toks[1].to_string();
1226 } else if eq_ci(kw, "p2p")
1227 || eq_ci(kw, "p2p_sel")
1228 || eq_ci(kw, "affectedByWind")
1229 || eq_ci(kw, "m_isTinted")
1230 || eq_ci(kw, "bounce")
1231 || eq_ci(kw, "random")
1232 || eq_ci(kw, "inherit")
1233 || eq_ci(kw, "inheritvel")
1234 || eq_ci(kw, "inherit_local")
1235 || eq_ci(kw, "splat")
1236 || eq_ci(kw, "inherit_part")
1237 || eq_ci(kw, "depth_texture")
1238 {
1239 } else {
1242 return Ok(false);
1243 }
1244 Ok(true)
1245}
1246
1247fn parse_reference_field(
1252 toks: &[&str],
1253 ln: usize,
1254 reference: &mut MdlReference,
1255) -> Result<bool, MdlAsciiError> {
1256 let kw = toks[0];
1257
1258 if eq_ci(kw, "refModel") && toks.len() >= 2 {
1259 reference.ref_model = toks[1].to_string();
1260 } else if eq_ci(kw, "reattachable") && toks.len() >= 2 {
1261 reference.reattachable = parse_i32(toks[1], ln)?;
1262 } else {
1263 return Ok(false);
1264 }
1265 Ok(true)
1266}
1267
1268fn parse_animmesh_field(
1273 toks: &[&str],
1274 ln: usize,
1275 p: &mut AsciiParser,
1276 am: &mut MdlAnimMesh,
1277) -> Result<bool, MdlAsciiError> {
1278 let kw = toks[0];
1279
1280 if eq_ci(kw, "sampleperiod") && toks.len() >= 2 {
1281 am.sample_period = parse_f32(toks[1], ln)?;
1282 } else if eq_ci(kw, "animverts") && toks.len() >= 2 {
1283 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
1284 am.anim_verts = parse_vec3_block(p, count)?;
1285 } else if eq_ci(kw, "animtverts") && toks.len() >= 2 {
1286 let count = usize::try_from(parse_u32(toks[1], ln)?).expect("count fits in usize");
1287 am.anim_t_verts = parse_vec3_block(p, count)?;
1288 } else {
1289 return Ok(false);
1290 }
1291 Ok(true)
1292}
1293
1294fn parse_animation(
1299 p: &mut AsciiParser,
1300 anim_name: &str,
1301 _model_name: &str,
1302 _start_line: usize,
1303) -> Result<MdlAnimation, MdlAsciiError> {
1304 let mut length: f32 = 0.0;
1305 let mut transition_time: f32 = 0.0;
1306 let mut anim_root = String::new();
1307 let mut events: Vec<MdlAnimEvent> = Vec::new();
1308 let mut flat_nodes: Vec<FlatAnimNode> = Vec::new();
1309
1310 while let Some((ln, line)) = p.next_line() {
1311 let toks = tokens(&line);
1312 if toks.is_empty() {
1313 continue;
1314 }
1315 let kw = toks[0];
1316
1317 if eq_ci(kw, "length") && toks.len() >= 2 {
1318 length = parse_f32(toks[1], ln)?;
1319 } else if eq_ci(kw, "transtime") && toks.len() >= 2 {
1320 transition_time = parse_f32(toks[1], ln)?;
1321 } else if eq_ci(kw, "animroot") && toks.len() >= 2 {
1322 anim_root = toks[1].to_string();
1323 } else if eq_ci(kw, "event") && toks.len() >= 3 {
1324 let time = parse_f32(toks[1], ln)?;
1325 let name = toks[2].to_string();
1326 events.push(MdlAnimEvent { time, name });
1327 } else if eq_ci(kw, "node") && toks.len() >= 3 {
1328 let flat = parse_anim_node(p, toks[2], ln)?;
1329 flat_nodes.push(flat);
1330 } else if eq_ci(kw, "doneanim") {
1331 break;
1332 }
1333 }
1334
1335 let root_node = assemble_anim_node_tree(flat_nodes)?;
1336
1337 Ok(MdlAnimation {
1338 name: anim_name.to_string(),
1339 length,
1340 transition_time,
1341 anim_root,
1342 events,
1343 root_node,
1344 fn_ptr1: 0,
1345 fn_ptr2: 0,
1346 })
1347}
1348
1349fn parse_anim_node(
1350 p: &mut AsciiParser,
1351 name: &str,
1352 _node_line: usize,
1353) -> Result<FlatAnimNode, MdlAsciiError> {
1354 let mut parent_name = "NULL".into();
1355 let mut controllers = Vec::new();
1356
1357 while let Some((ln, line)) = p.next_line() {
1358 let toks = tokens(&line);
1359 if toks.is_empty() {
1360 continue;
1361 }
1362 let kw = toks[0];
1363
1364 if eq_ci(kw, "endnode") {
1365 break;
1366 } else if eq_ci(kw, "parent") && toks.len() >= 2 {
1367 parent_name = toks[1].to_string();
1368 } else {
1369 try_parse_controller_line(p, &toks, ln, NodeTypeContext::Base, &mut controllers)?;
1371 }
1372 }
1373
1374 Ok(FlatAnimNode {
1375 name: name.to_string(),
1376 parent_name,
1377 controllers,
1378 })
1379}
1380
1381fn assemble_node_tree(flat_nodes: Vec<FlatNode>) -> Result<MdlNode, MdlAsciiError> {
1386 if flat_nodes.is_empty() {
1387 return Err(MdlAsciiError::InvalidData("no geometry nodes found".into()));
1388 }
1389
1390 let root_idx = flat_nodes
1392 .iter()
1393 .position(|n| eq_ci(&n.parent_name, "NULL"))
1394 .ok_or_else(|| MdlAsciiError::InvalidData("no root node (parent NULL) found".into()))?;
1395
1396 let mut children_of_idx: HashMap<usize, Vec<usize>> = HashMap::new();
1400 let mut stack: Vec<(usize, String)> = Vec::new();
1401
1402 for (i, node) in flat_nodes.iter().enumerate() {
1403 if i == root_idx {
1404 stack.push((i, node.name.clone()));
1405 continue;
1406 }
1407
1408 let parent_pos = stack.iter().rposition(|(_, n)| eq_ci(n, &node.parent_name));
1411 if let Some(pos) = parent_pos {
1412 stack.truncate(pos + 1);
1414 let parent_idx = stack[pos].0;
1415 children_of_idx.entry(parent_idx).or_default().push(i);
1416 }
1417 stack.push((i, node.name.clone()));
1419 }
1420
1421 fn build_node(
1422 idx: usize,
1423 flat: &[FlatNode],
1424 children_of_idx: &HashMap<usize, Vec<usize>>,
1425 ) -> MdlNode {
1426 let f = &flat[idx];
1427 let children: Vec<MdlNode> = children_of_idx
1428 .get(&idx)
1429 .cloned()
1430 .unwrap_or_default()
1431 .iter()
1432 .map(|&ci| build_node(ci, flat, children_of_idx))
1433 .collect();
1434
1435 MdlNode {
1436 name: f.name.clone(),
1437 parent_index: None, children,
1439 position: f.position,
1440 rotation: f.rotation,
1441 node_data: f.node_data.clone(),
1442 controllers: f.controllers.clone(),
1443 orphan_controller_data: Vec::new(),
1444 header_padding_02: [0, 0],
1445 header_padding_06: [0, 0],
1446 }
1447 }
1448
1449 Ok(build_node(root_idx, &flat_nodes, &children_of_idx))
1450}
1451
1452fn assemble_anim_node_tree(flat_nodes: Vec<FlatAnimNode>) -> Result<MdlAnimNode, MdlAsciiError> {
1453 if flat_nodes.is_empty() {
1454 return Ok(MdlAnimNode {
1456 name: String::new(),
1457 node_number: 0,
1458 controllers: Vec::new(),
1459 orphan_controller_data: Vec::new(),
1460 children: Vec::new(),
1461 });
1462 }
1463
1464 let root_idx = flat_nodes
1465 .iter()
1466 .position(|n| eq_ci(&n.parent_name, "NULL"))
1467 .unwrap_or(0);
1468
1469 let mut children_of_idx: HashMap<usize, Vec<usize>> = HashMap::new();
1471 let mut stack: Vec<(usize, String)> = Vec::new();
1472 for (i, node) in flat_nodes.iter().enumerate() {
1473 if i == root_idx {
1474 stack.push((i, node.name.clone()));
1475 continue;
1476 }
1477 let parent_pos = stack.iter().rposition(|(_, n)| eq_ci(n, &node.parent_name));
1478 if let Some(pos) = parent_pos {
1479 stack.truncate(pos + 1);
1480 let parent_idx = stack[pos].0;
1481 children_of_idx.entry(parent_idx).or_default().push(i);
1482 }
1483 stack.push((i, node.name.clone()));
1484 }
1485
1486 fn build_anim(
1487 idx: usize,
1488 flat: &[FlatAnimNode],
1489 children_of_idx: &HashMap<usize, Vec<usize>>,
1490 ) -> MdlAnimNode {
1491 let f = &flat[idx];
1492 let children: Vec<MdlAnimNode> = children_of_idx
1493 .get(&idx)
1494 .cloned()
1495 .unwrap_or_default()
1496 .iter()
1497 .map(|&ci| build_anim(ci, flat, children_of_idx))
1498 .collect();
1499
1500 MdlAnimNode {
1501 name: f.name.clone(),
1502 node_number: 0, controllers: f.controllers.clone(),
1504 orphan_controller_data: Vec::new(),
1505 children,
1506 }
1507 }
1508
1509 Ok(build_anim(root_idx, &flat_nodes, &children_of_idx))
1510}
1511
1512fn build_name_index_map(node: &MdlNode) -> HashMap<String, u16> {
1517 let mut map = HashMap::new();
1518 let mut idx = 0u16;
1519 build_name_idx_recursive(node, &mut map, &mut idx);
1520 map
1521}
1522
1523fn build_name_idx_recursive(node: &MdlNode, map: &mut HashMap<String, u16>, idx: &mut u16) {
1524 map.insert(node.name.clone(), *idx);
1525 *idx += 1;
1526 for child in &node.children {
1527 build_name_idx_recursive(child, map, idx);
1528 }
1529}
1530
1531fn subtract_geo_positions_from_anim(
1532 node: &mut MdlAnimNode,
1533 geo_positions: &HashMap<&str, [f32; 3]>,
1534) {
1535 if let Some(geo_pos) = geo_positions.get(node.name.as_str()) {
1536 for ctrl in &mut node.controllers {
1537 if ctrl.controller_type == MdlControllerType::POSITION {
1538 for key in &mut ctrl.keys {
1539 if key.values.len() >= 3 {
1540 key.values[0] -= geo_pos[0];
1541 key.values[1] -= geo_pos[1];
1542 key.values[2] -= geo_pos[2];
1543 }
1544 }
1545 }
1546 }
1547 }
1548 for child in &mut node.children {
1549 subtract_geo_positions_from_anim(child, geo_positions);
1550 }
1551}
1552
1553fn assign_anim_node_numbers(node: &mut MdlAnimNode, name_to_index: &HashMap<String, u16>) {
1554 node.node_number = name_to_index.get(&node.name).copied().unwrap_or(0);
1555 for child in &mut node.children {
1556 assign_anim_node_numbers(child, name_to_index);
1557 }
1558}
1559
1560fn strip_suffix_ci<'a>(s: &'a str, suffix: &str) -> Option<&'a str> {
1565 let s_lower = s.to_ascii_lowercase();
1566 let suffix_lower = suffix.to_ascii_lowercase();
1567 if s_lower.ends_with(&suffix_lower) {
1568 Some(&s[..s.len() - suffix.len()])
1569 } else {
1570 None
1571 }
1572}
1573
1574#[cfg(test)]
1579mod tests {
1580 use super::*;
1581 use crate::mdl::ascii_writer::write_mdl_ascii_to_string;
1582
1583 #[test]
1584 fn minimal_model_parse() {
1585 let input = "\
1586newmodel test
1587setsupermodel test NULL
1588classification other
1589classification_unk1 0
1590ignorefog 0
1591setanimationscale 1.0
1592compress_quaternions 0
1593headlink 0
1594beginmodelgeom test
1595 bmin -1.0 -1.0 -1.0
1596 bmax 1.0 1.0 1.0
1597 radius 1.73
1598 node dummy test
1599 parent NULL
1600 endnode
1601endmodelgeom test
1602donemodel test
1603";
1604 let mdl = read_mdl_ascii_from_str(input).unwrap();
1605 assert_eq!(mdl.root_node.name, "test");
1606 assert_eq!(mdl.supermodel_name, "NULL");
1607 assert_eq!(mdl.classification, 0);
1608 assert_eq!(mdl.affected_by_fog, 1);
1609 assert_eq!(mdl.node_count, 1);
1610 }
1611
1612 #[test]
1613 fn mesh_node_parse() {
1614 let input = "\
1615newmodel m
1616setsupermodel m NULL
1617classification other
1618beginmodelgeom m
1619 node trimesh mesh1
1620 parent NULL
1621 diffuse 0.8 0.8 0.8
1622 ambient 0.2 0.2 0.2
1623 bitmap texture_a
1624 render 1
1625 verts 3
1626 0.0 0.0 0.0
1627 1.0 0.0 0.0
1628 0.0 1.0 0.0
1629 faces 1
1630 0 1 2 1 0 1 2 0
1631 tverts 3
1632 0.0 0.0
1633 1.0 0.0
1634 0.0 1.0
1635 endnode
1636endmodelgeom m
1637donemodel m
1638";
1639 let mdl = read_mdl_ascii_from_str(input).unwrap();
1640 let mesh = mdl.root_node.node_data.mesh().unwrap();
1641 assert_eq!(mesh.positions.len(), 3);
1642 assert_eq!(mesh.faces.len(), 1);
1643 assert_eq!(mesh.uv1.len(), 3);
1644 assert_eq!(mesh.texture_0, "texture_a");
1645 assert_eq!(mesh.faces[0].vertex_indices, [0, 1, 2]);
1646 assert_eq!(mesh.vertex_count, 3);
1647 }
1648
1649 #[test]
1650 fn controller_keyed_parse() {
1651 let input = "\
1652newmodel m
1653setsupermodel m NULL
1654classification other
1655beginmodelgeom m
1656 node dummy root
1657 parent NULL
1658 positionkey
1659 0.0 1.0 2.0 3.0
1660 0.5 4.0 5.0 6.0
1661 endlist
1662 endnode
1663endmodelgeom m
1664donemodel m
1665";
1666 let mdl = read_mdl_ascii_from_str(input).unwrap();
1667 assert_eq!(mdl.root_node.controllers.len(), 1);
1668 let ctrl = &mdl.root_node.controllers[0];
1669 assert_eq!(ctrl.controller_type, MdlControllerType::POSITION);
1670 assert_eq!(ctrl.keys.len(), 2);
1671 assert_eq!(ctrl.keys[0].time, 0.0);
1672 assert_eq!(ctrl.keys[0].values, vec![1.0, 2.0, 3.0]);
1673 assert_eq!(ctrl.keys[1].time, 0.5);
1674 assert_eq!(ctrl.keys[1].values, vec![4.0, 5.0, 6.0]);
1675 }
1676
1677 #[test]
1678 fn orientation_conversion() {
1679 let input = "\
1680newmodel m
1681setsupermodel m NULL
1682classification other
1683beginmodelgeom m
1684 node dummy root
1685 parent NULL
1686 orientation 0.0 0.0 1.0 1.5707963
1687 endnode
1688endmodelgeom m
1689donemodel m
1690";
1691 let mdl = read_mdl_ascii_from_str(input).unwrap();
1692 let q = mdl.root_node.rotation;
1694 let expected_half = std::f32::consts::FRAC_1_SQRT_2;
1696 assert!((q[0] - expected_half).abs() < 0.01, "w = {}", q[0]);
1697 assert!(q[1].abs() < 0.01, "x = {}", q[1]);
1698 assert!(q[2].abs() < 0.01, "y = {}", q[2]);
1699 assert!((q[3] - expected_half).abs() < 0.01, "z = {}", q[3]);
1700 }
1701
1702 #[test]
1703 fn animation_parse() {
1704 let input = "\
1705newmodel m
1706setsupermodel m NULL
1707classification other
1708beginmodelgeom m
1709 node dummy root
1710 parent NULL
1711 position 10.0 20.0 30.0
1712 endnode
1713endmodelgeom m
1714newanim walk m
1715 length 1.0
1716 transtime 0.25
1717 animroot root
1718 event 0.5 footstep
1719 node dummy root
1720 parent NULL
1721 positionkey
1722 0.0 10.0 20.0 30.0
1723 1.0 11.0 21.0 31.0
1724 endlist
1725 endnode
1726doneanim walk m
1727donemodel m
1728";
1729 let mdl = read_mdl_ascii_from_str(input).unwrap();
1730 assert_eq!(mdl.animations.len(), 1);
1731 let anim = &mdl.animations[0];
1732 assert_eq!(anim.name, "walk");
1733 assert_eq!(anim.length, 1.0);
1734 assert_eq!(anim.anim_root, "root");
1735 assert_eq!(anim.events.len(), 1);
1736 assert_eq!(anim.events[0].name, "footstep");
1737
1738 let ctrl = &anim.root_node.controllers[0];
1742 assert_eq!(ctrl.controller_type, MdlControllerType::POSITION);
1743 assert!((ctrl.keys[0].values[0]).abs() < 0.001);
1744 assert!((ctrl.keys[0].values[1]).abs() < 0.001);
1745 assert!((ctrl.keys[0].values[2]).abs() < 0.001);
1746 assert!((ctrl.keys[1].values[0] - 1.0).abs() < 0.001);
1747 assert!((ctrl.keys[1].values[1] - 1.0).abs() < 0.001);
1748 assert!((ctrl.keys[1].values[2] - 1.0).abs() < 0.001);
1749 }
1750
1751 #[test]
1752 fn aabb_tree_reconstruction() {
1753 let input = "\
1755newmodel m
1756setsupermodel m NULL
1757classification other
1758beginmodelgeom m
1759 node aabb walkmesh
1760 parent NULL
1761 verts 4
1762 0.0 0.0 0.0
1763 1.0 0.0 0.0
1764 1.0 1.0 0.0
1765 0.0 1.0 0.0
1766 faces 2
1767 0 1 2 1 0 1 2 0
1768 0 2 3 1 0 2 3 0
1769 aabb
1770 0.0 0.0 0.0 1.0 0.5 0.0 0
1771 0.0 0.5 0.0 1.0 1.0 0.0 1
1772 endnode
1773endmodelgeom m
1774donemodel m
1775";
1776 let mdl = read_mdl_ascii_from_str(input).unwrap();
1777 if let MdlNodeData::Aabb(ref aabb) = mdl.root_node.node_data {
1778 assert!(aabb.aabb_tree.is_some());
1779 let tree = aabb.aabb_tree.as_ref().unwrap();
1780 assert_eq!(tree.face_index, -1);
1782 assert!(tree.left.is_some());
1783 assert!(tree.right.is_some());
1784 } else {
1785 panic!("expected AABB node");
1786 }
1787 }
1788
1789 fn ascii_self_roundtrip(input: &str) {
1794 let mdl = read_mdl_ascii_from_str(input).unwrap();
1795 let ascii1 = write_mdl_ascii_to_string(&mdl).unwrap();
1796 let mdl2 = read_mdl_ascii_from_str(&ascii1).unwrap();
1797 let ascii2 = write_mdl_ascii_to_string(&mdl2).unwrap();
1798 if ascii1 != ascii2 {
1799 for (i, (a, b)) in ascii1.lines().zip(ascii2.lines()).enumerate() {
1801 if a != b {
1802 panic!(
1803 "ASCII self round-trip mismatch at line {}:\n pass 1: {}\n pass 2: {}",
1804 i + 1,
1805 a,
1806 b
1807 );
1808 }
1809 }
1810 let c1 = ascii1.lines().count();
1811 let c2 = ascii2.lines().count();
1812 if c1 != c2 {
1813 panic!("ASCII self round-trip: line count differs ({c1} vs {c2})");
1814 }
1815 }
1816 }
1817
1818 #[test]
1819 fn self_roundtrip_minimal() {
1820 let input = "\
1821newmodel test
1822setsupermodel test NULL
1823classification other
1824classification_unk1 0
1825ignorefog 0
1826setanimationscale 1.0
1827compress_quaternions 0
1828headlink 0
1829beginmodelgeom test
1830 bmin -1.0 -1.0 -1.0
1831 bmax 1.0 1.0 1.0
1832 radius 1.73
1833 node dummy test
1834 parent NULL
1835 endnode
1836endmodelgeom test
1837donemodel test
1838";
1839 ascii_self_roundtrip(input);
1840 }
1841
1842 #[test]
1843 fn self_roundtrip_mesh_with_controllers() {
1844 let input = "\
1845newmodel m
1846setsupermodel m NULL
1847classification other
1848classification_unk1 0
1849ignorefog 0
1850setanimationscale 1.0
1851compress_quaternions 0
1852headlink 0
1853beginmodelgeom m
1854 bmin -1.0 -1.0 -1.0
1855 bmax 1.0 1.0 1.0
1856 radius 1.73
1857 node trimesh mesh1
1858 parent NULL
1859 diffuse 0.8 0.8 0.8
1860 ambient 0.2 0.2 0.2
1861 bitmap texture_a
1862 render 1
1863 shadow 0
1864 verts 3
1865 0.0 0.0 0.0
1866 1.0 0.0 0.0
1867 0.0 1.0 0.0
1868 faces 1
1869 0 1 2 1 0 1 2 0
1870 tverts 3
1871 0.0 0.0
1872 1.0 0.0
1873 0.0 1.0
1874 endnode
1875endmodelgeom m
1876donemodel m
1877";
1878 ascii_self_roundtrip(input);
1879 }
1880
1881 #[test]
1882 fn self_roundtrip_animation() {
1883 let input = "\
1884newmodel m
1885setsupermodel m NULL
1886classification character
1887classification_unk1 0
1888ignorefog 0
1889setanimationscale 1.0
1890compress_quaternions 0
1891headlink 0
1892beginmodelgeom m
1893 bmin -1.0 -1.0 -1.0
1894 bmax 1.0 1.0 1.0
1895 radius 1.73
1896 node dummy root
1897 parent NULL
1898 position 10.0 20.0 30.0
1899 endnode
1900endmodelgeom m
1901newanim walk m
1902 length 1.0
1903 transtime 0.25
1904 animroot root
1905 event 0.5 footstep
1906 node dummy root
1907 parent NULL
1908 positionkey
1909 0.0 10.0 20.0 30.0
1910 1.0 11.0 21.0 31.0
1911 endlist
1912 endnode
1913doneanim walk m
1914donemodel m
1915";
1916 ascii_self_roundtrip(input);
1917 }
1918
1919 fn assert_nodes_equivalent(a: &super::super::MdlNode, b: &super::super::MdlNode, path: &str) {
1926 assert_eq!(a.name, b.name, "{path}: name mismatch");
1927 assert_eq!(
1928 std::mem::discriminant(&a.node_data),
1929 std::mem::discriminant(&b.node_data),
1930 "{path}: node type mismatch"
1931 );
1932 for i in 0..3 {
1934 assert!(
1935 (a.position[i] - b.position[i]).abs() < 1e-4,
1936 "{path}: position[{i}] {:.6} vs {:.6}",
1937 a.position[i],
1938 b.position[i]
1939 );
1940 }
1941 for i in 0..4 {
1944 assert!(
1945 (a.rotation[i] - b.rotation[i]).abs() < 2e-3,
1946 "{path}: rotation[{i}] {:.6} vs {:.6}",
1947 a.rotation[i],
1948 b.rotation[i]
1949 );
1950 }
1951 for cb in &b.controllers {
1955 if let Some(ca) = a
1956 .controllers
1957 .iter()
1958 .find(|c| c.controller_type == cb.controller_type)
1959 {
1960 assert_eq!(
1961 ca.keys.len(),
1962 cb.keys.len(),
1963 "{path}: controller {:?} key count ({} vs {})",
1964 cb.controller_type,
1965 ca.keys.len(),
1966 cb.keys.len()
1967 );
1968 } else {
1969 panic!(
1970 "{path}: roundtripped has controller {:?} not in original",
1971 cb.controller_type
1972 );
1973 }
1974 }
1975 if let (Some(ma), Some(mb)) = (a.node_data.mesh(), b.node_data.mesh()) {
1977 assert_eq!(
1978 ma.positions.len(),
1979 mb.positions.len(),
1980 "{path}: vertex count"
1981 );
1982 assert_eq!(ma.faces.len(), mb.faces.len(), "{path}: face count");
1983 assert_eq!(ma.uv1.len(), mb.uv1.len(), "{path}: uv1 count");
1984 }
1985 assert_eq!(
1987 a.children.len(),
1988 b.children.len(),
1989 "{path}: child count ({} vs {})",
1990 a.children.len(),
1991 b.children.len()
1992 );
1993 for (ca, cb) in a.children.iter().zip(b.children.iter()) {
1994 assert_nodes_equivalent(ca, cb, &format!("{path}/{}", ca.name));
1995 }
1996 }
1997
1998 fn assert_anims_equivalent(a: &[super::super::MdlAnimation], b: &[super::super::MdlAnimation]) {
1999 assert_eq!(a.len(), b.len(), "animation count");
2000 for (i, (aa, ab)) in a.iter().zip(b.iter()).enumerate() {
2001 assert_eq!(aa.name, ab.name, "anim[{i}] name");
2002 assert!((aa.length - ab.length).abs() < 1e-4, "anim[{i}] length");
2003 assert_eq!(aa.anim_root, ab.anim_root, "anim[{i}] anim_root");
2004 assert_eq!(aa.events.len(), ab.events.len(), "anim[{i}] event count");
2005 }
2006 }
2007
2008 fn k1_override_dir() -> Option<String> {
2011 std::env::var("KOTOR_GAME_DIR")
2012 .ok()
2013 .map(|d| format!("{d}/Override"))
2014 }
2015
2016 fn binary_ascii_roundtrip(mdl_path: &str, mdx_path: Option<&str>) {
2018 let mdl_data = match std::fs::read(mdl_path) {
2019 Ok(d) => d,
2020 Err(_) => return,
2021 };
2022 let mdx_data = mdx_path.and_then(|p| std::fs::read(p).ok());
2023 let original =
2024 super::super::reader::read_mdl_from_bytes(&mdl_data, mdx_data.as_deref()).unwrap();
2025
2026 let ascii = write_mdl_ascii_to_string(&original).unwrap();
2027 let roundtripped = read_mdl_ascii_from_str(&ascii).unwrap();
2028
2029 assert_eq!(
2031 original.root_node.name, roundtripped.root_node.name,
2032 "model_name"
2033 );
2034 assert_eq!(
2035 original.supermodel_name, roundtripped.supermodel_name,
2036 "supermodel_name"
2037 );
2038 assert_eq!(
2039 original.classification, roundtripped.classification,
2040 "classification"
2041 );
2042 assert_eq!(
2043 original.affected_by_fog, roundtripped.affected_by_fog,
2044 "affected_by_fog"
2045 );
2046 assert!(
2047 (original.animation_scale - roundtripped.animation_scale).abs() < 1e-4,
2048 "animation_scale"
2049 );
2050 assert!(roundtripped.node_count > 0, "node_count should be > 0");
2053
2054 assert_nodes_equivalent(
2056 &original.root_node,
2057 &roundtripped.root_node,
2058 &original.root_node.name,
2059 );
2060
2061 assert_anims_equivalent(&original.animations, &roundtripped.animations);
2063 }
2064
2065 #[test]
2066 fn roundtrip_vanilla_item() {
2067 let base = match k1_override_dir() {
2069 Some(d) => d,
2070 None => return,
2071 };
2072 binary_ascii_roundtrip(
2073 &format!("{base}/i_adrnaline_001.mdl"),
2074 Some(&format!("{base}/i_adrnaline_001.mdx")),
2075 );
2076 }
2077
2078 #[test]
2079 fn roundtrip_vanilla_placeable() {
2080 let base = match k1_override_dir() {
2082 Some(d) => d,
2083 None => return,
2084 };
2085 binary_ascii_roundtrip(&format!("{base}/3dgui.mdl"), None);
2086 }
2087
2088 #[test]
2089 fn roundtrip_vanilla_character() {
2090 let base = match k1_override_dir() {
2092 Some(d) => d,
2093 None => return,
2094 };
2095 binary_ascii_roundtrip(
2096 &format!("{base}/p_bastilabb.mdl"),
2097 Some(&format!("{base}/p_bastilabb.mdx")),
2098 );
2099 }
2100
2101 #[test]
2102 fn roundtrip_vanilla_supermodel() {
2103 let base = match k1_override_dir() {
2105 Some(d) => d,
2106 None => return,
2107 };
2108 binary_ascii_roundtrip(
2109 &format!("{base}/s_female03.mdl"),
2110 Some(&format!("{base}/s_female03.mdx")),
2111 );
2112 }
2113
2114 #[test]
2115 fn roundtrip_vanilla_effect() {
2116 let base = match k1_override_dir() {
2118 Some(d) => d,
2119 None => return,
2120 };
2121 binary_ascii_roundtrip(&format!("{base}/fx_carbref.mdl"), None);
2122 }
2123}