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 m2_version >= M2Version::Cataclysm {
347 Some(M2Array::parse(reader)?)
348 } else {
349 None
350 };
351
352 let texture_transforms = if m2_version >= M2Version::Legion {
353 Some(M2Array::parse(reader)?)
354 } else {
355 None
356 };
357
358 Ok(Self {
359 magic,
360 version,
361 name,
362 flags,
363 global_sequences,
364 animations,
365 animation_lookup,
366 playable_animation_lookup,
367 bones,
368 key_bone_lookup,
369 vertices,
370 views,
371 num_skin_profiles,
372 color_animations,
373 textures,
374 transparency_lookup,
375 texture_flipbooks,
376 texture_animations,
377 color_replacements,
378 render_flags,
379 bone_lookup_table,
380 texture_lookup_table,
381 texture_units,
382 transparency_lookup_table,
383 texture_animation_lookup,
384 bounding_box_min,
385 bounding_box_max,
386 bounding_sphere_radius,
387 collision_box_min,
388 collision_box_max,
389 collision_sphere_radius,
390 bounding_triangles,
391 bounding_vertices,
392 bounding_normals,
393 attachments,
394 attachment_lookup_table,
395 events,
396 lights,
397 cameras,
398 camera_lookup_table,
399 ribbon_emitters,
400 particle_emitters,
401 blend_map_overrides,
402 texture_combiner_combos,
403 texture_transforms,
404 })
405 }
406
407 pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
409 writer.write_all(&self.magic)?;
411 writer.write_u32_le(self.version)?;
412
413 self.name.write(writer)?;
415 writer.write_u32_le(self.flags.bits())?;
416
417 self.global_sequences.write(writer)?;
418 self.animations.write(writer)?;
419 self.animation_lookup.write(writer)?;
420
421 if self.version <= 263
423 && let Some(ref pal) = self.playable_animation_lookup
424 {
425 pal.write(writer)?;
426 }
427
428 self.bones.write(writer)?;
429 self.key_bone_lookup.write(writer)?;
430
431 self.vertices.write(writer)?;
432
433 if self.version <= 263 {
435 self.views.write(writer)?;
437 } else {
438 let count = self.num_skin_profiles.unwrap_or(0);
440 writer.write_u32_le(count)?;
441 }
442
443 self.color_animations.write(writer)?;
444
445 self.textures.write(writer)?;
446 self.transparency_lookup.write(writer)?;
447
448 if self.version <= 263
450 && let Some(ref flipbooks) = self.texture_flipbooks
451 {
452 flipbooks.write(writer)?;
453 }
454
455 self.texture_animations.write(writer)?;
456
457 self.color_replacements.write(writer)?;
458 self.render_flags.write(writer)?;
459 self.bone_lookup_table.write(writer)?;
460 self.texture_lookup_table.write(writer)?;
461 self.texture_units.write(writer)?;
462 self.transparency_lookup_table.write(writer)?;
463 self.texture_animation_lookup.write(writer)?;
464
465 for &value in &self.bounding_box_min {
467 writer.write_f32_le(value)?;
468 }
469
470 for &value in &self.bounding_box_max {
471 writer.write_f32_le(value)?;
472 }
473
474 writer.write_f32_le(self.bounding_sphere_radius)?;
475
476 for &value in &self.collision_box_min {
478 writer.write_f32_le(value)?;
479 }
480
481 for &value in &self.collision_box_max {
482 writer.write_f32_le(value)?;
483 }
484
485 writer.write_f32_le(self.collision_sphere_radius)?;
486
487 self.bounding_triangles.write(writer)?;
488 self.bounding_vertices.write(writer)?;
489 self.bounding_normals.write(writer)?;
490
491 self.attachments.write(writer)?;
492 self.attachment_lookup_table.write(writer)?;
493 self.events.write(writer)?;
494 self.lights.write(writer)?;
495 self.cameras.write(writer)?;
496 self.camera_lookup_table.write(writer)?;
497
498 self.ribbon_emitters.write(writer)?;
499 self.particle_emitters.write(writer)?;
500
501 if let Some(ref overrides) = self.blend_map_overrides {
503 overrides.write(writer)?;
504 }
505
506 if let Some(ref combos) = self.texture_combiner_combos {
507 combos.write(writer)?;
508 }
509
510 if let Some(ref transforms) = self.texture_transforms {
511 transforms.write(writer)?;
512 }
513
514 Ok(())
515 }
516
517 pub fn version(&self) -> Option<M2Version> {
519 M2Version::from_header_version(self.version)
520 }
521
522 pub fn new(version: M2Version) -> Self {
524 let version_num = version.to_header_version();
525
526 let texture_combiner_combos = if version >= M2Version::Cataclysm {
527 Some(M2Array::new(0, 0))
528 } else {
529 None
530 };
531
532 let texture_transforms = if version >= M2Version::Legion {
533 Some(M2Array::new(0, 0))
534 } else {
535 None
536 };
537
538 let playable_animation_lookup = if (260..=263).contains(&version_num) {
540 Some(M2Array::new(0, 0))
541 } else {
542 None
543 };
544
545 let texture_flipbooks = if version_num <= 263 {
546 Some(M2Array::new(0, 0))
547 } else {
548 None
549 };
550
551 let num_skin_profiles = if version_num > 263 { Some(0) } else { None };
552
553 Self {
554 magic: M2_MAGIC_LEGACY,
555 version: version_num,
556 name: M2Array::new(0, 0),
557 flags: M2ModelFlags::empty(),
558 global_sequences: M2Array::new(0, 0),
559 animations: M2Array::new(0, 0),
560 animation_lookup: M2Array::new(0, 0),
561 playable_animation_lookup,
562 bones: M2Array::new(0, 0),
563 key_bone_lookup: M2Array::new(0, 0),
564 vertices: M2Array::new(0, 0),
565 views: M2Array::new(0, 0),
566 num_skin_profiles,
567 color_animations: M2Array::new(0, 0),
568 textures: M2Array::new(0, 0),
569 transparency_lookup: M2Array::new(0, 0),
570 texture_flipbooks,
571 texture_animations: M2Array::new(0, 0),
572 color_replacements: M2Array::new(0, 0),
573 render_flags: M2Array::new(0, 0),
574 bone_lookup_table: M2Array::new(0, 0),
575 texture_lookup_table: M2Array::new(0, 0),
576 texture_units: M2Array::new(0, 0),
577 transparency_lookup_table: M2Array::new(0, 0),
578 texture_animation_lookup: M2Array::new(0, 0),
579 bounding_box_min: [0.0, 0.0, 0.0],
580 bounding_box_max: [0.0, 0.0, 0.0],
581 bounding_sphere_radius: 0.0,
582 collision_box_min: [0.0, 0.0, 0.0],
583 collision_box_max: [0.0, 0.0, 0.0],
584 collision_sphere_radius: 0.0,
585 bounding_triangles: M2Array::new(0, 0),
586 bounding_vertices: M2Array::new(0, 0),
587 bounding_normals: M2Array::new(0, 0),
588 attachments: M2Array::new(0, 0),
589 attachment_lookup_table: M2Array::new(0, 0),
590 events: M2Array::new(0, 0),
591 lights: M2Array::new(0, 0),
592 cameras: M2Array::new(0, 0),
593 camera_lookup_table: M2Array::new(0, 0),
594 ribbon_emitters: M2Array::new(0, 0),
595 particle_emitters: M2Array::new(0, 0),
596 blend_map_overrides: None,
597 texture_combiner_combos,
598 texture_transforms,
599 }
600 }
601
602 pub fn convert(&self, target_version: M2Version) -> Result<Self> {
604 let source_version = self.version().ok_or(M2Error::ConversionError {
605 from: self.version,
606 to: target_version.to_header_version(),
607 reason: "Unknown source version".to_string(),
608 })?;
609
610 if source_version == target_version {
611 return Ok(self.clone());
612 }
613
614 let mut new_header = self.clone();
615 new_header.version = target_version.to_header_version();
616
617 if target_version >= M2Version::WotLK && source_version < M2Version::WotLK {
621 let count = if self.views.count > 0 {
624 self.views.count
625 } else {
626 1 };
628 new_header.num_skin_profiles = Some(count);
629 new_header.views = M2Array::new(0, 0); } else if target_version < M2Version::WotLK && source_version >= M2Version::WotLK {
631 new_header.num_skin_profiles = None;
635 new_header.views = M2Array::new(0, 0);
636 }
637
638 if target_version >= M2Version::WotLK && source_version < M2Version::WotLK {
640 new_header.playable_animation_lookup = None;
642 } else if target_version < M2Version::WotLK && source_version >= M2Version::WotLK {
643 new_header.playable_animation_lookup = Some(M2Array::new(0, 0));
645 }
646
647 if target_version >= M2Version::WotLK && source_version < M2Version::WotLK {
649 new_header.texture_flipbooks = None;
651 } else if target_version < M2Version::WotLK && source_version >= M2Version::WotLK {
652 new_header.texture_flipbooks = Some(M2Array::new(0, 0));
654 }
655
656 if target_version >= M2Version::Cataclysm && source_version < M2Version::Cataclysm {
658 new_header.texture_combiner_combos = Some(M2Array::new(0, 0));
659 } else if target_version < M2Version::Cataclysm && source_version >= M2Version::Cataclysm {
660 new_header.texture_combiner_combos = None;
661 }
662
663 if target_version >= M2Version::Legion && source_version < M2Version::Legion {
665 new_header.texture_transforms = Some(M2Array::new(0, 0));
666 } else if target_version < M2Version::Legion && source_version >= M2Version::Legion {
667 new_header.texture_transforms = None;
668 }
669
670 Ok(new_header)
671 }
672}
673
674#[cfg(test)]
675mod tests {
676 use super::*;
677 use std::io::Cursor;
678
679 fn create_test_header(version: M2Version) -> Vec<u8> {
681 let mut data = Vec::new();
682
683 data.extend_from_slice(&M2_MAGIC_LEGACY);
685
686 data.extend_from_slice(&version.to_header_version().to_le_bytes());
688
689 data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes());
695
696 data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); for _ in 0..100 {
705 data.extend_from_slice(&0u32.to_le_bytes());
706 }
707
708 data
709 }
710
711 #[test]
712 fn test_header_parse_classic() {
713 let data = create_test_header(M2Version::Vanilla);
714 let mut cursor = Cursor::new(data);
715
716 let header = M2Header::parse(&mut cursor).unwrap();
717
718 assert_eq!(header.magic, M2_MAGIC_LEGACY);
719 assert_eq!(header.version, M2Version::Vanilla.to_header_version());
720 assert_eq!(header.texture_combiner_combos, None);
721 assert_eq!(header.texture_transforms, None);
722 }
723
724 #[test]
725 fn test_header_parse_cataclysm() {
726 let data = create_test_header(M2Version::Cataclysm);
727 let mut cursor = Cursor::new(data);
728
729 let header = M2Header::parse(&mut cursor).unwrap();
730
731 assert_eq!(header.magic, M2_MAGIC_LEGACY);
732 assert_eq!(header.version, M2Version::Cataclysm.to_header_version());
733 assert!(header.texture_combiner_combos.is_some());
734 assert_eq!(header.texture_transforms, None);
735 }
736
737 #[test]
738 fn test_header_parse_legion() {
739 let data = create_test_header(M2Version::Legion);
740 let mut cursor = Cursor::new(data);
741
742 let header = M2Header::parse(&mut cursor).unwrap();
743
744 assert_eq!(header.magic, M2_MAGIC_LEGACY);
745 assert_eq!(header.version, M2Version::Legion.to_header_version());
746 assert!(header.texture_combiner_combos.is_some());
747 assert!(header.texture_transforms.is_some());
748 }
749
750 #[test]
751 fn test_header_conversion() {
752 let classic_header = M2Header::new(M2Version::Vanilla);
753
754 let cataclysm_header = classic_header.convert(M2Version::Cataclysm).unwrap();
756 assert_eq!(
757 cataclysm_header.version,
758 M2Version::Cataclysm.to_header_version()
759 );
760 assert!(cataclysm_header.texture_combiner_combos.is_some());
761 assert_eq!(cataclysm_header.texture_transforms, None);
762
763 let legion_header = cataclysm_header.convert(M2Version::Legion).unwrap();
765 assert_eq!(legion_header.version, M2Version::Legion.to_header_version());
766 assert!(legion_header.texture_combiner_combos.is_some());
767 assert!(legion_header.texture_transforms.is_some());
768
769 let classic_header_2 = legion_header.convert(M2Version::Vanilla).unwrap();
771 assert_eq!(
772 classic_header_2.version,
773 M2Version::Vanilla.to_header_version()
774 );
775 assert_eq!(classic_header_2.texture_combiner_combos, None);
776 assert_eq!(classic_header_2.texture_transforms, None);
777 }
778}