1use std::collections::HashMap;
8use std::io::{Cursor, Seek};
9
10use crate::chunks::animation::M2Animation;
11use crate::chunks::bone::M2Bone;
12use crate::chunks::material::M2Material;
13use crate::chunks::{M2Texture, M2Vertex};
14use crate::error::Result;
15use crate::model::M2Model;
16use crate::skin::SkinFile;
17
18#[derive(Debug, Clone)]
20pub struct EnhancedModelData {
21 pub vertices: Vec<M2Vertex>,
23 pub bones: Vec<BoneInfo>,
25 pub animations: Vec<AnimationInfo>,
27 pub textures: Vec<TextureInfo>,
29 pub embedded_skins: Vec<SkinFile>,
31 pub materials: Vec<MaterialInfo>,
33 pub stats: ModelStats,
35}
36
37#[derive(Debug, Clone)]
39pub struct BoneInfo {
40 pub bone: M2Bone,
42 pub parent_index: i16,
44 pub children: Vec<u16>,
46 pub name: Option<String>,
48}
49
50#[derive(Debug, Clone)]
52pub struct AnimationInfo {
53 pub animation: M2Animation,
55 pub name: String,
57 pub duration_ms: u32,
59 pub is_looping: bool,
61}
62
63#[derive(Debug, Clone)]
65pub struct TextureInfo {
66 pub texture: M2Texture,
68 pub filename: Option<String>,
70 pub texture_type: String,
72}
73
74#[derive(Debug, Clone)]
76pub struct MaterialInfo {
77 pub material: M2Material,
79 pub blend_mode: String,
81 pub is_transparent: bool,
83 pub is_two_sided: bool,
85}
86
87#[derive(Debug, Clone, Default)]
89pub struct ModelStats {
90 pub vertex_count: usize,
92 pub triangle_count: usize,
94 pub bone_count: usize,
96 pub animation_count: usize,
98 pub texture_count: usize,
100 pub material_count: usize,
102 pub embedded_skin_count: usize,
104 pub bounding_box: BoundingBox,
106}
107
108#[derive(Debug, Clone, Default)]
110pub struct BoundingBox {
111 pub min_x: f32,
112 pub min_y: f32,
113 pub min_z: f32,
114 pub max_x: f32,
115 pub max_y: f32,
116 pub max_z: f32,
117}
118
119impl BoundingBox {
120 pub fn center(&self) -> (f32, f32, f32) {
122 (
123 (self.min_x + self.max_x) / 2.0,
124 (self.min_y + self.max_y) / 2.0,
125 (self.min_z + self.max_z) / 2.0,
126 )
127 }
128
129 pub fn size(&self) -> (f32, f32, f32) {
131 (
132 self.max_x - self.min_x,
133 self.max_y - self.min_y,
134 self.max_z - self.min_z,
135 )
136 }
137
138 pub fn diagonal_length(&self) -> f32 {
140 let (width, height, depth) = self.size();
141 (width * width + height * height + depth * depth).sqrt()
142 }
143}
144
145impl M2Model {
146 pub fn parse_all_data(&self, original_data: &[u8]) -> Result<EnhancedModelData> {
181 let mut enhanced_data = EnhancedModelData {
182 vertices: Vec::new(),
183 bones: Vec::new(),
184 animations: Vec::new(),
185 textures: Vec::new(),
186 embedded_skins: Vec::new(),
187 materials: Vec::new(),
188 stats: ModelStats::default(),
189 };
190
191 enhanced_data.vertices = self.extract_all_vertices(original_data)?;
193
194 enhanced_data.bones = self.extract_all_bones_with_hierarchy(original_data)?;
196
197 enhanced_data.animations = self.extract_all_animations(original_data)?;
199
200 enhanced_data.textures = self.extract_all_textures(original_data)?;
202
203 enhanced_data.materials = self.extract_all_materials()?;
205
206 if self.has_embedded_skins() {
208 enhanced_data.embedded_skins = self.parse_all_embedded_skins(original_data)?;
209 }
210
211 enhanced_data.stats = self.calculate_model_stats(&enhanced_data)?;
213
214 Ok(enhanced_data)
215 }
216
217 fn extract_all_vertices(&self, original_data: &[u8]) -> Result<Vec<M2Vertex>> {
222 if self.header.vertices.count == 0 {
223 return Ok(Vec::new());
224 }
225
226 let mut cursor = Cursor::new(original_data);
227 cursor.seek(std::io::SeekFrom::Start(self.header.vertices.offset as u64))?;
228
229 let bone_count = self.header.bones.count;
231
232 let mut vertices = Vec::new();
233 for _ in 0..self.header.vertices.count {
234 vertices.push(M2Vertex::parse_with_validation(
236 &mut cursor,
237 self.header.version,
238 Some(bone_count),
239 crate::chunks::vertex::ValidationMode::default(),
240 )?);
241 }
242
243 Ok(vertices)
244 }
245
246 fn extract_all_bones_with_hierarchy(&self, original_data: &[u8]) -> Result<Vec<BoneInfo>> {
248 if self.header.bones.count == 0 {
249 return Ok(Vec::new());
250 }
251
252 let mut cursor = Cursor::new(original_data);
253 cursor.seek(std::io::SeekFrom::Start(self.header.bones.offset as u64))?;
254
255 let mut bone_infos = Vec::new();
256 let mut parent_map: HashMap<u16, Vec<u16>> = HashMap::new();
257
258 for i in 0..self.header.bones.count {
260 let bone = M2Bone::parse(&mut cursor, self.header.version)?;
261
262 if !bone.is_valid_for_model(self.header.bones.count) {
264 break;
267 }
268
269 let parent_index = bone.parent_bone;
270
271 if parent_index >= 0 && (parent_index as u16) < self.header.bones.count as u16 {
273 parent_map
274 .entry(parent_index as u16)
275 .or_default()
276 .push(i as u16);
277 }
278
279 bone_infos.push(BoneInfo {
280 bone,
281 parent_index,
282 children: Vec::new(),
283 name: self.get_bone_name(i as u16),
284 });
285 }
286
287 for (parent_idx, children) in parent_map {
289 if (parent_idx as usize) < bone_infos.len() {
290 bone_infos[parent_idx as usize].children = children;
291 }
292 }
293
294 Ok(bone_infos)
295 }
296
297 fn extract_all_animations(&self, original_data: &[u8]) -> Result<Vec<AnimationInfo>> {
299 if self.header.animations.count == 0 {
300 return Ok(Vec::new());
301 }
302
303 let mut cursor = Cursor::new(original_data);
304 cursor.seek(std::io::SeekFrom::Start(
305 self.header.animations.offset as u64,
306 ))?;
307
308 let mut animation_infos = Vec::new();
309
310 for i in 0..self.header.animations.count {
311 let animation = M2Animation::parse(&mut cursor, self.header.version)?;
312
313 let (name, is_looping) = self.get_animation_info(i as u16, &animation);
315
316 let duration_ms = if self.header.version <= 256 {
318 if let Some(end_timestamp) = animation.end_timestamp {
320 end_timestamp.saturating_sub(animation.start_timestamp)
321 } else {
322 animation.start_timestamp
324 }
325 } else {
326 animation.start_timestamp
328 };
329
330 animation_infos.push(AnimationInfo {
331 animation,
332 name,
333 duration_ms,
334 is_looping,
335 });
336 }
337
338 Ok(animation_infos)
339 }
340
341 fn extract_all_textures(&self, original_data: &[u8]) -> Result<Vec<TextureInfo>> {
343 if self.header.textures.count == 0 {
344 return Ok(Vec::new());
345 }
346
347 let mut cursor = Cursor::new(original_data);
348 cursor.seek(std::io::SeekFrom::Start(self.header.textures.offset as u64))?;
349
350 let mut texture_infos = Vec::new();
351
352 for i in 0..self.header.textures.count {
353 let texture = M2Texture::parse(&mut cursor, self.header.version)?;
354
355 let filename = self.resolve_texture_filename(i as u16, &texture, original_data);
357 let texture_type = self.get_texture_type_description(&texture);
358
359 texture_infos.push(TextureInfo {
360 texture,
361 filename,
362 texture_type,
363 });
364 }
365
366 Ok(texture_infos)
367 }
368
369 fn extract_all_materials(&self) -> Result<Vec<MaterialInfo>> {
371 let mut material_infos = Vec::new();
372
373 for material in &self.materials {
374 let blend_mode = self.get_blend_mode_description(material);
375 let is_transparent = material
376 .flags
377 .contains(crate::chunks::material::M2RenderFlags::NO_ZBUFFER)
378 || material.blend_mode.bits() != 0; let is_two_sided = material
380 .flags
381 .contains(crate::chunks::material::M2RenderFlags::NO_BACKFACE_CULLING);
382
383 material_infos.push(MaterialInfo {
384 material: material.clone(),
385 blend_mode,
386 is_transparent,
387 is_two_sided,
388 });
389 }
390
391 Ok(material_infos)
392 }
393
394 fn calculate_model_stats(&self, enhanced_data: &EnhancedModelData) -> Result<ModelStats> {
396 let mut stats = ModelStats {
397 vertex_count: enhanced_data.vertices.len(),
398 bone_count: enhanced_data.bones.len(),
399 animation_count: enhanced_data.animations.len(),
400 texture_count: enhanced_data.textures.len(),
401 material_count: enhanced_data.materials.len(),
402 embedded_skin_count: enhanced_data.embedded_skins.len(),
403 triangle_count: 0,
404 bounding_box: BoundingBox::default(),
405 };
406
407 for skin in &enhanced_data.embedded_skins {
409 stats.triangle_count += skin.triangles().len();
410 }
411
412 if !enhanced_data.vertices.is_empty() {
414 let first_vertex = &enhanced_data.vertices[0];
415 stats.bounding_box = BoundingBox {
416 min_x: first_vertex.position.x,
417 min_y: first_vertex.position.y,
418 min_z: first_vertex.position.z,
419 max_x: first_vertex.position.x,
420 max_y: first_vertex.position.y,
421 max_z: first_vertex.position.z,
422 };
423
424 for vertex in &enhanced_data.vertices[1..] {
425 let pos = &vertex.position;
426 stats.bounding_box.min_x = stats.bounding_box.min_x.min(pos.x);
427 stats.bounding_box.min_y = stats.bounding_box.min_y.min(pos.y);
428 stats.bounding_box.min_z = stats.bounding_box.min_z.min(pos.z);
429 stats.bounding_box.max_x = stats.bounding_box.max_x.max(pos.x);
430 stats.bounding_box.max_y = stats.bounding_box.max_y.max(pos.y);
431 stats.bounding_box.max_z = stats.bounding_box.max_z.max(pos.z);
432 }
433 }
434
435 Ok(stats)
436 }
437
438 pub fn display_info(&self, enhanced_data: &EnhancedModelData) {
463 println!("=== M2 Model Information ===");
464 println!("Model name: {:?}", self.name);
465 println!("Version: {}", self.header.version);
466 println!(
467 "Format: {}",
468 if self.header.version <= 260 {
469 "Legacy (embedded skins)"
470 } else {
471 "Modern (external skins)"
472 }
473 );
474 println!();
475
476 println!("=== Statistics ===");
478 println!("Vertices: {}", enhanced_data.stats.vertex_count);
479 println!("Triangles: {}", enhanced_data.stats.triangle_count);
480 println!("Bones: {}", enhanced_data.stats.bone_count);
481 println!("Animations: {}", enhanced_data.stats.animation_count);
482 println!("Textures: {}", enhanced_data.stats.texture_count);
483 println!("Materials: {}", enhanced_data.stats.material_count);
484 if enhanced_data.stats.embedded_skin_count > 0 {
485 println!(
486 "Embedded skins: {}",
487 enhanced_data.stats.embedded_skin_count
488 );
489 }
490 println!();
491
492 println!("=== Bounding Box ===");
494 let bbox = &enhanced_data.stats.bounding_box;
495 println!(
496 "Min: ({:.2}, {:.2}, {:.2})",
497 bbox.min_x, bbox.min_y, bbox.min_z
498 );
499 println!(
500 "Max: ({:.2}, {:.2}, {:.2})",
501 bbox.max_x, bbox.max_y, bbox.max_z
502 );
503 let (cx, cy, cz) = bbox.center();
504 println!("Center: ({:.2}, {:.2}, {:.2})", cx, cy, cz);
505 let (sx, sy, sz) = bbox.size();
506 println!("Size: ({:.2}, {:.2}, {:.2})", sx, sy, sz);
507 println!("Diagonal: {:.2}", bbox.diagonal_length());
508 println!();
509
510 if !enhanced_data.bones.is_empty() {
512 println!("=== Bone Hierarchy ===");
513 self.display_bone_hierarchy(&enhanced_data.bones, 0, 0);
514 println!();
515 }
516
517 if !enhanced_data.animations.is_empty() {
519 println!("=== Animations ===");
520 for (i, anim_info) in enhanced_data.animations.iter().enumerate() {
521 println!(
522 " {}: {} ({}ms) {}",
523 i,
524 anim_info.name,
525 anim_info.duration_ms,
526 if anim_info.is_looping { "[LOOP]" } else { "" }
527 );
528 }
529 println!();
530 }
531
532 if !enhanced_data.textures.is_empty() {
534 println!("=== Textures ===");
535 for (i, tex_info) in enhanced_data.textures.iter().enumerate() {
536 println!(
537 " {}: {} ({})",
538 i,
539 tex_info.filename.as_deref().unwrap_or("<unresolved>"),
540 tex_info.texture_type
541 );
542 }
543 println!();
544 }
545
546 if !enhanced_data.materials.is_empty() {
548 println!("=== Materials ===");
549 for (i, mat_info) in enhanced_data.materials.iter().enumerate() {
550 let flags = format!(
551 "{}{}",
552 if mat_info.is_transparent {
553 "TRANSPARENT "
554 } else {
555 ""
556 },
557 if mat_info.is_two_sided {
558 "TWO_SIDED"
559 } else {
560 ""
561 }
562 );
563 println!(" {}: {} {}", i, mat_info.blend_mode, flags);
564 }
565 println!();
566 }
567
568 if !enhanced_data.embedded_skins.is_empty() {
570 println!("=== Embedded Skins ===");
571 for (i, skin) in enhanced_data.embedded_skins.iter().enumerate() {
572 println!(
573 " Skin {}: {} indices, {} triangles, {} submeshes",
574 i,
575 skin.indices().len(),
576 skin.triangles().len(),
577 skin.submeshes().len()
578 );
579 }
580 println!();
581 }
582 }
583
584 #[allow(clippy::only_used_in_recursion)]
586 fn display_bone_hierarchy(&self, bones: &[BoneInfo], bone_index: usize, depth: usize) {
587 if bone_index >= bones.len() {
588 return;
589 }
590
591 let bone_info = &bones[bone_index];
592 let indent = " ".repeat(depth);
593 let default_name = format!("Bone_{}", bone_index);
594 let bone_name = bone_info.name.as_deref().unwrap_or(&default_name);
595
596 println!(
597 "{}└─ {} (parent: {})",
598 indent,
599 bone_name,
600 if bone_info.parent_index >= 0 {
601 bone_info.parent_index.to_string()
602 } else {
603 "ROOT".to_string()
604 }
605 );
606
607 for &child_index in &bone_info.children {
609 self.display_bone_hierarchy(bones, child_index as usize, depth + 1);
610 }
611 }
612
613 fn get_bone_name(&self, bone_index: u16) -> Option<String> {
616 Some(format!("Bone_{}", bone_index))
619 }
620
621 fn get_animation_info(&self, anim_index: u16, animation: &M2Animation) -> (String, bool) {
622 let name = match animation.animation_id {
624 0 => "Stand".to_string(),
625 1 => "Death".to_string(),
626 4 => "Walk".to_string(),
627 5 => "Run".to_string(),
628 6 => "Dead".to_string(),
629 26 => "Attack".to_string(),
630 64..=67 => "Spell Cast".to_string(),
631 _ => format!("Animation_{}", anim_index),
633 };
634
635 let is_looping = !matches!(animation.animation_id, 1 | 6 | 64..=67); (name, is_looping)
639 }
640
641 fn resolve_texture_filename(
642 &self,
643 _texture_index: u16,
644 texture: &M2Texture,
645 original_data: &[u8],
646 ) -> Option<String> {
647 if texture.filename.array.count > 0 && texture.filename.array.offset > 0 {
649 let offset = texture.filename.array.offset as usize;
650 if offset < original_data.len() {
651 let mut end_pos = offset;
653 while end_pos < original_data.len() && original_data[end_pos] != 0 {
654 end_pos += 1;
655 }
656
657 if let Ok(filename) = std::str::from_utf8(&original_data[offset..end_pos]) {
658 return Some(filename.to_string());
659 }
660 }
661 }
662 None
663 }
664
665 fn get_texture_type_description(&self, texture: &M2Texture) -> String {
666 match texture.texture_type {
667 crate::chunks::texture::M2TextureType::Hardcoded => "Hardcoded",
668 crate::chunks::texture::M2TextureType::Body => "Body + Clothes",
669 crate::chunks::texture::M2TextureType::Item => "Cape",
670 crate::chunks::texture::M2TextureType::SkinExtra => "Skin Extra",
671 crate::chunks::texture::M2TextureType::Hair => "Hair",
672 crate::chunks::texture::M2TextureType::Environment => "Environment",
673 crate::chunks::texture::M2TextureType::Monster1 => "Monster Skin 1",
674 _ => "Unknown",
675 }
676 .to_string()
677 }
678
679 fn get_blend_mode_description(&self, material: &M2Material) -> String {
680 match material.blend_mode.bits() {
683 0 => "Opaque".to_string(),
684 1 => "Alpha Key".to_string(),
685 2 => "Alpha".to_string(),
686 3 => "No Alpha Add".to_string(),
687 4 => "Add".to_string(),
688 5 => "Mod".to_string(),
689 6 => "Mod2x".to_string(),
690 7 => "Blend Add".to_string(),
691 _ => format!("Blend_Mode_{}", material.blend_mode.bits()),
692 }
693 }
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699 use crate::common::M2Array;
700
701 #[test]
702 fn test_bounding_box_calculations() {
703 let bbox = BoundingBox {
704 min_x: -1.0,
705 min_y: -2.0,
706 min_z: -3.0,
707 max_x: 1.0,
708 max_y: 2.0,
709 max_z: 3.0,
710 };
711
712 let (cx, cy, cz) = bbox.center();
713 assert_eq!(cx, 0.0);
714 assert_eq!(cy, 0.0);
715 assert_eq!(cz, 0.0);
716
717 let (sx, sy, sz) = bbox.size();
718 assert_eq!(sx, 2.0);
719 assert_eq!(sy, 4.0);
720 assert_eq!(sz, 6.0);
721
722 let diagonal = bbox.diagonal_length();
723 assert!((diagonal - (4.0 + 16.0 + 36.0_f32).sqrt()).abs() < 0.001);
724 }
725
726 #[test]
727 fn test_enhanced_data_creation() {
728 let mut model = M2Model::default();
729 model.header.version = 256;
730 model.header.views = M2Array::new(1, 0);
731
732 assert!(model.has_embedded_skins());
735 }
736}