1use crate::io_ext::{ReadExt, WriteExt};
2use bitflags::bitflags;
3use std::io::{Read, Seek, Write};
4
5use crate::common::M2Array;
6use crate::error::{M2Error, Result};
7use crate::version::M2Version;
8
9pub const M2_MAGIC_LEGACY: [u8; 4] = *b"MD20";
11
12pub const M2_MAGIC_CHUNKED: [u8; 4] = *b"MD21";
14
15pub const M2_MAGIC: [u8; 4] = M2_MAGIC_LEGACY;
17
18bitflags! {
19 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
21 pub struct M2ModelFlags: u32 {
22 const TILT_X = 0x0001;
24 const TILT_Y = 0x0002;
26 const ADD_BACK_REFERENCE = 0x0004;
28 const USE_TEXTURE_COMBINERS = 0x0008;
30 const IS_CAMERA = 0x0010;
32 const UNUSED = 0x0020;
34 const NO_PARTICLE_TRAILS = 0x0040;
36 const UNKNOWN_0x80 = 0x0080;
38 const LOAD_PHYS_DATA = 0x0100;
40 const UNKNOWN_0x200 = 0x0200;
42 const HAS_BONES = 0x0400;
44 const UNUSED_0x800 = 0x0800;
46 const UNKNOWN_0x1000 = 0x1000;
48 const USE_TEXTURE_IDS = 0x2000;
50 const CAMERA_MODIFIABLE = 0x4000;
52 const NEW_PARTICLE_SYSTEM = 0x8000;
54 const UNKNOWN_0x10000 = 0x10000;
56 const UNKNOWN_0x20000 = 0x20000;
58 const UNKNOWN_0x40000 = 0x40000;
60 const UNKNOWN_0x80000 = 0x80000;
62 const UNKNOWN_0x100000 = 0x100000;
64 const UNKNOWN_0x200000 = 0x200000;
66 const UNKNOWN_0x400000 = 0x400000;
68 const UNKNOWN_0x800000 = 0x800000;
70 const UNKNOWN_0x1000000 = 0x1000000;
72 const UNKNOWN_0x2000000 = 0x2000000;
74 const UNKNOWN_0x4000000 = 0x4000000;
76 const UNKNOWN_0x8000000 = 0x8000000;
78 const UNKNOWN_0x10000000 = 0x10000000;
80 const UNKNOWN_0x20000000 = 0x20000000;
82 const UNKNOWN_0x40000000 = 0x40000000;
84 const UNKNOWN_0x80000000 = 0x80000000;
86 }
87}
88
89#[derive(Debug, Clone)]
92pub struct M2Header {
93 pub magic: [u8; 4],
95 pub version: u32,
97 pub name: M2Array<u8>,
99 pub flags: M2ModelFlags,
101
102 pub global_sequences: M2Array<u32>,
105 pub animations: M2Array<u32>,
107 pub animation_lookup: M2Array<u16>,
109 pub playable_animation_lookup: Option<M2Array<u16>>,
111
112 pub bones: M2Array<u32>,
115 pub key_bone_lookup: M2Array<u16>,
117
118 pub vertices: M2Array<u32>,
121 pub views: M2Array<u32>,
123 pub num_skin_profiles: Option<u32>,
125
126 pub color_animations: M2Array<u32>,
129
130 pub textures: M2Array<u32>,
133 pub transparency_lookup: M2Array<u16>,
135 pub texture_flipbooks: Option<M2Array<u32>>,
137 pub texture_animations: M2Array<u32>,
139
140 pub color_replacements: M2Array<u32>,
143 pub render_flags: M2Array<u32>,
145 pub bone_lookup_table: M2Array<u16>,
147 pub texture_lookup_table: M2Array<u16>,
149 pub texture_units: M2Array<u16>,
151 pub transparency_lookup_table: M2Array<u16>,
153 pub texture_animation_lookup: M2Array<u16>,
155
156 pub bounding_box_min: [f32; 3],
159 pub bounding_box_max: [f32; 3],
161 pub bounding_sphere_radius: f32,
163 pub collision_box_min: [f32; 3],
165 pub collision_box_max: [f32; 3],
167 pub collision_sphere_radius: f32,
169
170 pub bounding_triangles: M2Array<u32>,
173 pub bounding_vertices: M2Array<u32>,
175 pub bounding_normals: M2Array<u32>,
177
178 pub attachments: M2Array<u32>,
181 pub attachment_lookup_table: M2Array<u16>,
183 pub events: M2Array<u32>,
185 pub lights: M2Array<u32>,
187 pub cameras: M2Array<u32>,
189 pub camera_lookup_table: M2Array<u16>,
191
192 pub ribbon_emitters: M2Array<u32>,
195
196 pub particle_emitters: M2Array<u32>,
199
200 pub blend_map_overrides: Option<M2Array<u32>>,
203 pub texture_combiner_combos: Option<M2Array<u32>>,
205
206 pub texture_transforms: Option<M2Array<u32>>,
209}
210
211impl M2Header {
212 pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
214 let mut magic = [0u8; 4];
216 reader.read_exact(&mut magic)?;
217
218 if magic != M2_MAGIC_LEGACY {
219 return Err(M2Error::InvalidMagic {
220 expected: String::from_utf8_lossy(&M2_MAGIC_LEGACY).to_string(),
221 actual: String::from_utf8_lossy(&magic).to_string(),
222 });
223 }
224
225 let version = reader.read_u32_le()?;
227
228 if M2Version::from_header_version(version).is_none() {
230 return Err(M2Error::UnsupportedVersion(version.to_string()));
231 }
232
233 let name = M2Array::parse(reader)?;
235 let flags = M2ModelFlags::from_bits_retain(reader.read_u32_le()?);
236
237 let global_sequences = M2Array::parse(reader)?;
238 let animations = M2Array::parse(reader)?;
239 let animation_lookup = M2Array::parse(reader)?;
240
241 let playable_animation_lookup = if (256..=263).contains(&version) {
244 Some(M2Array::parse(reader)?)
245 } else {
246 None
247 };
248
249 let bones = M2Array::parse(reader)?;
250 let key_bone_lookup = M2Array::parse(reader)?;
251
252 let vertices = M2Array::parse(reader)?;
253
254 let (views, num_skin_profiles) = if version <= 263 {
256 (M2Array::parse(reader)?, None)
258 } else {
259 let count = reader.read_u32_le()?;
261 (M2Array::new(0, 0), Some(count))
262 };
263
264 let color_animations = M2Array::parse(reader)?;
265
266 let textures = M2Array::parse(reader)?;
267 let transparency_lookup = M2Array::parse(reader)?;
268
269 let texture_flipbooks = if version <= 263 {
271 Some(M2Array::parse(reader)?)
272 } else {
273 None
274 };
275
276 let texture_animations = M2Array::parse(reader)?;
277
278 let color_replacements = M2Array::parse(reader)?;
279 let render_flags = M2Array::parse(reader)?;
280 let bone_lookup_table = M2Array::parse(reader)?;
281 let texture_lookup_table = M2Array::parse(reader)?;
282 let texture_units = M2Array::parse(reader)?;
283 let transparency_lookup_table = M2Array::parse(reader)?;
284 let mut texture_animation_lookup = M2Array::parse(reader)?;
285
286 if texture_animation_lookup.count > 1_000_000 {
290 texture_animation_lookup = M2Array::new(0, 0);
291 }
292
293 let mut bounding_box_min = [0.0; 3];
295 let mut bounding_box_max = [0.0; 3];
296
297 for item in &mut bounding_box_min {
298 *item = reader.read_f32_le()?;
299 }
300
301 for item in &mut bounding_box_max {
302 *item = reader.read_f32_le()?;
303 }
304
305 let bounding_sphere_radius = reader.read_f32_le()?;
306
307 let mut collision_box_min = [0.0; 3];
309 let mut collision_box_max = [0.0; 3];
310
311 for item in &mut collision_box_min {
312 *item = reader.read_f32_le()?;
313 }
314
315 for item in &mut collision_box_max {
316 *item = reader.read_f32_le()?;
317 }
318
319 let collision_sphere_radius = reader.read_f32_le()?;
320
321 let bounding_triangles = M2Array::parse(reader)?;
322 let bounding_vertices = M2Array::parse(reader)?;
323 let bounding_normals = M2Array::parse(reader)?;
324
325 let attachments = M2Array::parse(reader)?;
326 let attachment_lookup_table = M2Array::parse(reader)?;
327 let events = M2Array::parse(reader)?;
328 let lights = M2Array::parse(reader)?;
329 let cameras = M2Array::parse(reader)?;
330 let camera_lookup_table = M2Array::parse(reader)?;
331
332 let ribbon_emitters = M2Array::parse(reader)?;
333 let particle_emitters = M2Array::parse(reader)?;
334
335 let m2_version = M2Version::from_header_version(version).unwrap();
337
338 let blend_map_overrides = if version >= 260 && (flags.bits() & 0x8000000 != 0) {
340 Some(M2Array::parse(reader)?)
342 } else {
343 None
344 };
345
346 let texture_combiner_combos = if flags.contains(M2ModelFlags::USE_TEXTURE_COMBINERS) {
348 Some(M2Array::parse(reader)?)
349 } else {
350 None
351 };
352
353 let texture_transforms = if m2_version >= M2Version::Legion {
354 Some(M2Array::parse(reader)?)
355 } else {
356 None
357 };
358
359 Ok(Self {
360 magic,
361 version,
362 name,
363 flags,
364 global_sequences,
365 animations,
366 animation_lookup,
367 playable_animation_lookup,
368 bones,
369 key_bone_lookup,
370 vertices,
371 views,
372 num_skin_profiles,
373 color_animations,
374 textures,
375 transparency_lookup,
376 texture_flipbooks,
377 texture_animations,
378 color_replacements,
379 render_flags,
380 bone_lookup_table,
381 texture_lookup_table,
382 texture_units,
383 transparency_lookup_table,
384 texture_animation_lookup,
385 bounding_box_min,
386 bounding_box_max,
387 bounding_sphere_radius,
388 collision_box_min,
389 collision_box_max,
390 collision_sphere_radius,
391 bounding_triangles,
392 bounding_vertices,
393 bounding_normals,
394 attachments,
395 attachment_lookup_table,
396 events,
397 lights,
398 cameras,
399 camera_lookup_table,
400 ribbon_emitters,
401 particle_emitters,
402 blend_map_overrides,
403 texture_combiner_combos,
404 texture_transforms,
405 })
406 }
407
408 pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
410 writer.write_all(&self.magic)?;
412 writer.write_u32_le(self.version)?;
413
414 self.name.write(writer)?;
416 writer.write_u32_le(self.flags.bits())?;
417
418 self.global_sequences.write(writer)?;
419 self.animations.write(writer)?;
420 self.animation_lookup.write(writer)?;
421
422 if self.version <= 263
424 && let Some(ref pal) = self.playable_animation_lookup
425 {
426 pal.write(writer)?;
427 }
428
429 self.bones.write(writer)?;
430 self.key_bone_lookup.write(writer)?;
431
432 self.vertices.write(writer)?;
433
434 if self.version <= 263 {
436 self.views.write(writer)?;
438 } else {
439 let count = self.num_skin_profiles.unwrap_or(0);
441 writer.write_u32_le(count)?;
442 }
443
444 self.color_animations.write(writer)?;
445
446 self.textures.write(writer)?;
447 self.transparency_lookup.write(writer)?;
448
449 if self.version <= 263
451 && let Some(ref flipbooks) = self.texture_flipbooks
452 {
453 flipbooks.write(writer)?;
454 }
455
456 self.texture_animations.write(writer)?;
457
458 self.color_replacements.write(writer)?;
459 self.render_flags.write(writer)?;
460 self.bone_lookup_table.write(writer)?;
461 self.texture_lookup_table.write(writer)?;
462 self.texture_units.write(writer)?;
463 self.transparency_lookup_table.write(writer)?;
464 self.texture_animation_lookup.write(writer)?;
465
466 for &value in &self.bounding_box_min {
468 writer.write_f32_le(value)?;
469 }
470
471 for &value in &self.bounding_box_max {
472 writer.write_f32_le(value)?;
473 }
474
475 writer.write_f32_le(self.bounding_sphere_radius)?;
476
477 for &value in &self.collision_box_min {
479 writer.write_f32_le(value)?;
480 }
481
482 for &value in &self.collision_box_max {
483 writer.write_f32_le(value)?;
484 }
485
486 writer.write_f32_le(self.collision_sphere_radius)?;
487
488 self.bounding_triangles.write(writer)?;
489 self.bounding_vertices.write(writer)?;
490 self.bounding_normals.write(writer)?;
491
492 self.attachments.write(writer)?;
493 self.attachment_lookup_table.write(writer)?;
494 self.events.write(writer)?;
495 self.lights.write(writer)?;
496 self.cameras.write(writer)?;
497 self.camera_lookup_table.write(writer)?;
498
499 self.ribbon_emitters.write(writer)?;
500 self.particle_emitters.write(writer)?;
501
502 if let Some(ref overrides) = self.blend_map_overrides {
504 overrides.write(writer)?;
505 }
506
507 if let Some(ref combos) = self.texture_combiner_combos {
508 combos.write(writer)?;
509 }
510
511 if let Some(ref transforms) = self.texture_transforms {
512 transforms.write(writer)?;
513 }
514
515 Ok(())
516 }
517
518 pub fn version(&self) -> Option<M2Version> {
520 M2Version::from_header_version(self.version)
521 }
522
523 pub fn new(version: M2Version) -> Self {
525 let version_num = version.to_header_version();
526
527 let texture_combiner_combos = None;
528
529 let texture_transforms = if version >= M2Version::Legion {
530 Some(M2Array::new(0, 0))
531 } else {
532 None
533 };
534
535 let playable_animation_lookup = if (260..=263).contains(&version_num) {
537 Some(M2Array::new(0, 0))
538 } else {
539 None
540 };
541
542 let texture_flipbooks = if version_num <= 263 {
543 Some(M2Array::new(0, 0))
544 } else {
545 None
546 };
547
548 let num_skin_profiles = if version_num > 263 { Some(0) } else { None };
549
550 Self {
551 magic: M2_MAGIC_LEGACY,
552 version: version_num,
553 name: M2Array::new(0, 0),
554 flags: M2ModelFlags::empty(),
555 global_sequences: M2Array::new(0, 0),
556 animations: M2Array::new(0, 0),
557 animation_lookup: M2Array::new(0, 0),
558 playable_animation_lookup,
559 bones: M2Array::new(0, 0),
560 key_bone_lookup: M2Array::new(0, 0),
561 vertices: M2Array::new(0, 0),
562 views: M2Array::new(0, 0),
563 num_skin_profiles,
564 color_animations: M2Array::new(0, 0),
565 textures: M2Array::new(0, 0),
566 transparency_lookup: M2Array::new(0, 0),
567 texture_flipbooks,
568 texture_animations: M2Array::new(0, 0),
569 color_replacements: M2Array::new(0, 0),
570 render_flags: M2Array::new(0, 0),
571 bone_lookup_table: M2Array::new(0, 0),
572 texture_lookup_table: M2Array::new(0, 0),
573 texture_units: M2Array::new(0, 0),
574 transparency_lookup_table: M2Array::new(0, 0),
575 texture_animation_lookup: M2Array::new(0, 0),
576 bounding_box_min: [0.0, 0.0, 0.0],
577 bounding_box_max: [0.0, 0.0, 0.0],
578 bounding_sphere_radius: 0.0,
579 collision_box_min: [0.0, 0.0, 0.0],
580 collision_box_max: [0.0, 0.0, 0.0],
581 collision_sphere_radius: 0.0,
582 bounding_triangles: M2Array::new(0, 0),
583 bounding_vertices: M2Array::new(0, 0),
584 bounding_normals: M2Array::new(0, 0),
585 attachments: M2Array::new(0, 0),
586 attachment_lookup_table: M2Array::new(0, 0),
587 events: M2Array::new(0, 0),
588 lights: M2Array::new(0, 0),
589 cameras: M2Array::new(0, 0),
590 camera_lookup_table: M2Array::new(0, 0),
591 ribbon_emitters: M2Array::new(0, 0),
592 particle_emitters: M2Array::new(0, 0),
593 blend_map_overrides: None,
594 texture_combiner_combos,
595 texture_transforms,
596 }
597 }
598
599 pub fn convert(&self, target_version: M2Version) -> Result<Self> {
601 let source_version = self.version().ok_or(M2Error::ConversionError {
602 from: self.version,
603 to: target_version.to_header_version(),
604 reason: "Unknown source version".to_string(),
605 })?;
606
607 if source_version == target_version {
608 return Ok(self.clone());
609 }
610
611 let mut new_header = self.clone();
612 new_header.version = target_version.to_header_version();
613
614 if target_version >= M2Version::WotLK && source_version < M2Version::WotLK {
618 let count = if self.views.count > 0 {
621 self.views.count
622 } else {
623 1 };
625 new_header.num_skin_profiles = Some(count);
626 new_header.views = M2Array::new(0, 0); } else if target_version < M2Version::WotLK && source_version >= M2Version::WotLK {
628 new_header.num_skin_profiles = None;
632 new_header.views = M2Array::new(0, 0);
633 }
634
635 if target_version >= M2Version::WotLK && source_version < M2Version::WotLK {
637 new_header.playable_animation_lookup = None;
639 } else if target_version < M2Version::WotLK && source_version >= M2Version::WotLK {
640 new_header.playable_animation_lookup = Some(M2Array::new(0, 0));
642 }
643
644 if target_version >= M2Version::WotLK && source_version < M2Version::WotLK {
646 new_header.texture_flipbooks = None;
648 } else if target_version < M2Version::WotLK && source_version >= M2Version::WotLK {
649 new_header.texture_flipbooks = Some(M2Array::new(0, 0));
651 }
652
653 let source_has_combos = self.flags.contains(M2ModelFlags::USE_TEXTURE_COMBINERS);
656 if target_version >= M2Version::Cataclysm && source_has_combos {
657 new_header.texture_combiner_combos = Some(M2Array::new(0, 0));
658 } else {
659 new_header.texture_combiner_combos = None;
660 }
661
662 if target_version >= M2Version::Legion && source_version < M2Version::Legion {
664 new_header.texture_transforms = Some(M2Array::new(0, 0));
665 } else if target_version < M2Version::Legion && source_version >= M2Version::Legion {
666 new_header.texture_transforms = None;
667 }
668
669 Ok(new_header)
670 }
671}
672
673#[cfg(test)]
674mod tests {
675 use super::*;
676 use std::io::Cursor;
677
678 fn create_test_header(version: M2Version, flags: M2ModelFlags) -> Vec<u8> {
680 let mut data = Vec::new();
681
682 data.extend_from_slice(&M2_MAGIC_LEGACY);
684
685 data.extend_from_slice(&version.to_header_version().to_le_bytes());
687
688 data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&flags.bits().to_le_bytes());
694
695 data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); for _ in 0..100 {
704 data.extend_from_slice(&0u32.to_le_bytes());
705 }
706
707 data
708 }
709
710 #[test]
711 fn test_header_parse_classic() {
712 let data = create_test_header(M2Version::Vanilla, M2ModelFlags::empty());
713 let mut cursor = Cursor::new(data);
714
715 let header = M2Header::parse(&mut cursor).unwrap();
716
717 assert_eq!(header.magic, M2_MAGIC_LEGACY);
718 assert_eq!(header.version, M2Version::Vanilla.to_header_version());
719 assert_eq!(header.texture_combiner_combos, None);
720 assert_eq!(header.texture_transforms, None);
721 }
722
723 #[test]
724 fn test_header_parse_cataclysm() {
725 let data = create_test_header(M2Version::Cataclysm, M2ModelFlags::USE_TEXTURE_COMBINERS);
726 let mut cursor = Cursor::new(data);
727
728 let header = M2Header::parse(&mut cursor).unwrap();
729
730 assert_eq!(header.magic, M2_MAGIC_LEGACY);
731 assert_eq!(header.version, M2Version::Cataclysm.to_header_version());
732 assert!(header.texture_combiner_combos.is_some());
733 assert_eq!(header.texture_transforms, None);
734 }
735
736 #[test]
737 fn test_header_parse_legion() {
738 let data = create_test_header(M2Version::Legion, M2ModelFlags::USE_TEXTURE_COMBINERS);
739 let mut cursor = Cursor::new(data);
740
741 let header = M2Header::parse(&mut cursor).unwrap();
742
743 assert_eq!(header.magic, M2_MAGIC_LEGACY);
744 assert_eq!(header.version, M2Version::Legion.to_header_version());
745 assert!(header.texture_combiner_combos.is_some());
746 assert!(header.texture_transforms.is_some());
747 }
748
749 #[test]
750 fn test_header_conversion() {
751 let mut classic_header = M2Header::new(M2Version::Vanilla);
752 classic_header.flags = M2ModelFlags::USE_TEXTURE_COMBINERS;
754
755 let cataclysm_header = classic_header.convert(M2Version::Cataclysm).unwrap();
757 assert_eq!(
758 cataclysm_header.version,
759 M2Version::Cataclysm.to_header_version()
760 );
761 assert!(cataclysm_header.texture_combiner_combos.is_some());
762 assert_eq!(cataclysm_header.texture_transforms, None);
763
764 let legion_header = cataclysm_header.convert(M2Version::Legion).unwrap();
766 assert_eq!(legion_header.version, M2Version::Legion.to_header_version());
767 assert!(legion_header.texture_combiner_combos.is_some());
768 assert!(legion_header.texture_transforms.is_some());
769
770 let classic_header_2 = legion_header.convert(M2Version::Vanilla).unwrap();
772 assert_eq!(
773 classic_header_2.version,
774 M2Version::Vanilla.to_header_version()
775 );
776 assert_eq!(classic_header_2.texture_combiner_combos, None);
777 assert_eq!(classic_header_2.texture_transforms, None);
778 }
779}