1use std::collections::HashMap;
14use std::io::Write;
15
16use super::ascii_names::{
17 classification_to_ascii, controller_ascii_name, node_type_ascii_name, node_type_context,
18};
19use super::controllers::{MdlController, MdlControllerType, MdlKey, CTRL_FLAG_BEZIER};
20use super::orientation::quat_to_axis_angle;
21use super::types::{AabbNode, MdlNodeData};
22use super::{collect_geo_positions, Mdl, MdlAnimNode, MdlAnimation, MdlNode};
23
24#[derive(Debug)]
26pub enum MdlAsciiError {
27 Io(std::io::Error),
29 InvalidData(String),
31 Parse {
33 line: usize,
35 message: String,
37 },
38}
39
40impl std::fmt::Display for MdlAsciiError {
41 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42 match self {
43 Self::Io(e) => write!(f, "I/O error: {e}"),
44 Self::InvalidData(msg) => write!(f, "invalid data: {msg}"),
45 Self::Parse { line, message } => write!(f, "line {line}: {message}"),
46 }
47 }
48}
49
50impl std::error::Error for MdlAsciiError {
51 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
52 match self {
53 Self::Io(e) => Some(e),
54 Self::InvalidData(_) | Self::Parse { .. } => None,
55 }
56 }
57}
58
59impl From<std::io::Error> for MdlAsciiError {
60 fn from(e: std::io::Error) -> Self {
61 Self::Io(e)
62 }
63}
64
65#[cfg_attr(
76 feature = "tracing",
77 tracing::instrument(level = "debug", skip(w, mdl))
78)]
79pub fn write_mdl_ascii<W: Write>(w: &mut W, mdl: &Mdl) -> Result<(), MdlAsciiError> {
80 let name = &mdl.root_node.name;
81 let super_name = if mdl.supermodel_name.is_empty() {
82 "NULL"
83 } else {
84 &mdl.supermodel_name
85 };
86
87 writeln!(w, "newmodel {name}")?;
89 writeln!(w, "setsupermodel {name} {super_name}")?;
90 writeln!(
91 w,
92 "classification {}",
93 classification_to_ascii(mdl.classification)
94 )?;
95 writeln!(w, "classification_unk1 {}", mdl.subclassification)?;
96 let ignore_fog = if mdl.affected_by_fog != 0 { 0 } else { 1 };
97 writeln!(w, "ignorefog {ignore_fog}")?;
98 writeln!(w, "setanimationscale {}", format_float(mdl.animation_scale))?;
99
100 let has_compressed_quats = has_compressed_quaternions(mdl);
103 writeln!(
104 w,
105 "compress_quaternions {}",
106 i32::from(has_compressed_quats)
107 )?;
108
109 let is_headlinked = mdl.anim_root_node.is_some();
112 writeln!(w, "headlink {}", i32::from(is_headlinked))?;
113
114 writeln!(w, "beginmodelgeom {name}")?;
116 writeln!(
117 w,
118 " bmin {} {} {}",
119 format_float(mdl.bounding_box[0]),
120 format_float(mdl.bounding_box[1]),
121 format_float(mdl.bounding_box[2])
122 )?;
123 writeln!(
124 w,
125 " bmax {} {} {}",
126 format_float(mdl.bounding_box[3]),
127 format_float(mdl.bounding_box[4]),
128 format_float(mdl.bounding_box[5])
129 )?;
130 writeln!(w, " radius {}", format_float(mdl.radius))?;
131
132 write_node(w, &mdl.root_node, None, &mdl.root_node)?;
134
135 writeln!(w, "endmodelgeom {name}")?;
136
137 let geo_positions = collect_geo_positions(&mdl.root_node);
142
143 for anim in &mdl.animations {
145 write_animation(w, anim, name, &geo_positions)?;
146 }
147
148 writeln!(w, "donemodel {name}")?;
149 Ok(())
150}
151
152#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(mdl)))]
159pub fn write_mdl_ascii_to_string(mdl: &Mdl) -> Result<String, MdlAsciiError> {
160 let mut buf = Vec::new();
161 write_mdl_ascii(&mut buf, mdl)?;
162 String::from_utf8(buf)
163 .map_err(|e| MdlAsciiError::InvalidData(format!("output is not valid UTF-8: {e}")))
164}
165
166fn write_node<W: Write>(
172 w: &mut W,
173 node: &MdlNode,
174 parent_name: Option<&str>,
175 model_root: &MdlNode,
176) -> Result<(), MdlAsciiError> {
177 let type_str = node_type_ascii_name(&node.node_data);
178 let parent_str = parent_name.unwrap_or("NULL");
179
180 writeln!(w, "node {type_str} {}", node.name)?;
181 writeln!(w, " parent {parent_str}")?;
182
183 let has_pos_ctrl = node
186 .controllers
187 .iter()
188 .any(|c| c.controller_type == MdlControllerType::POSITION);
189 let has_ori_ctrl = node
190 .controllers
191 .iter()
192 .any(|c| c.controller_type == MdlControllerType::ORIENTATION);
193
194 if !has_pos_ctrl {
197 let [px, py, pz] = node.position;
198 if px != 0.0 || py != 0.0 || pz != 0.0 {
199 writeln!(
200 w,
201 " position {} {} {}",
202 format_float(px),
203 format_float(py),
204 format_float(pz)
205 )?;
206 }
207 }
208
209 if !has_ori_ctrl {
212 let aa = quat_to_axis_angle(node.rotation);
213 if aa[3] != 0.0 {
214 writeln!(
215 w,
216 " orientation {} {} {} {}",
217 format_float(aa[0]),
218 format_float(aa[1]),
219 format_float(aa[2]),
220 format_float(aa[3])
221 )?;
222 }
223 }
224
225 let ctx = node_type_context(&node.node_data);
229 for ctrl in &node.controllers {
230 write_controller(w, ctrl, ctx, " ")?;
231 }
232
233 write_node_data(w, &node.node_data, model_root)?;
235
236 writeln!(w, "endnode")?;
237
238 for child in &node.children {
240 write_node(w, child, Some(&node.name), model_root)?;
241 }
242
243 Ok(())
244}
245
246fn write_node_data<W: Write>(
248 w: &mut W,
249 data: &MdlNodeData,
250 model_root: &MdlNode,
251) -> Result<(), MdlAsciiError> {
252 match data {
253 MdlNodeData::Base | MdlNodeData::Camera(_) => {}
254 MdlNodeData::Mesh(mesh) => write_mesh_fields(w, mesh)?,
255 MdlNodeData::Skin(skin) => {
256 write_mesh_fields(w, &skin.mesh)?;
257 write_skin_fields(w, skin, model_root)?;
258 }
259 MdlNodeData::AnimMesh(am) => {
260 write_mesh_fields(w, &am.mesh)?;
261 write_animmesh_fields(w, am)?;
262 }
263 MdlNodeData::Dangly(dangly) => {
264 write_mesh_fields(w, &dangly.mesh)?;
265 write_dangly_fields(w, dangly)?;
266 }
267 MdlNodeData::Aabb(aabb) => {
268 write_mesh_fields(w, &aabb.mesh)?;
269 write_aabb_fields(w, aabb)?;
270 }
271 MdlNodeData::Saber(saber) => {
272 write_mesh_fields(w, &saber.mesh)?;
273 }
274 MdlNodeData::Light(light) => write_light_fields(w, light)?,
275 MdlNodeData::Emitter(emitter) => write_emitter_fields(w, emitter)?,
276 MdlNodeData::Reference(reference) => write_reference_fields(w, reference)?,
277 }
278 Ok(())
279}
280
281fn write_mesh_fields<W: Write>(
289 w: &mut W,
290 mesh: &super::types::MdlMesh,
291) -> Result<(), MdlAsciiError> {
292 writeln!(
294 w,
295 " diffuse {} {} {}",
296 format_float(mesh.diffuse_color[0]),
297 format_float(mesh.diffuse_color[1]),
298 format_float(mesh.diffuse_color[2])
299 )?;
300 writeln!(
301 w,
302 " ambient {} {} {}",
303 format_float(mesh.ambient_color[0]),
304 format_float(mesh.ambient_color[1]),
305 format_float(mesh.ambient_color[2])
306 )?;
307 writeln!(w, " transparencyhint {}", mesh.transparency_hint)?;
308
309 writeln!(w, " animateuv {}", mesh.animate_uv)?;
311 writeln!(w, " uvdirectionx {}", format_float(mesh.uv_direction_x))?;
312 writeln!(w, " uvdirectiony {}", format_float(mesh.uv_direction_y))?;
313 writeln!(w, " uvjitter {}", format_float(mesh.uv_jitter))?;
314 writeln!(w, " uvjitterspeed {}", format_float(mesh.uv_jitter_speed))?;
315
316 writeln!(w, " lightmapped {}", i32::from(mesh.light_mapped))?;
318 writeln!(w, " rotatetexture {}", i32::from(mesh.rotate_texture))?;
319 writeln!(
320 w,
321 " m_bIsBackgroundGeometry {}",
322 i32::from(mesh.is_background_geometry)
323 )?;
324 writeln!(w, " shadow {}", i32::from(mesh.shadow))?;
325 writeln!(w, " beaming {}", i32::from(mesh.beaming))?;
326 writeln!(w, " render {}", i32::from(mesh.render))?;
327
328 writeln!(w, " dirt_enabled 0")?;
330 writeln!(w, " dirt_texture 1")?;
331 writeln!(w, " dirt_worldspace 1")?;
332 writeln!(w, " hologram_donotdraw 0")?;
333
334 let has_tangent = !mesh.tangent_space.is_empty();
336 writeln!(w, " tangentspace {}", i32::from(has_tangent))?;
337
338 writeln!(w, " inv_count {}", mesh.inverted_counter)?;
340
341 if mesh.texture_0.is_empty() {
343 writeln!(w, " bitmap NULL")?;
344 } else {
345 writeln!(w, " bitmap {}", mesh.texture_0)?;
346 }
347 if !mesh.texture_1.is_empty() {
348 writeln!(w, " bitmap2 {}", mesh.texture_1)?;
349 }
350
351 let vert_count = mesh.positions.len();
353 if vert_count > 0 {
354 writeln!(w, " verts {vert_count}")?;
355 for pos in &mesh.positions {
356 writeln!(
357 w,
358 " {} {} {}",
359 format_float(pos[0]),
360 format_float(pos[1]),
361 format_float(pos[2])
362 )?;
363 }
364 }
365
366 if !mesh.faces.is_empty() {
368 let has_uvs = !mesh.uv1.is_empty();
369 writeln!(w, " faces {}", mesh.faces.len())?;
370 for face in &mesh.faces {
371 let [v0, v1, v2] = face.vertex_indices;
372 let (tv0, tv1, tv2) = if has_uvs { (v0, v1, v2) } else { (0, 0, 0) };
376 writeln!(
377 w,
378 " {v0} {v1} {v2} 1 {tv0} {tv1} {tv2} {}",
379 face.surface_id
380 )?;
381 }
382 }
383
384 write_tverts(w, &mesh.uv1, "tverts")?;
386 write_tverts(w, &mesh.uv2, "tverts1")?;
387 write_tverts(w, &mesh.uv3, "tverts2")?;
388 write_tverts(w, &mesh.uv4, "tverts3")?;
389
390 if !mesh.vertex_colors.is_empty() {
392 writeln!(w, " colors {}", mesh.vertex_colors.len())?;
393 for c in &mesh.vertex_colors {
394 writeln!(
395 w,
396 " {} {} {}",
397 format_float(f32::from(c[0]) / 255.0),
398 format_float(f32::from(c[1]) / 255.0),
399 format_float(f32::from(c[2]) / 255.0)
400 )?;
401 }
402 }
403
404 Ok(())
405}
406
407fn write_tverts<W: Write>(w: &mut W, uvs: &[[f32; 2]], keyword: &str) -> Result<(), MdlAsciiError> {
409 if !uvs.is_empty() {
410 writeln!(w, " {keyword} {}", uvs.len())?;
411 for uv in uvs {
412 writeln!(w, " {} {}", format_float(uv[0]), format_float(uv[1]))?;
413 }
414 }
415 Ok(())
416}
417
418fn write_skin_fields<W: Write>(
424 w: &mut W,
425 skin: &super::types::MdlSkin,
426 model_root: &MdlNode,
427) -> Result<(), MdlAsciiError> {
428 let vert_count = skin.mesh.positions.len();
429 if vert_count == 0 || skin.bone_weights.is_empty() || skin.bone_indices.is_empty() {
430 return Ok(());
431 }
432
433 let node_names = collect_node_names(model_root);
440 let mut bone_index_to_name: Vec<Option<String>> = Vec::new();
441 for (tree_pos, &raw) in skin.bonemap.iter().enumerate() {
442 let mdx_bone_idx_f = f32::from_bits(raw);
443 if mdx_bone_idx_f >= 0.0 {
444 #[allow(
447 clippy::cast_possible_truncation,
448 clippy::cast_sign_loss,
449 clippy::as_conversions
450 )]
451 let mdx_bone_idx = mdx_bone_idx_f as usize;
452 if mdx_bone_idx >= bone_index_to_name.len() {
453 bone_index_to_name.resize(mdx_bone_idx + 1, None);
454 }
455 bone_index_to_name[mdx_bone_idx] = node_names.get(tree_pos).cloned();
456 }
457 }
458
459 writeln!(w, " weights {vert_count}")?;
460 for (weights, indices) in skin.bone_weights.iter().zip(&skin.bone_indices) {
461 let mut parts = Vec::new();
462 for j in 0..4 {
463 let weight = weights[j];
464 let bone_idx_f = indices[j];
465
466 if weight > 0.0 && bone_idx_f >= 0.0 {
467 #[allow(
470 clippy::cast_possible_truncation,
471 clippy::cast_sign_loss,
472 clippy::as_conversions
473 )]
474 let bone_idx = bone_idx_f as usize;
475 let bone_name = bone_index_to_name
476 .get(bone_idx)
477 .and_then(|opt| opt.as_ref())
478 .cloned()
479 .unwrap_or_else(|| format!("bone_{bone_idx}"));
480 parts.push(format!("{bone_name} {}", format_float(weight)));
481 }
482 }
483
484 if parts.is_empty() {
485 writeln!(w, " ")?;
486 } else {
487 writeln!(w, " {}", parts.join(" "))?;
488 }
489 }
490
491 Ok(())
492}
493
494fn has_compressed_quaternions(mdl: &Mdl) -> bool {
497 if node_has_compressed_quats(&mdl.root_node) {
499 return true;
500 }
501 for anim in &mdl.animations {
503 if anim_node_has_compressed_quats(&anim.root_node) {
504 return true;
505 }
506 }
507 false
508}
509
510fn node_has_compressed_quats(node: &MdlNode) -> bool {
511 for ctrl in &node.controllers {
512 if ctrl.controller_type == MdlControllerType::ORIENTATION && ctrl.raw_column_count == 2 {
513 return true;
514 }
515 }
516 node.children.iter().any(node_has_compressed_quats)
517}
518
519fn anim_node_has_compressed_quats(node: &MdlAnimNode) -> bool {
520 for ctrl in &node.controllers {
521 if ctrl.controller_type == MdlControllerType::ORIENTATION && ctrl.raw_column_count == 2 {
522 return true;
523 }
524 }
525 node.children.iter().any(anim_node_has_compressed_quats)
526}
527
528fn collect_node_names(node: &MdlNode) -> Vec<String> {
530 let mut names = Vec::new();
531 collect_names_recursive(node, &mut names);
532 names
533}
534
535fn collect_names_recursive(node: &MdlNode, names: &mut Vec<String>) {
536 names.push(node.name.clone());
537 for child in &node.children {
538 collect_names_recursive(child, names);
539 }
540}
541
542fn write_animmesh_fields<W: Write>(
548 w: &mut W,
549 am: &super::types::MdlAnimMesh,
550) -> Result<(), MdlAsciiError> {
551 writeln!(w, " sampleperiod {}", format_float(am.sample_period))?;
552
553 if !am.anim_verts.is_empty() {
554 writeln!(w, " animverts {}", am.anim_verts.len())?;
555 for v in &am.anim_verts {
556 writeln!(
557 w,
558 " {} {} {}",
559 format_float(v[0]),
560 format_float(v[1]),
561 format_float(v[2])
562 )?;
563 }
564 }
565
566 if !am.anim_t_verts.is_empty() {
567 writeln!(w, " animtverts {}", am.anim_t_verts.len())?;
568 for v in &am.anim_t_verts {
569 writeln!(
570 w,
571 " {} {} {}",
572 format_float(v[0]),
573 format_float(v[1]),
574 format_float(v[2])
575 )?;
576 }
577 }
578
579 Ok(())
580}
581
582fn write_dangly_fields<W: Write>(
588 w: &mut W,
589 dangly: &super::types::MdlDangly,
590) -> Result<(), MdlAsciiError> {
591 writeln!(w, " displacement {}", format_float(dangly.displacement))?;
592 writeln!(w, " tightness {}", format_float(dangly.tightness))?;
593 writeln!(w, " period {}", format_float(dangly.period))?;
594
595 if !dangly.constraints.is_empty() {
596 writeln!(w, " constraints {}", dangly.constraints.len())?;
597 for c in &dangly.constraints {
598 writeln!(w, " {}", format_float(*c))?;
599 }
600 }
601
602 Ok(())
603}
604
605fn write_aabb_fields<W: Write>(
611 w: &mut W,
612 aabb: &super::types::MdlAabb,
613) -> Result<(), MdlAsciiError> {
614 if let Some(tree) = &aabb.aabb_tree {
615 let mut leaves = Vec::new();
617 collect_aabb_leaves(tree, &mut leaves);
618
619 if !leaves.is_empty() {
620 writeln!(w, " aabb")?;
621 for leaf in &leaves {
622 writeln!(
623 w,
624 " {} {} {} {} {} {} {}",
625 format_float(leaf.box_min[0]),
626 format_float(leaf.box_min[1]),
627 format_float(leaf.box_min[2]),
628 format_float(leaf.box_max[0]),
629 format_float(leaf.box_max[1]),
630 format_float(leaf.box_max[2]),
631 leaf.face_index
632 )?;
633 }
634 }
635 }
636 Ok(())
637}
638
639fn collect_aabb_leaves<'a>(node: &'a AabbNode, leaves: &mut Vec<&'a AabbNode>) {
641 if node.face_index >= 0 {
642 leaves.push(node);
644 }
645 if let Some(left) = &node.left {
646 collect_aabb_leaves(left, leaves);
647 }
648 if let Some(right) = &node.right {
649 collect_aabb_leaves(right, leaves);
650 }
651}
652
653fn write_light_fields<W: Write>(
659 w: &mut W,
660 light: &super::types::MdlLight,
661) -> Result<(), MdlAsciiError> {
662 writeln!(w, " lightpriority {}", light.priority)?;
663 writeln!(w, " ambientonly {}", light.ambientonly)?;
664 writeln!(w, " ndynamictype {}", light.num_dynamic_types)?;
665 writeln!(w, " affectdynamic {}", light.affectdynamic)?;
666 writeln!(w, " shadow {}", light.shadow)?;
667 writeln!(w, " generateflare {}", light.generateflare)?;
668 writeln!(w, " fadingLight {}", light.fading_light)?;
669 writeln!(w, " flareradius {}", format_float(light.flare_radius))?;
670
671 let flare_count = light.flare_sizes.len();
673 if flare_count > 0 {
674 writeln!(w, " lensflares {flare_count}")?;
675 }
676
677 if !light.flare_texture_names.is_empty() {
678 writeln!(w, " texturenames {}", light.flare_texture_names.len())?;
679 for name in &light.flare_texture_names {
680 writeln!(w, " {name}")?;
681 }
682 }
683
684 if !light.flare_positions.is_empty() {
685 writeln!(w, " flarepositions {}", light.flare_positions.len())?;
686 for p in &light.flare_positions {
687 writeln!(w, " {}", format_float(*p))?;
688 }
689 }
690
691 if !light.flare_sizes.is_empty() {
692 writeln!(w, " flaresizes {}", light.flare_sizes.len())?;
693 for s in &light.flare_sizes {
694 writeln!(w, " {}", format_float(*s))?;
695 }
696 }
697
698 if !light.flare_color_shifts.is_empty() {
699 writeln!(w, " flarecolorshifts {}", light.flare_color_shifts.len())?;
700 for c in &light.flare_color_shifts {
701 writeln!(
702 w,
703 " {} {} {}",
704 format_float(c[0]),
705 format_float(c[1]),
706 format_float(c[2])
707 )?;
708 }
709 }
710
711 Ok(())
712}
713
714fn write_emitter_fields<W: Write>(
720 w: &mut W,
721 e: &super::types::MdlEmitter,
722) -> Result<(), MdlAsciiError> {
723 writeln!(w, " deadspace {}", format_float(e.deadspace))?;
724 writeln!(w, " blastRadius {}", format_float(e.blast_radius))?;
725 writeln!(w, " blastLength {}", format_float(e.blast_length))?;
726 writeln!(w, " numBranches {}", e.num_branches)?;
727 writeln!(w, " controlptsmoothing {}", e.control_pt_smoothing)?;
728 writeln!(w, " xgrid {}", e.x_grid)?;
729 writeln!(w, " ygrid {}", e.y_grid)?;
730 writeln!(w, " spawntype {}", e.spawn_type)?;
731
732 if !e.update.is_empty() {
733 writeln!(w, " update {}", e.update)?;
734 }
735 if !e.render.is_empty() {
736 writeln!(w, " render {}", e.render)?;
737 }
738 if !e.blend.is_empty() {
739 writeln!(w, " blend {}", e.blend)?;
740 }
741 if !e.texture.is_empty() {
742 writeln!(w, " texture {}", e.texture)?;
743 }
744 if !e.chunk_name.is_empty() {
745 writeln!(w, " chunkName {}", e.chunk_name)?;
746 }
747
748 writeln!(w, " twosidedtex {}", e.two_sided_tex)?;
749 writeln!(w, " loop {}", e.loop_emitter)?;
750 writeln!(w, " renderorder {}", e.render_order)?;
751 writeln!(w, " m_bFrameBlending {}", i32::from(e.frame_blending))?;
752
753 if !e.depth_texture_name.is_empty() {
754 writeln!(w, " m_sDepthTextureName {}", e.depth_texture_name)?;
755 }
756
757 Ok(())
758}
759
760fn write_reference_fields<W: Write>(
766 w: &mut W,
767 r: &super::types::MdlReference,
768) -> Result<(), MdlAsciiError> {
769 writeln!(w, " refModel {}", r.ref_model)?;
770 writeln!(w, " reattachable {}", r.reattachable)?;
771 Ok(())
772}
773
774fn write_controller<W: Write>(
780 w: &mut W,
781 ctrl: &MdlController,
782 ctx: super::ascii_names::NodeTypeContext,
783 indent: &str,
784) -> Result<(), MdlAsciiError> {
785 let name = controller_name(ctrl.controller_type, ctx);
786 let is_bezier = (ctrl.raw_column_count & CTRL_FLAG_BEZIER) != 0;
787 let is_orientation = ctrl.controller_type == MdlControllerType::ORIENTATION;
788 let is_compressed = is_orientation && ctrl.raw_column_count == 2;
789
790 if ctrl.keys.len() == 1 && ctrl.keys[0].time == 0.0 {
791 write!(w, "{indent}{name} ")?;
793 write_key_values(w, &ctrl.keys[0], is_orientation, is_compressed)?;
794 writeln!(w)?;
795 } else {
796 let suffix = if is_bezier { "bezierkey" } else { "key" };
798 writeln!(w, "{indent}{name}{suffix}")?;
799 for key in &ctrl.keys {
800 write!(w, "{indent} {} ", format_float(key.time))?;
801 write_key_values(w, key, is_orientation, is_compressed)?;
802 writeln!(w)?;
803 }
804 writeln!(w, "{indent}endlist")?;
805 }
806
807 Ok(())
808}
809
810fn write_key_values<W: Write>(
812 w: &mut W,
813 key: &MdlKey,
814 is_orientation: bool,
815 is_compressed: bool,
816) -> Result<(), MdlAsciiError> {
817 if is_orientation && !is_compressed && key.values.len() >= 4 {
818 let q = [key.values[3], key.values[0], key.values[1], key.values[2]];
821 let aa = quat_to_axis_angle(q);
822 write!(
823 w,
824 "{} {} {} {}",
825 format_float(aa[0]),
826 format_float(aa[1]),
827 format_float(aa[2]),
828 format_float(aa[3])
829 )?;
830 } else {
831 let formatted: Vec<String> = key.values.iter().map(|v| format_float(*v)).collect();
833 write!(w, "{}", formatted.join(" "))?;
834 }
835 Ok(())
836}
837
838fn controller_name(code: MdlControllerType, ctx: super::ascii_names::NodeTypeContext) -> String {
840 controller_ascii_name(code, ctx)
841 .map(String::from)
842 .unwrap_or_else(|| format!("controller_{}", code.raw()))
843}
844
845fn write_animation<W: Write>(
851 w: &mut W,
852 anim: &MdlAnimation,
853 model_name: &str,
854 geo_positions: &HashMap<&str, [f32; 3]>,
855) -> Result<(), MdlAsciiError> {
856 writeln!(w, "newanim {} {model_name}", anim.name)?;
857 writeln!(w, " length {}", format_float(anim.length))?;
858 writeln!(w, " transtime {}", format_float(anim.transition_time))?;
859 writeln!(w, " animroot {}", anim.anim_root)?;
860
861 for event in &anim.events {
863 writeln!(w, " event {} {}", format_float(event.time), event.name)?;
864 }
865
866 write_anim_node(w, &anim.root_node, None, geo_positions)?;
868
869 writeln!(w, "doneanim {} {model_name}", anim.name)?;
870 Ok(())
871}
872
873fn write_anim_node<W: Write>(
875 w: &mut W,
876 node: &MdlAnimNode,
877 parent_name: Option<&str>,
878 geo_positions: &HashMap<&str, [f32; 3]>,
879) -> Result<(), MdlAsciiError> {
880 let parent_str = parent_name.unwrap_or("NULL");
881 writeln!(w, " node dummy {}", node.name)?;
882 writeln!(w, " parent {parent_str}")?;
883
884 let geo_pos = geo_positions
888 .get(node.name.as_str())
889 .copied()
890 .unwrap_or([0.0, 0.0, 0.0]);
891
892 for ctrl in &node.controllers {
897 write_anim_controller(w, ctrl, geo_pos)?;
898 }
899
900 writeln!(w, " endnode")?;
901
902 for child in &node.children {
903 write_anim_node(w, child, Some(&node.name), geo_positions)?;
904 }
905
906 Ok(())
907}
908
909fn write_anim_controller<W: Write>(
914 w: &mut W,
915 ctrl: &MdlController,
916 geo_pos: [f32; 3],
917) -> Result<(), MdlAsciiError> {
918 let name = controller_ascii_name(
920 ctrl.controller_type,
921 super::ascii_names::NodeTypeContext::Base,
922 )
923 .or_else(|| {
924 controller_ascii_name(
925 ctrl.controller_type,
926 super::ascii_names::NodeTypeContext::Mesh,
927 )
928 })
929 .or_else(|| {
930 controller_ascii_name(
931 ctrl.controller_type,
932 super::ascii_names::NodeTypeContext::Light,
933 )
934 })
935 .or_else(|| {
936 controller_ascii_name(
937 ctrl.controller_type,
938 super::ascii_names::NodeTypeContext::Emitter,
939 )
940 })
941 .map(String::from)
942 .unwrap_or_else(|| format!("controller_{}", ctrl.controller_type.raw()));
943
944 let is_bezier = (ctrl.raw_column_count & CTRL_FLAG_BEZIER) != 0;
945 let is_orientation = ctrl.controller_type == MdlControllerType::ORIENTATION;
946 let is_position = ctrl.controller_type == MdlControllerType::POSITION;
947 let is_compressed = is_orientation && ctrl.raw_column_count == 2;
948 let suffix = if is_bezier { "bezierkey" } else { "key" };
949
950 writeln!(w, " {name}{suffix}")?;
951 for key in &ctrl.keys {
952 write!(w, " {} ", format_float(key.time))?;
953 if is_position && key.values.len() >= 3 {
954 let mut offset_key = key.clone();
958 offset_key.values[0] += geo_pos[0];
959 offset_key.values[1] += geo_pos[1];
960 offset_key.values[2] += geo_pos[2];
961 write_key_values(w, &offset_key, false, false)?;
962 } else {
963 write_key_values(w, key, is_orientation, is_compressed)?;
964 }
965 writeln!(w)?;
966 }
967 writeln!(w, " endlist")?;
968
969 Ok(())
970}
971
972fn format_float(v: f32) -> String {
983 if v.is_nan() {
984 return "0.0".into();
985 }
986 if v.is_infinite() {
987 return if v > 0.0 {
988 "3.4028235e+38".into()
989 } else {
990 "-3.4028235e+38".into()
991 };
992 }
993 if v == 0.0 {
994 return "0.0".into();
996 }
997
998 let abs = v.abs();
999
1000 if abs < 1e-4 && abs > 0.0 {
1002 let raw = format!("{v:e}");
1005 if let Some(e_pos) = raw.find('e') {
1006 let mantissa = &raw[..e_pos];
1007 let exp: i32 = raw[e_pos + 1..]
1008 .parse()
1009 .expect("Rust {:e} formatting always produces a valid exponent");
1010 let trimmed_m = trim_trailing_zeros_keep_one(mantissa);
1011 return format!("{trimmed_m}e{exp:+03}");
1012 }
1013 return raw;
1014 }
1015
1016 let s = v.to_string();
1017
1018 if let Some(e_pos) = s.find('e') {
1021 let mantissa = &s[..e_pos];
1022 let exp_str = &s[e_pos + 1..];
1023 let exp: i32 = exp_str
1024 .parse()
1025 .expect("Rust Display formatting always produces a valid exponent");
1026 let trimmed_m = trim_trailing_zeros_keep_one(mantissa);
1027 return format!("{trimmed_m}e{exp:+03}");
1028 }
1029
1030 trim_trailing_zeros_keep_one(&s)
1031}
1032
1033fn trim_trailing_zeros_keep_one(s: &str) -> String {
1036 if !s.contains('.') {
1037 return format!("{s}.0");
1039 }
1040 let trimmed = s.trim_end_matches('0');
1041 if trimmed.ends_with('.') {
1042 format!("{trimmed}0")
1044 } else {
1045 trimmed.into()
1046 }
1047}
1048
1049#[cfg(test)]
1050mod tests {
1051 use super::*;
1052
1053 #[test]
1054 fn format_float_integers() {
1055 assert_eq!(format_float(0.0), "0.0");
1056 assert_eq!(format_float(1.0), "1.0");
1057 assert_eq!(format_float(-1.0), "-1.0");
1058 assert_eq!(format_float(42.0), "42.0");
1059 }
1060
1061 #[test]
1062 fn format_float_negative_zero() {
1063 assert_eq!(format_float(-0.0), "0.0");
1064 }
1065
1066 #[test]
1067 fn format_float_decimals() {
1068 assert_eq!(format_float(0.5), "0.5");
1069 assert_eq!(format_float(1.5), "1.5");
1070 assert_eq!(format_float(-2.75), "-2.75");
1071 }
1072
1073 #[test]
1074 fn format_float_no_trailing_zeros() {
1075 let s = format_float(0.25);
1077 assert_eq!(s, "0.25");
1079 }
1080
1081 #[test]
1082 fn format_float_scientific_notation() {
1083 let v: f32 = 7.84e-06;
1085 let s = format_float(v);
1086 assert!(s.contains("e-"), "expected scientific notation: {s}");
1087 }
1088
1089 #[test]
1090 fn minimal_model_roundtrip() {
1091 let mdl = Mdl {
1093 root_node: MdlNode {
1094 name: "test_model".into(),
1095 parent_index: None,
1096 position: [0.0, 0.0, 0.0],
1097 rotation: [1.0, 0.0, 0.0, 0.0],
1098 node_data: MdlNodeData::Base,
1099 controllers: Vec::new(),
1100 children: Vec::new(),
1101 orphan_controller_data: Vec::new(),
1102 header_padding_02: [0, 0],
1103 header_padding_06: [0, 0],
1104 },
1105 geometry_fn_ptr1: 0,
1106 geometry_fn_ptr2: 0,
1107 model_type: 0,
1108 classification: 0,
1109 subclassification: 0,
1110 affected_by_fog: 1,
1111 supermodel_name: String::new(),
1112 node_count: 1,
1113 bounding_box: [0.0; 6],
1114 radius: 0.0,
1115 animation_scale: 1.0,
1116 animations: Vec::new(),
1117 anim_root_node: None,
1118 };
1119
1120 let ascii = write_mdl_ascii_to_string(&mdl).unwrap();
1121 assert!(ascii.contains("newmodel test_model"));
1122 assert!(ascii.contains("setsupermodel test_model NULL"));
1123 assert!(ascii.contains("classification other"));
1124 assert!(ascii.contains("node dummy test_model"));
1125 assert!(ascii.contains("parent NULL"));
1126 assert!(!ascii.contains("orientation 0.0 0.0 0.0 0.0"));
1128 assert!(!ascii.contains("position 0.0 0.0 0.0"));
1129 assert!(ascii.contains("endnode"));
1130 assert!(ascii.contains("endmodelgeom test_model"));
1131 assert!(ascii.contains("donemodel test_model"));
1132 }
1133
1134 fn k1_override_dir() -> Option<String> {
1137 std::env::var("KOTOR_GAME_DIR")
1138 .ok()
1139 .map(|d| format!("{d}/Override"))
1140 }
1141
1142 #[test]
1143 fn smoke_test_vanilla_model() {
1144 let base = match k1_override_dir() {
1147 Some(d) => d,
1148 None => return,
1149 };
1150 let path = format!("{base}/3dgui.mdl");
1151 let data = match std::fs::read(&path) {
1152 Ok(d) => d,
1153 Err(_) => return,
1154 };
1155 let mdl = super::super::reader::read_mdl_from_bytes(&data, None).unwrap();
1156 let ascii = write_mdl_ascii_to_string(&mdl).unwrap();
1157
1158 assert!(ascii.starts_with("newmodel "));
1160 assert!(ascii.contains("beginmodelgeom"));
1161 assert!(ascii.contains("endmodelgeom"));
1162 assert!(ascii.contains("donemodel"));
1163
1164 let node_count =
1166 ascii.matches("\nnode ").count() + if ascii.starts_with("node ") { 1 } else { 0 };
1167 assert!(node_count > 0, "expected at least one node");
1168
1169 eprintln!(
1170 "3dgui.mdl: {} bytes ASCII, {} nodes",
1171 ascii.len(),
1172 node_count
1173 );
1174 }
1175
1176 #[test]
1177 fn smoke_test_character_model() {
1178 let base = match k1_override_dir() {
1180 Some(d) => d,
1181 None => return,
1182 };
1183 let mdl_path = format!("{base}/p_bastilabb.mdl");
1184 let mdx_path = format!("{base}/p_bastilabb.mdx");
1185 let mdl_data = match std::fs::read(&mdl_path) {
1186 Ok(d) => d,
1187 Err(_) => return,
1188 };
1189 let mdx_data = std::fs::read(&mdx_path).ok();
1190 let mdl =
1191 super::super::reader::read_mdl_from_bytes(&mdl_data, mdx_data.as_deref()).unwrap();
1192 let ascii = write_mdl_ascii_to_string(&mdl).unwrap();
1193
1194 assert!(ascii.contains("newmodel"));
1195 assert!(ascii.contains("donemodel"));
1196
1197 let has_skin = ascii.contains("node skin ");
1199 let has_weights = ascii.contains("weights ");
1200 let has_anim = ascii.contains("newanim ");
1201
1202 eprintln!(
1203 "p_bastilabb.mdl: {} bytes ASCII, skin={has_skin}, weights={has_weights}, anims={has_anim}",
1204 ascii.len()
1205 );
1206 }
1207
1208 #[test]
1209 fn smoke_test_animated_model() {
1210 let base = match k1_override_dir() {
1212 Some(d) => d,
1213 None => return,
1214 };
1215 let mdl_path = format!("{base}/s_female03.mdl");
1216 let mdx_path = format!("{base}/s_female03.mdx");
1217 let mdl_data = match std::fs::read(&mdl_path) {
1218 Ok(d) => d,
1219 Err(_) => return,
1220 };
1221 let mdx_data = std::fs::read(&mdx_path).ok();
1222 let mdl =
1223 super::super::reader::read_mdl_from_bytes(&mdl_data, mdx_data.as_deref()).unwrap();
1224 let ascii = write_mdl_ascii_to_string(&mdl).unwrap();
1225
1226 assert!(ascii.contains("newmodel"));
1227 assert!(ascii.contains("donemodel"));
1228 assert!(ascii.contains("newanim "));
1229 assert!(ascii.contains("doneanim "));
1230
1231 let anim_count = ascii.matches("newanim ").count();
1232 eprintln!(
1233 "s_female03.mdl: {} bytes ASCII, {} animations",
1234 ascii.len(),
1235 anim_count
1236 );
1237 }
1238
1239 #[test]
1240 fn smoke_test_effect_model() {
1241 let base = match k1_override_dir() {
1243 Some(d) => d,
1244 None => return,
1245 };
1246 let mdl_path = format!("{base}/fx_carbref.mdl");
1247 let mdl_data = match std::fs::read(&mdl_path) {
1248 Ok(d) => d,
1249 Err(_) => return,
1250 };
1251 let mdl = super::super::reader::read_mdl_from_bytes(&mdl_data, None).unwrap();
1252 let ascii = write_mdl_ascii_to_string(&mdl).unwrap();
1253
1254 assert!(ascii.contains("newmodel"));
1255 assert!(ascii.contains("donemodel"));
1256
1257 let has_emitter = ascii.contains("node emitter ");
1258 let has_light = ascii.contains("node light ");
1259 eprintln!(
1260 "fx_carbref.mdl: {} bytes ASCII, emitter={has_emitter}, light={has_light}",
1261 ascii.len()
1262 );
1263
1264 if let Some(pos) = ascii.find("node emitter ") {
1266 let snippet = &ascii[pos..ascii.len().min(pos + 800)];
1267 for line in snippet.lines().take(30) {
1268 eprintln!(" {line}");
1269 }
1270 }
1271 }
1272}