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(
70 feature = "tracing",
71 tracing::instrument(level = "debug", skip(w, mdl))
72)]
73pub fn write_mdl_ascii<W: Write>(w: &mut W, mdl: &Mdl) -> Result<(), MdlAsciiError> {
74 let name = &mdl.root_node.name;
75 let super_name = if mdl.supermodel_name.is_empty() {
76 "NULL"
77 } else {
78 &mdl.supermodel_name
79 };
80
81 writeln!(w, "newmodel {name}")?;
83 writeln!(w, "setsupermodel {name} {super_name}")?;
84 writeln!(
85 w,
86 "classification {}",
87 classification_to_ascii(mdl.classification)
88 )?;
89 writeln!(w, "classification_unk1 {}", mdl.subclassification)?;
90 let ignore_fog = if mdl.affected_by_fog != 0 { 0 } else { 1 };
91 writeln!(w, "ignorefog {ignore_fog}")?;
92 writeln!(w, "setanimationscale {}", format_float(mdl.animation_scale))?;
93
94 let has_compressed_quats = has_compressed_quaternions(mdl);
97 writeln!(
98 w,
99 "compress_quaternions {}",
100 i32::from(has_compressed_quats)
101 )?;
102
103 let is_headlinked = mdl.anim_root_node.is_some();
106 writeln!(w, "headlink {}", i32::from(is_headlinked))?;
107
108 writeln!(w, "beginmodelgeom {name}")?;
110 writeln!(
111 w,
112 " bmin {} {} {}",
113 format_float(mdl.bounding_box[0]),
114 format_float(mdl.bounding_box[1]),
115 format_float(mdl.bounding_box[2])
116 )?;
117 writeln!(
118 w,
119 " bmax {} {} {}",
120 format_float(mdl.bounding_box[3]),
121 format_float(mdl.bounding_box[4]),
122 format_float(mdl.bounding_box[5])
123 )?;
124 writeln!(w, " radius {}", format_float(mdl.radius))?;
125
126 write_node(w, &mdl.root_node, None, &mdl.root_node)?;
128
129 writeln!(w, "endmodelgeom {name}")?;
130
131 let geo_positions = collect_geo_positions(&mdl.root_node);
136
137 for anim in &mdl.animations {
139 write_animation(w, anim, name, &geo_positions)?;
140 }
141
142 writeln!(w, "donemodel {name}")?;
143 Ok(())
144}
145
146#[cfg_attr(feature = "tracing", tracing::instrument(level = "debug", skip(mdl)))]
148pub fn write_mdl_ascii_to_string(mdl: &Mdl) -> Result<String, MdlAsciiError> {
149 let mut buf = Vec::new();
150 write_mdl_ascii(&mut buf, mdl)?;
151 String::from_utf8(buf)
152 .map_err(|e| MdlAsciiError::InvalidData(format!("output is not valid UTF-8: {e}")))
153}
154
155fn write_node<W: Write>(
161 w: &mut W,
162 node: &MdlNode,
163 parent_name: Option<&str>,
164 model_root: &MdlNode,
165) -> Result<(), MdlAsciiError> {
166 let type_str = node_type_ascii_name(&node.node_data);
167 let parent_str = parent_name.unwrap_or("NULL");
168
169 writeln!(w, "node {type_str} {}", node.name)?;
170 writeln!(w, " parent {parent_str}")?;
171
172 let has_pos_ctrl = node
175 .controllers
176 .iter()
177 .any(|c| c.controller_type == MdlControllerType::POSITION);
178 let has_ori_ctrl = node
179 .controllers
180 .iter()
181 .any(|c| c.controller_type == MdlControllerType::ORIENTATION);
182
183 if !has_pos_ctrl {
186 let [px, py, pz] = node.position;
187 if px != 0.0 || py != 0.0 || pz != 0.0 {
188 writeln!(
189 w,
190 " position {} {} {}",
191 format_float(px),
192 format_float(py),
193 format_float(pz)
194 )?;
195 }
196 }
197
198 if !has_ori_ctrl {
201 let aa = quat_to_axis_angle(node.rotation);
202 if aa[3] != 0.0 {
203 writeln!(
204 w,
205 " orientation {} {} {} {}",
206 format_float(aa[0]),
207 format_float(aa[1]),
208 format_float(aa[2]),
209 format_float(aa[3])
210 )?;
211 }
212 }
213
214 let ctx = node_type_context(&node.node_data);
218 for ctrl in &node.controllers {
219 write_controller(w, ctrl, ctx, " ")?;
220 }
221
222 write_node_data(w, &node.node_data, model_root)?;
224
225 writeln!(w, "endnode")?;
226
227 for child in &node.children {
229 write_node(w, child, Some(&node.name), model_root)?;
230 }
231
232 Ok(())
233}
234
235fn write_node_data<W: Write>(
237 w: &mut W,
238 data: &MdlNodeData,
239 model_root: &MdlNode,
240) -> Result<(), MdlAsciiError> {
241 match data {
242 MdlNodeData::Base | MdlNodeData::Camera(_) => {}
243 MdlNodeData::Mesh(mesh) => write_mesh_fields(w, mesh)?,
244 MdlNodeData::Skin(skin) => {
245 write_mesh_fields(w, &skin.mesh)?;
246 write_skin_fields(w, skin, model_root)?;
247 }
248 MdlNodeData::AnimMesh(am) => {
249 write_mesh_fields(w, &am.mesh)?;
250 write_animmesh_fields(w, am)?;
251 }
252 MdlNodeData::Dangly(dangly) => {
253 write_mesh_fields(w, &dangly.mesh)?;
254 write_dangly_fields(w, dangly)?;
255 }
256 MdlNodeData::Aabb(aabb) => {
257 write_mesh_fields(w, &aabb.mesh)?;
258 write_aabb_fields(w, aabb)?;
259 }
260 MdlNodeData::Saber(saber) => {
261 write_mesh_fields(w, &saber.mesh)?;
262 }
263 MdlNodeData::Light(light) => write_light_fields(w, light)?,
264 MdlNodeData::Emitter(emitter) => write_emitter_fields(w, emitter)?,
265 MdlNodeData::Reference(reference) => write_reference_fields(w, reference)?,
266 }
267 Ok(())
268}
269
270fn write_mesh_fields<W: Write>(
278 w: &mut W,
279 mesh: &super::types::MdlMesh,
280) -> Result<(), MdlAsciiError> {
281 writeln!(
283 w,
284 " diffuse {} {} {}",
285 format_float(mesh.diffuse_color[0]),
286 format_float(mesh.diffuse_color[1]),
287 format_float(mesh.diffuse_color[2])
288 )?;
289 writeln!(
290 w,
291 " ambient {} {} {}",
292 format_float(mesh.ambient_color[0]),
293 format_float(mesh.ambient_color[1]),
294 format_float(mesh.ambient_color[2])
295 )?;
296 writeln!(w, " transparencyhint {}", mesh.transparency_hint)?;
297
298 writeln!(w, " animateuv {}", mesh.animate_uv)?;
300 writeln!(w, " uvdirectionx {}", format_float(mesh.uv_direction_x))?;
301 writeln!(w, " uvdirectiony {}", format_float(mesh.uv_direction_y))?;
302 writeln!(w, " uvjitter {}", format_float(mesh.uv_jitter))?;
303 writeln!(w, " uvjitterspeed {}", format_float(mesh.uv_jitter_speed))?;
304
305 writeln!(w, " lightmapped {}", i32::from(mesh.light_mapped))?;
307 writeln!(w, " rotatetexture {}", i32::from(mesh.rotate_texture))?;
308 writeln!(
309 w,
310 " m_bIsBackgroundGeometry {}",
311 i32::from(mesh.is_background_geometry)
312 )?;
313 writeln!(w, " shadow {}", i32::from(mesh.shadow))?;
314 writeln!(w, " beaming {}", i32::from(mesh.beaming))?;
315 writeln!(w, " render {}", i32::from(mesh.render))?;
316
317 writeln!(w, " dirt_enabled 0")?;
319 writeln!(w, " dirt_texture 1")?;
320 writeln!(w, " dirt_worldspace 1")?;
321 writeln!(w, " hologram_donotdraw 0")?;
322
323 let has_tangent = !mesh.tangent_space.is_empty();
325 writeln!(w, " tangentspace {}", i32::from(has_tangent))?;
326
327 writeln!(w, " inv_count {}", mesh.inverted_counter)?;
329
330 if mesh.texture_0.is_empty() {
332 writeln!(w, " bitmap NULL")?;
333 } else {
334 writeln!(w, " bitmap {}", mesh.texture_0)?;
335 }
336 if !mesh.texture_1.is_empty() {
337 writeln!(w, " bitmap2 {}", mesh.texture_1)?;
338 }
339
340 let vert_count = mesh.positions.len();
342 if vert_count > 0 {
343 writeln!(w, " verts {vert_count}")?;
344 for pos in &mesh.positions {
345 writeln!(
346 w,
347 " {} {} {}",
348 format_float(pos[0]),
349 format_float(pos[1]),
350 format_float(pos[2])
351 )?;
352 }
353 }
354
355 if !mesh.faces.is_empty() {
357 let has_uvs = !mesh.uv1.is_empty();
358 writeln!(w, " faces {}", mesh.faces.len())?;
359 for face in &mesh.faces {
360 let [v0, v1, v2] = face.vertex_indices;
361 let (tv0, tv1, tv2) = if has_uvs { (v0, v1, v2) } else { (0, 0, 0) };
365 writeln!(
366 w,
367 " {v0} {v1} {v2} 1 {tv0} {tv1} {tv2} {}",
368 face.surface_id
369 )?;
370 }
371 }
372
373 write_tverts(w, &mesh.uv1, "tverts")?;
375 write_tverts(w, &mesh.uv2, "tverts1")?;
376 write_tverts(w, &mesh.uv3, "tverts2")?;
377 write_tverts(w, &mesh.uv4, "tverts3")?;
378
379 if !mesh.vertex_colors.is_empty() {
381 writeln!(w, " colors {}", mesh.vertex_colors.len())?;
382 for c in &mesh.vertex_colors {
383 writeln!(
384 w,
385 " {} {} {}",
386 format_float(f32::from(c[0]) / 255.0),
387 format_float(f32::from(c[1]) / 255.0),
388 format_float(f32::from(c[2]) / 255.0)
389 )?;
390 }
391 }
392
393 Ok(())
394}
395
396fn write_tverts<W: Write>(w: &mut W, uvs: &[[f32; 2]], keyword: &str) -> Result<(), MdlAsciiError> {
398 if !uvs.is_empty() {
399 writeln!(w, " {keyword} {}", uvs.len())?;
400 for uv in uvs {
401 writeln!(w, " {} {}", format_float(uv[0]), format_float(uv[1]))?;
402 }
403 }
404 Ok(())
405}
406
407fn write_skin_fields<W: Write>(
413 w: &mut W,
414 skin: &super::types::MdlSkin,
415 model_root: &MdlNode,
416) -> Result<(), MdlAsciiError> {
417 let vert_count = skin.mesh.positions.len();
418 if vert_count == 0 || skin.bone_weights.is_empty() || skin.bone_indices.is_empty() {
419 return Ok(());
420 }
421
422 let node_names = collect_node_names(model_root);
429 let mut bone_index_to_name: Vec<Option<String>> = Vec::new();
430 for (tree_pos, &raw) in skin.bonemap.iter().enumerate() {
431 let mdx_bone_idx_f = f32::from_bits(raw);
432 if mdx_bone_idx_f >= 0.0 {
433 #[allow(
436 clippy::cast_possible_truncation,
437 clippy::cast_sign_loss,
438 clippy::as_conversions
439 )]
440 let mdx_bone_idx = mdx_bone_idx_f as usize;
441 if mdx_bone_idx >= bone_index_to_name.len() {
442 bone_index_to_name.resize(mdx_bone_idx + 1, None);
443 }
444 bone_index_to_name[mdx_bone_idx] = node_names.get(tree_pos).cloned();
445 }
446 }
447
448 writeln!(w, " weights {vert_count}")?;
449 for (weights, indices) in skin.bone_weights.iter().zip(&skin.bone_indices) {
450 let mut parts = Vec::new();
451 for j in 0..4 {
452 let weight = weights[j];
453 let bone_idx_f = indices[j];
454
455 if weight > 0.0 && bone_idx_f >= 0.0 {
456 #[allow(
459 clippy::cast_possible_truncation,
460 clippy::cast_sign_loss,
461 clippy::as_conversions
462 )]
463 let bone_idx = bone_idx_f as usize;
464 let bone_name = bone_index_to_name
465 .get(bone_idx)
466 .and_then(|opt| opt.as_ref())
467 .cloned()
468 .unwrap_or_else(|| format!("bone_{bone_idx}"));
469 parts.push(format!("{bone_name} {}", format_float(weight)));
470 }
471 }
472
473 if parts.is_empty() {
474 writeln!(w, " ")?;
475 } else {
476 writeln!(w, " {}", parts.join(" "))?;
477 }
478 }
479
480 Ok(())
481}
482
483fn has_compressed_quaternions(mdl: &Mdl) -> bool {
486 if node_has_compressed_quats(&mdl.root_node) {
488 return true;
489 }
490 for anim in &mdl.animations {
492 if anim_node_has_compressed_quats(&anim.root_node) {
493 return true;
494 }
495 }
496 false
497}
498
499fn node_has_compressed_quats(node: &MdlNode) -> bool {
500 for ctrl in &node.controllers {
501 if ctrl.controller_type == MdlControllerType::ORIENTATION && ctrl.raw_column_count == 2 {
502 return true;
503 }
504 }
505 node.children.iter().any(node_has_compressed_quats)
506}
507
508fn anim_node_has_compressed_quats(node: &MdlAnimNode) -> bool {
509 for ctrl in &node.controllers {
510 if ctrl.controller_type == MdlControllerType::ORIENTATION && ctrl.raw_column_count == 2 {
511 return true;
512 }
513 }
514 node.children.iter().any(anim_node_has_compressed_quats)
515}
516
517fn collect_node_names(node: &MdlNode) -> Vec<String> {
519 let mut names = Vec::new();
520 collect_names_recursive(node, &mut names);
521 names
522}
523
524fn collect_names_recursive(node: &MdlNode, names: &mut Vec<String>) {
525 names.push(node.name.clone());
526 for child in &node.children {
527 collect_names_recursive(child, names);
528 }
529}
530
531fn write_animmesh_fields<W: Write>(
537 w: &mut W,
538 am: &super::types::MdlAnimMesh,
539) -> Result<(), MdlAsciiError> {
540 writeln!(w, " sampleperiod {}", format_float(am.sample_period))?;
541
542 if !am.anim_verts.is_empty() {
543 writeln!(w, " animverts {}", am.anim_verts.len())?;
544 for v in &am.anim_verts {
545 writeln!(
546 w,
547 " {} {} {}",
548 format_float(v[0]),
549 format_float(v[1]),
550 format_float(v[2])
551 )?;
552 }
553 }
554
555 if !am.anim_t_verts.is_empty() {
556 writeln!(w, " animtverts {}", am.anim_t_verts.len())?;
557 for v in &am.anim_t_verts {
558 writeln!(
559 w,
560 " {} {} {}",
561 format_float(v[0]),
562 format_float(v[1]),
563 format_float(v[2])
564 )?;
565 }
566 }
567
568 Ok(())
569}
570
571fn write_dangly_fields<W: Write>(
577 w: &mut W,
578 dangly: &super::types::MdlDangly,
579) -> Result<(), MdlAsciiError> {
580 writeln!(w, " displacement {}", format_float(dangly.displacement))?;
581 writeln!(w, " tightness {}", format_float(dangly.tightness))?;
582 writeln!(w, " period {}", format_float(dangly.period))?;
583
584 if !dangly.constraints.is_empty() {
585 writeln!(w, " constraints {}", dangly.constraints.len())?;
586 for c in &dangly.constraints {
587 writeln!(w, " {}", format_float(*c))?;
588 }
589 }
590
591 Ok(())
592}
593
594fn write_aabb_fields<W: Write>(
600 w: &mut W,
601 aabb: &super::types::MdlAabb,
602) -> Result<(), MdlAsciiError> {
603 if let Some(tree) = &aabb.aabb_tree {
604 let mut leaves = Vec::new();
606 collect_aabb_leaves(tree, &mut leaves);
607
608 if !leaves.is_empty() {
609 writeln!(w, " aabb")?;
610 for leaf in &leaves {
611 writeln!(
612 w,
613 " {} {} {} {} {} {} {}",
614 format_float(leaf.box_min[0]),
615 format_float(leaf.box_min[1]),
616 format_float(leaf.box_min[2]),
617 format_float(leaf.box_max[0]),
618 format_float(leaf.box_max[1]),
619 format_float(leaf.box_max[2]),
620 leaf.face_index
621 )?;
622 }
623 }
624 }
625 Ok(())
626}
627
628fn collect_aabb_leaves<'a>(node: &'a AabbNode, leaves: &mut Vec<&'a AabbNode>) {
630 if node.face_index >= 0 {
631 leaves.push(node);
633 }
634 if let Some(left) = &node.left {
635 collect_aabb_leaves(left, leaves);
636 }
637 if let Some(right) = &node.right {
638 collect_aabb_leaves(right, leaves);
639 }
640}
641
642fn write_light_fields<W: Write>(
648 w: &mut W,
649 light: &super::types::MdlLight,
650) -> Result<(), MdlAsciiError> {
651 writeln!(w, " lightpriority {}", light.priority)?;
652 writeln!(w, " ambientonly {}", light.ambientonly)?;
653 writeln!(w, " ndynamictype {}", light.num_dynamic_types)?;
654 writeln!(w, " affectdynamic {}", light.affectdynamic)?;
655 writeln!(w, " shadow {}", light.shadow)?;
656 writeln!(w, " generateflare {}", light.generateflare)?;
657 writeln!(w, " fadingLight {}", light.fading_light)?;
658 writeln!(w, " flareradius {}", format_float(light.flare_radius))?;
659
660 let flare_count = light.flare_sizes.len();
662 if flare_count > 0 {
663 writeln!(w, " lensflares {flare_count}")?;
664 }
665
666 if !light.flare_texture_names.is_empty() {
667 writeln!(w, " texturenames {}", light.flare_texture_names.len())?;
668 for name in &light.flare_texture_names {
669 writeln!(w, " {name}")?;
670 }
671 }
672
673 if !light.flare_positions.is_empty() {
674 writeln!(w, " flarepositions {}", light.flare_positions.len())?;
675 for p in &light.flare_positions {
676 writeln!(w, " {}", format_float(*p))?;
677 }
678 }
679
680 if !light.flare_sizes.is_empty() {
681 writeln!(w, " flaresizes {}", light.flare_sizes.len())?;
682 for s in &light.flare_sizes {
683 writeln!(w, " {}", format_float(*s))?;
684 }
685 }
686
687 if !light.flare_color_shifts.is_empty() {
688 writeln!(w, " flarecolorshifts {}", light.flare_color_shifts.len())?;
689 for c in &light.flare_color_shifts {
690 writeln!(
691 w,
692 " {} {} {}",
693 format_float(c[0]),
694 format_float(c[1]),
695 format_float(c[2])
696 )?;
697 }
698 }
699
700 Ok(())
701}
702
703fn write_emitter_fields<W: Write>(
709 w: &mut W,
710 e: &super::types::MdlEmitter,
711) -> Result<(), MdlAsciiError> {
712 writeln!(w, " deadspace {}", format_float(e.deadspace))?;
713 writeln!(w, " blastRadius {}", format_float(e.blast_radius))?;
714 writeln!(w, " blastLength {}", format_float(e.blast_length))?;
715 writeln!(w, " numBranches {}", e.num_branches)?;
716 writeln!(w, " controlptsmoothing {}", e.control_pt_smoothing)?;
717 writeln!(w, " xgrid {}", e.x_grid)?;
718 writeln!(w, " ygrid {}", e.y_grid)?;
719 writeln!(w, " spawntype {}", e.spawn_type)?;
720
721 if !e.update.is_empty() {
722 writeln!(w, " update {}", e.update)?;
723 }
724 if !e.render.is_empty() {
725 writeln!(w, " render {}", e.render)?;
726 }
727 if !e.blend.is_empty() {
728 writeln!(w, " blend {}", e.blend)?;
729 }
730 if !e.texture.is_empty() {
731 writeln!(w, " texture {}", e.texture)?;
732 }
733 if !e.chunk_name.is_empty() {
734 writeln!(w, " chunkName {}", e.chunk_name)?;
735 }
736
737 writeln!(w, " twosidedtex {}", e.two_sided_tex)?;
738 writeln!(w, " loop {}", e.loop_emitter)?;
739 writeln!(w, " renderorder {}", e.render_order)?;
740 writeln!(w, " m_bFrameBlending {}", i32::from(e.frame_blending))?;
741
742 if !e.depth_texture_name.is_empty() {
743 writeln!(w, " m_sDepthTextureName {}", e.depth_texture_name)?;
744 }
745
746 Ok(())
747}
748
749fn write_reference_fields<W: Write>(
755 w: &mut W,
756 r: &super::types::MdlReference,
757) -> Result<(), MdlAsciiError> {
758 writeln!(w, " refModel {}", r.ref_model)?;
759 writeln!(w, " reattachable {}", r.reattachable)?;
760 Ok(())
761}
762
763fn write_controller<W: Write>(
769 w: &mut W,
770 ctrl: &MdlController,
771 ctx: super::ascii_names::NodeTypeContext,
772 indent: &str,
773) -> Result<(), MdlAsciiError> {
774 let name = controller_name(ctrl.controller_type, ctx);
775 let is_bezier = (ctrl.raw_column_count & CTRL_FLAG_BEZIER) != 0;
776 let is_orientation = ctrl.controller_type == MdlControllerType::ORIENTATION;
777 let is_compressed = is_orientation && ctrl.raw_column_count == 2;
778
779 if ctrl.keys.len() == 1 && ctrl.keys[0].time == 0.0 {
780 write!(w, "{indent}{name} ")?;
782 write_key_values(w, &ctrl.keys[0], is_orientation, is_compressed)?;
783 writeln!(w)?;
784 } else {
785 let suffix = if is_bezier { "bezierkey" } else { "key" };
787 writeln!(w, "{indent}{name}{suffix}")?;
788 for key in &ctrl.keys {
789 write!(w, "{indent} {} ", format_float(key.time))?;
790 write_key_values(w, key, is_orientation, is_compressed)?;
791 writeln!(w)?;
792 }
793 writeln!(w, "{indent}endlist")?;
794 }
795
796 Ok(())
797}
798
799fn write_key_values<W: Write>(
801 w: &mut W,
802 key: &MdlKey,
803 is_orientation: bool,
804 is_compressed: bool,
805) -> Result<(), MdlAsciiError> {
806 if is_orientation && !is_compressed && key.values.len() >= 4 {
807 let q = [key.values[3], key.values[0], key.values[1], key.values[2]];
810 let aa = quat_to_axis_angle(q);
811 write!(
812 w,
813 "{} {} {} {}",
814 format_float(aa[0]),
815 format_float(aa[1]),
816 format_float(aa[2]),
817 format_float(aa[3])
818 )?;
819 } else {
820 let formatted: Vec<String> = key.values.iter().map(|v| format_float(*v)).collect();
822 write!(w, "{}", formatted.join(" "))?;
823 }
824 Ok(())
825}
826
827fn controller_name(code: MdlControllerType, ctx: super::ascii_names::NodeTypeContext) -> String {
829 controller_ascii_name(code, ctx)
830 .map(String::from)
831 .unwrap_or_else(|| format!("controller_{}", code.raw()))
832}
833
834fn write_animation<W: Write>(
840 w: &mut W,
841 anim: &MdlAnimation,
842 model_name: &str,
843 geo_positions: &HashMap<&str, [f32; 3]>,
844) -> Result<(), MdlAsciiError> {
845 writeln!(w, "newanim {} {model_name}", anim.name)?;
846 writeln!(w, " length {}", format_float(anim.length))?;
847 writeln!(w, " transtime {}", format_float(anim.transition_time))?;
848 writeln!(w, " animroot {}", anim.anim_root)?;
849
850 for event in &anim.events {
852 writeln!(w, " event {} {}", format_float(event.time), event.name)?;
853 }
854
855 write_anim_node(w, &anim.root_node, None, geo_positions)?;
857
858 writeln!(w, "doneanim {} {model_name}", anim.name)?;
859 Ok(())
860}
861
862fn write_anim_node<W: Write>(
864 w: &mut W,
865 node: &MdlAnimNode,
866 parent_name: Option<&str>,
867 geo_positions: &HashMap<&str, [f32; 3]>,
868) -> Result<(), MdlAsciiError> {
869 let parent_str = parent_name.unwrap_or("NULL");
870 writeln!(w, " node dummy {}", node.name)?;
871 writeln!(w, " parent {parent_str}")?;
872
873 let geo_pos = geo_positions
877 .get(node.name.as_str())
878 .copied()
879 .unwrap_or([0.0, 0.0, 0.0]);
880
881 for ctrl in &node.controllers {
886 write_anim_controller(w, ctrl, geo_pos)?;
887 }
888
889 writeln!(w, " endnode")?;
890
891 for child in &node.children {
892 write_anim_node(w, child, Some(&node.name), geo_positions)?;
893 }
894
895 Ok(())
896}
897
898fn write_anim_controller<W: Write>(
903 w: &mut W,
904 ctrl: &MdlController,
905 geo_pos: [f32; 3],
906) -> Result<(), MdlAsciiError> {
907 let name = controller_ascii_name(
909 ctrl.controller_type,
910 super::ascii_names::NodeTypeContext::Base,
911 )
912 .or_else(|| {
913 controller_ascii_name(
914 ctrl.controller_type,
915 super::ascii_names::NodeTypeContext::Mesh,
916 )
917 })
918 .or_else(|| {
919 controller_ascii_name(
920 ctrl.controller_type,
921 super::ascii_names::NodeTypeContext::Light,
922 )
923 })
924 .or_else(|| {
925 controller_ascii_name(
926 ctrl.controller_type,
927 super::ascii_names::NodeTypeContext::Emitter,
928 )
929 })
930 .map(String::from)
931 .unwrap_or_else(|| format!("controller_{}", ctrl.controller_type.raw()));
932
933 let is_bezier = (ctrl.raw_column_count & CTRL_FLAG_BEZIER) != 0;
934 let is_orientation = ctrl.controller_type == MdlControllerType::ORIENTATION;
935 let is_position = ctrl.controller_type == MdlControllerType::POSITION;
936 let is_compressed = is_orientation && ctrl.raw_column_count == 2;
937 let suffix = if is_bezier { "bezierkey" } else { "key" };
938
939 writeln!(w, " {name}{suffix}")?;
940 for key in &ctrl.keys {
941 write!(w, " {} ", format_float(key.time))?;
942 if is_position && key.values.len() >= 3 {
943 let mut offset_key = key.clone();
947 offset_key.values[0] += geo_pos[0];
948 offset_key.values[1] += geo_pos[1];
949 offset_key.values[2] += geo_pos[2];
950 write_key_values(w, &offset_key, false, false)?;
951 } else {
952 write_key_values(w, key, is_orientation, is_compressed)?;
953 }
954 writeln!(w)?;
955 }
956 writeln!(w, " endlist")?;
957
958 Ok(())
959}
960
961fn format_float(v: f32) -> String {
972 if v.is_nan() {
973 return "0.0".into();
974 }
975 if v.is_infinite() {
976 return if v > 0.0 {
977 "3.4028235e+38".into()
978 } else {
979 "-3.4028235e+38".into()
980 };
981 }
982 if v == 0.0 {
983 return "0.0".into();
985 }
986
987 let abs = v.abs();
988
989 if abs < 1e-4 && abs > 0.0 {
991 let raw = format!("{v:e}");
994 if let Some(e_pos) = raw.find('e') {
995 let mantissa = &raw[..e_pos];
996 let exp: i32 = raw[e_pos + 1..]
997 .parse()
998 .expect("Rust {:e} formatting always produces a valid exponent");
999 let trimmed_m = trim_trailing_zeros_keep_one(mantissa);
1000 return format!("{trimmed_m}e{exp:+03}");
1001 }
1002 return raw;
1003 }
1004
1005 let s = v.to_string();
1006
1007 if let Some(e_pos) = s.find('e') {
1010 let mantissa = &s[..e_pos];
1011 let exp_str = &s[e_pos + 1..];
1012 let exp: i32 = exp_str
1013 .parse()
1014 .expect("Rust Display formatting always produces a valid exponent");
1015 let trimmed_m = trim_trailing_zeros_keep_one(mantissa);
1016 return format!("{trimmed_m}e{exp:+03}");
1017 }
1018
1019 trim_trailing_zeros_keep_one(&s)
1020}
1021
1022fn trim_trailing_zeros_keep_one(s: &str) -> String {
1025 if !s.contains('.') {
1026 return format!("{s}.0");
1028 }
1029 let trimmed = s.trim_end_matches('0');
1030 if trimmed.ends_with('.') {
1031 format!("{trimmed}0")
1033 } else {
1034 trimmed.into()
1035 }
1036}
1037
1038#[cfg(test)]
1039mod tests {
1040 use super::*;
1041
1042 #[test]
1043 fn format_float_integers() {
1044 assert_eq!(format_float(0.0), "0.0");
1045 assert_eq!(format_float(1.0), "1.0");
1046 assert_eq!(format_float(-1.0), "-1.0");
1047 assert_eq!(format_float(42.0), "42.0");
1048 }
1049
1050 #[test]
1051 fn format_float_negative_zero() {
1052 assert_eq!(format_float(-0.0), "0.0");
1053 }
1054
1055 #[test]
1056 fn format_float_decimals() {
1057 assert_eq!(format_float(0.5), "0.5");
1058 assert_eq!(format_float(1.5), "1.5");
1059 assert_eq!(format_float(-2.75), "-2.75");
1060 }
1061
1062 #[test]
1063 fn format_float_no_trailing_zeros() {
1064 let s = format_float(0.25);
1066 assert_eq!(s, "0.25");
1068 }
1069
1070 #[test]
1071 fn format_float_scientific_notation() {
1072 let v: f32 = 7.84e-06;
1074 let s = format_float(v);
1075 assert!(s.contains("e-"), "expected scientific notation: {s}");
1076 }
1077
1078 #[test]
1079 fn minimal_model_roundtrip() {
1080 let mdl = Mdl {
1082 root_node: MdlNode {
1083 name: "test_model".into(),
1084 parent_index: None,
1085 position: [0.0, 0.0, 0.0],
1086 rotation: [1.0, 0.0, 0.0, 0.0],
1087 node_data: MdlNodeData::Base,
1088 controllers: Vec::new(),
1089 children: Vec::new(),
1090 orphan_controller_data: Vec::new(),
1091 header_padding_02: [0, 0],
1092 header_padding_06: [0, 0],
1093 },
1094 geometry_fn_ptr1: 0,
1095 geometry_fn_ptr2: 0,
1096 model_type: 0,
1097 classification: 0,
1098 subclassification: 0,
1099 affected_by_fog: 1,
1100 supermodel_name: String::new(),
1101 node_count: 1,
1102 bounding_box: [0.0; 6],
1103 radius: 0.0,
1104 animation_scale: 1.0,
1105 animations: Vec::new(),
1106 anim_root_node: None,
1107 };
1108
1109 let ascii = write_mdl_ascii_to_string(&mdl).unwrap();
1110 assert!(ascii.contains("newmodel test_model"));
1111 assert!(ascii.contains("setsupermodel test_model NULL"));
1112 assert!(ascii.contains("classification other"));
1113 assert!(ascii.contains("node dummy test_model"));
1114 assert!(ascii.contains("parent NULL"));
1115 assert!(!ascii.contains("orientation 0.0 0.0 0.0 0.0"));
1117 assert!(!ascii.contains("position 0.0 0.0 0.0"));
1118 assert!(ascii.contains("endnode"));
1119 assert!(ascii.contains("endmodelgeom test_model"));
1120 assert!(ascii.contains("donemodel test_model"));
1121 }
1122
1123 fn k1_override_dir() -> Option<String> {
1126 std::env::var("KOTOR_GAME_DIR")
1127 .ok()
1128 .map(|d| format!("{d}/Override"))
1129 }
1130
1131 #[test]
1132 fn smoke_test_vanilla_model() {
1133 let base = match k1_override_dir() {
1136 Some(d) => d,
1137 None => return,
1138 };
1139 let path = format!("{base}/3dgui.mdl");
1140 let data = match std::fs::read(&path) {
1141 Ok(d) => d,
1142 Err(_) => return,
1143 };
1144 let mdl = super::super::reader::read_mdl_from_bytes(&data, None).unwrap();
1145 let ascii = write_mdl_ascii_to_string(&mdl).unwrap();
1146
1147 assert!(ascii.starts_with("newmodel "));
1149 assert!(ascii.contains("beginmodelgeom"));
1150 assert!(ascii.contains("endmodelgeom"));
1151 assert!(ascii.contains("donemodel"));
1152
1153 let node_count =
1155 ascii.matches("\nnode ").count() + if ascii.starts_with("node ") { 1 } else { 0 };
1156 assert!(node_count > 0, "expected at least one node");
1157
1158 eprintln!(
1159 "3dgui.mdl: {} bytes ASCII, {} nodes",
1160 ascii.len(),
1161 node_count
1162 );
1163 }
1164
1165 #[test]
1166 fn smoke_test_character_model() {
1167 let base = match k1_override_dir() {
1169 Some(d) => d,
1170 None => return,
1171 };
1172 let mdl_path = format!("{base}/p_bastilabb.mdl");
1173 let mdx_path = format!("{base}/p_bastilabb.mdx");
1174 let mdl_data = match std::fs::read(&mdl_path) {
1175 Ok(d) => d,
1176 Err(_) => return,
1177 };
1178 let mdx_data = std::fs::read(&mdx_path).ok();
1179 let mdl =
1180 super::super::reader::read_mdl_from_bytes(&mdl_data, mdx_data.as_deref()).unwrap();
1181 let ascii = write_mdl_ascii_to_string(&mdl).unwrap();
1182
1183 assert!(ascii.contains("newmodel"));
1184 assert!(ascii.contains("donemodel"));
1185
1186 let has_skin = ascii.contains("node skin ");
1188 let has_weights = ascii.contains("weights ");
1189 let has_anim = ascii.contains("newanim ");
1190
1191 eprintln!(
1192 "p_bastilabb.mdl: {} bytes ASCII, skin={has_skin}, weights={has_weights}, anims={has_anim}",
1193 ascii.len()
1194 );
1195 }
1196
1197 #[test]
1198 fn smoke_test_animated_model() {
1199 let base = match k1_override_dir() {
1201 Some(d) => d,
1202 None => return,
1203 };
1204 let mdl_path = format!("{base}/s_female03.mdl");
1205 let mdx_path = format!("{base}/s_female03.mdx");
1206 let mdl_data = match std::fs::read(&mdl_path) {
1207 Ok(d) => d,
1208 Err(_) => return,
1209 };
1210 let mdx_data = std::fs::read(&mdx_path).ok();
1211 let mdl =
1212 super::super::reader::read_mdl_from_bytes(&mdl_data, mdx_data.as_deref()).unwrap();
1213 let ascii = write_mdl_ascii_to_string(&mdl).unwrap();
1214
1215 assert!(ascii.contains("newmodel"));
1216 assert!(ascii.contains("donemodel"));
1217 assert!(ascii.contains("newanim "));
1218 assert!(ascii.contains("doneanim "));
1219
1220 let anim_count = ascii.matches("newanim ").count();
1221 eprintln!(
1222 "s_female03.mdl: {} bytes ASCII, {} animations",
1223 ascii.len(),
1224 anim_count
1225 );
1226 }
1227
1228 #[test]
1229 fn smoke_test_effect_model() {
1230 let base = match k1_override_dir() {
1232 Some(d) => d,
1233 None => return,
1234 };
1235 let mdl_path = format!("{base}/fx_carbref.mdl");
1236 let mdl_data = match std::fs::read(&mdl_path) {
1237 Ok(d) => d,
1238 Err(_) => return,
1239 };
1240 let mdl = super::super::reader::read_mdl_from_bytes(&mdl_data, None).unwrap();
1241 let ascii = write_mdl_ascii_to_string(&mdl).unwrap();
1242
1243 assert!(ascii.contains("newmodel"));
1244 assert!(ascii.contains("donemodel"));
1245
1246 let has_emitter = ascii.contains("node emitter ");
1247 let has_light = ascii.contains("node light ");
1248 eprintln!(
1249 "fx_carbref.mdl: {} bytes ASCII, emitter={has_emitter}, light={has_light}",
1250 ascii.len()
1251 );
1252
1253 if let Some(pos) = ascii.find("node emitter ") {
1255 let snippet = &ascii[pos..ascii.len().min(pos + 800)];
1256 for line in snippet.lines().take(30) {
1257 eprintln!(" {line}");
1258 }
1259 }
1260 }
1261}