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: [u8; 4] = *b"MD20";
11
12bitflags! {
13 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
15 pub struct M2ModelFlags: u32 {
16 const TILT_X = 0x0001;
18 const TILT_Y = 0x0002;
20 const ADD_BACK_REFERENCE = 0x0004;
22 const USE_TEXTURE_COMBINERS = 0x0008;
24 const IS_CAMERA = 0x0010;
26 const UNUSED = 0x0020;
28 const NO_PARTICLE_TRAILS = 0x0040;
30 const UNKNOWN_0x80 = 0x0080;
32 const LOAD_PHYS_DATA = 0x0100;
34 const UNKNOWN_0x200 = 0x0200;
36 const HAS_BONES = 0x0400;
38 const UNUSED_0x800 = 0x0800;
40 const UNKNOWN_0x1000 = 0x1000;
42 const USE_TEXTURE_IDS = 0x2000;
44 const CAMERA_MODIFIABLE = 0x4000;
46 const NEW_PARTICLE_SYSTEM = 0x8000;
48 const UNKNOWN_0x10000 = 0x10000;
50 const UNKNOWN_0x20000 = 0x20000;
52 const UNKNOWN_0x40000 = 0x40000;
54 const UNKNOWN_0x80000 = 0x80000;
56 const UNKNOWN_0x100000 = 0x100000;
58 const UNKNOWN_0x200000 = 0x200000;
60 const UNKNOWN_0x400000 = 0x400000;
62 const UNKNOWN_0x800000 = 0x800000;
64 const UNKNOWN_0x1000000 = 0x1000000;
66 const UNKNOWN_0x2000000 = 0x2000000;
68 const UNKNOWN_0x4000000 = 0x4000000;
70 const UNKNOWN_0x8000000 = 0x8000000;
72 const UNKNOWN_0x10000000 = 0x10000000;
74 const UNKNOWN_0x20000000 = 0x20000000;
76 const UNKNOWN_0x40000000 = 0x40000000;
78 const UNKNOWN_0x80000000 = 0x80000000;
80 }
81}
82
83#[derive(Debug, Clone)]
86pub struct M2Header {
87 pub magic: [u8; 4],
89 pub version: u32,
91 pub name: M2Array<u8>,
93 pub flags: M2ModelFlags,
95
96 pub global_sequences: M2Array<u32>,
99 pub animations: M2Array<u32>,
101 pub animation_lookup: M2Array<u16>,
103 pub playable_animation_lookup: Option<M2Array<u16>>,
105
106 pub bones: M2Array<u32>,
109 pub key_bone_lookup: M2Array<u16>,
111
112 pub vertices: M2Array<u32>,
115 pub views: M2Array<u32>,
117 pub num_skin_profiles: Option<u32>,
119
120 pub color_animations: M2Array<u32>,
123
124 pub textures: M2Array<u32>,
127 pub transparency_lookup: M2Array<u16>,
129 pub transparency_animations: M2Array<u32>,
131 pub texture_flipbooks: Option<M2Array<u32>>,
133 pub texture_animations: M2Array<u32>,
135
136 pub color_replacements: M2Array<u32>,
139 pub render_flags: M2Array<u32>,
141 pub bone_lookup_table: M2Array<u16>,
143 pub texture_lookup_table: M2Array<u16>,
145 pub texture_units: M2Array<u16>,
147 pub transparency_lookup_table: M2Array<u16>,
149 pub texture_animation_lookup: M2Array<u16>,
151
152 pub bounding_box_min: [f32; 3],
155 pub bounding_box_max: [f32; 3],
157 pub bounding_sphere_radius: f32,
159 pub collision_box_min: [f32; 3],
161 pub collision_box_max: [f32; 3],
163 pub collision_sphere_radius: f32,
165
166 pub bounding_triangles: M2Array<u32>,
169 pub bounding_vertices: M2Array<u32>,
171 pub bounding_normals: M2Array<u32>,
173
174 pub attachments: M2Array<u32>,
177 pub attachment_lookup_table: M2Array<u16>,
179 pub events: M2Array<u32>,
181 pub lights: M2Array<u32>,
183 pub cameras: M2Array<u32>,
185 pub camera_lookup_table: M2Array<u16>,
187
188 pub ribbon_emitters: M2Array<u32>,
191
192 pub particle_emitters: M2Array<u32>,
195
196 pub blend_map_overrides: Option<M2Array<u32>>,
199 pub texture_combiner_combos: Option<M2Array<u32>>,
201
202 pub texture_transforms: Option<M2Array<u32>>,
205}
206
207impl M2Header {
208 pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
210 let mut magic = [0u8; 4];
212 reader.read_exact(&mut magic)?;
213
214 if magic != M2_MAGIC {
215 return Err(M2Error::InvalidMagic {
216 expected: String::from_utf8_lossy(&M2_MAGIC).to_string(),
217 actual: String::from_utf8_lossy(&magic).to_string(),
218 });
219 }
220
221 let version = reader.read_u32_le()?;
223
224 if M2Version::from_header_version(version).is_none() {
226 return Err(M2Error::UnsupportedVersion(version.to_string()));
227 }
228
229 let name = M2Array::parse(reader)?;
231 let flags = M2ModelFlags::from_bits_retain(reader.read_u32_le()?);
232
233 let global_sequences = M2Array::parse(reader)?;
234 let animations = M2Array::parse(reader)?;
235 let animation_lookup = M2Array::parse(reader)?;
236
237 let playable_animation_lookup = if version <= 263 {
239 Some(M2Array::parse(reader)?)
240 } else {
241 None
242 };
243
244 let bones = M2Array::parse(reader)?;
245 let key_bone_lookup = M2Array::parse(reader)?;
246
247 let vertices = M2Array::parse(reader)?;
248
249 let (views, num_skin_profiles) = if version <= 263 {
251 (M2Array::parse(reader)?, None)
253 } else {
254 let count = reader.read_u32_le()?;
256 (M2Array::new(0, 0), Some(count))
257 };
258
259 let color_animations = M2Array::parse(reader)?;
260
261 let textures = M2Array::parse(reader)?;
262 let transparency_lookup = M2Array::parse(reader)?;
263 let transparency_animations = M2Array::parse(reader)?;
264
265 let texture_flipbooks = if version <= 263 {
267 Some(M2Array::parse(reader)?)
268 } else {
269 None
270 };
271
272 let texture_animations = M2Array::parse(reader)?;
273
274 let color_replacements = M2Array::parse(reader)?;
275 let render_flags = M2Array::parse(reader)?;
276 let bone_lookup_table = M2Array::parse(reader)?;
277 let texture_lookup_table = M2Array::parse(reader)?;
278 let texture_units = M2Array::parse(reader)?;
279 let transparency_lookup_table = M2Array::parse(reader)?;
280 let mut texture_animation_lookup = M2Array::parse(reader)?;
281
282 if texture_animation_lookup.count > 1_000_000 {
286 texture_animation_lookup = M2Array::new(0, 0);
287 }
288
289 let mut bounding_box_min = [0.0; 3];
291 let mut bounding_box_max = [0.0; 3];
292
293 for item in &mut bounding_box_min {
294 *item = reader.read_f32_le()?;
295 }
296
297 for item in &mut bounding_box_max {
298 *item = reader.read_f32_le()?;
299 }
300
301 let bounding_sphere_radius = reader.read_f32_le()?;
302
303 let mut collision_box_min = [0.0; 3];
305 let mut collision_box_max = [0.0; 3];
306
307 for item in &mut collision_box_min {
308 *item = reader.read_f32_le()?;
309 }
310
311 for item in &mut collision_box_max {
312 *item = reader.read_f32_le()?;
313 }
314
315 let collision_sphere_radius = reader.read_f32_le()?;
316
317 let bounding_triangles = M2Array::parse(reader)?;
318 let bounding_vertices = M2Array::parse(reader)?;
319 let bounding_normals = M2Array::parse(reader)?;
320
321 let attachments = M2Array::parse(reader)?;
322 let attachment_lookup_table = M2Array::parse(reader)?;
323 let events = M2Array::parse(reader)?;
324 let lights = M2Array::parse(reader)?;
325 let cameras = M2Array::parse(reader)?;
326 let camera_lookup_table = M2Array::parse(reader)?;
327
328 let ribbon_emitters = M2Array::parse(reader)?;
329 let particle_emitters = M2Array::parse(reader)?;
330
331 let m2_version = M2Version::from_header_version(version).unwrap();
333
334 let blend_map_overrides = if version >= 260 && (flags.bits() & 0x8000000 != 0) {
336 Some(M2Array::parse(reader)?)
338 } else {
339 None
340 };
341
342 let texture_combiner_combos = if m2_version >= M2Version::Cataclysm {
343 Some(M2Array::parse(reader)?)
344 } else {
345 None
346 };
347
348 let texture_transforms = if m2_version >= M2Version::Legion {
349 Some(M2Array::parse(reader)?)
350 } else {
351 None
352 };
353
354 Ok(Self {
355 magic,
356 version,
357 name,
358 flags,
359 global_sequences,
360 animations,
361 animation_lookup,
362 playable_animation_lookup,
363 bones,
364 key_bone_lookup,
365 vertices,
366 views,
367 num_skin_profiles,
368 color_animations,
369 textures,
370 transparency_lookup,
371 transparency_animations,
372 texture_flipbooks,
373 texture_animations,
374 color_replacements,
375 render_flags,
376 bone_lookup_table,
377 texture_lookup_table,
378 texture_units,
379 transparency_lookup_table,
380 texture_animation_lookup,
381 bounding_box_min,
382 bounding_box_max,
383 bounding_sphere_radius,
384 collision_box_min,
385 collision_box_max,
386 collision_sphere_radius,
387 bounding_triangles,
388 bounding_vertices,
389 bounding_normals,
390 attachments,
391 attachment_lookup_table,
392 events,
393 lights,
394 cameras,
395 camera_lookup_table,
396 ribbon_emitters,
397 particle_emitters,
398 blend_map_overrides,
399 texture_combiner_combos,
400 texture_transforms,
401 })
402 }
403
404 pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
406 writer.write_all(&self.magic)?;
408 writer.write_u32_le(self.version)?;
409
410 self.name.write(writer)?;
412 writer.write_u32_le(self.flags.bits())?;
413
414 self.global_sequences.write(writer)?;
415 self.animations.write(writer)?;
416 self.animation_lookup.write(writer)?;
417
418 if self.version <= 263 {
420 if let Some(ref pal) = self.playable_animation_lookup {
421 pal.write(writer)?;
422 }
423 }
424
425 self.bones.write(writer)?;
426 self.key_bone_lookup.write(writer)?;
427
428 self.vertices.write(writer)?;
429
430 if self.version <= 263 {
432 self.views.write(writer)?;
434 } else {
435 let count = self.num_skin_profiles.unwrap_or(0);
437 writer.write_u32_le(count)?;
438 }
439
440 self.color_animations.write(writer)?;
441
442 self.textures.write(writer)?;
443 self.transparency_lookup.write(writer)?;
444 self.transparency_animations.write(writer)?;
445
446 if self.version <= 263 {
448 if let Some(ref flipbooks) = self.texture_flipbooks {
449 flipbooks.write(writer)?;
450 }
451 }
452
453 self.texture_animations.write(writer)?;
454
455 self.color_replacements.write(writer)?;
456 self.render_flags.write(writer)?;
457 self.bone_lookup_table.write(writer)?;
458 self.texture_lookup_table.write(writer)?;
459 self.texture_units.write(writer)?;
460 self.transparency_lookup_table.write(writer)?;
461 self.texture_animation_lookup.write(writer)?;
462
463 for &value in &self.bounding_box_min {
465 writer.write_f32_le(value)?;
466 }
467
468 for &value in &self.bounding_box_max {
469 writer.write_f32_le(value)?;
470 }
471
472 writer.write_f32_le(self.bounding_sphere_radius)?;
473
474 for &value in &self.collision_box_min {
476 writer.write_f32_le(value)?;
477 }
478
479 for &value in &self.collision_box_max {
480 writer.write_f32_le(value)?;
481 }
482
483 writer.write_f32_le(self.collision_sphere_radius)?;
484
485 self.bounding_triangles.write(writer)?;
486 self.bounding_vertices.write(writer)?;
487 self.bounding_normals.write(writer)?;
488
489 self.attachments.write(writer)?;
490 self.attachment_lookup_table.write(writer)?;
491 self.events.write(writer)?;
492 self.lights.write(writer)?;
493 self.cameras.write(writer)?;
494 self.camera_lookup_table.write(writer)?;
495
496 self.ribbon_emitters.write(writer)?;
497 self.particle_emitters.write(writer)?;
498
499 if let Some(ref overrides) = self.blend_map_overrides {
501 overrides.write(writer)?;
502 }
503
504 if let Some(ref combos) = self.texture_combiner_combos {
505 combos.write(writer)?;
506 }
507
508 if let Some(ref transforms) = self.texture_transforms {
509 transforms.write(writer)?;
510 }
511
512 Ok(())
513 }
514
515 pub fn version(&self) -> Option<M2Version> {
517 M2Version::from_header_version(self.version)
518 }
519
520 pub fn new(version: M2Version) -> Self {
522 let version_num = version.to_header_version();
523
524 let texture_combiner_combos = if version >= M2Version::Cataclysm {
525 Some(M2Array::new(0, 0))
526 } else {
527 None
528 };
529
530 let texture_transforms = if version >= M2Version::Legion {
531 Some(M2Array::new(0, 0))
532 } else {
533 None
534 };
535
536 let playable_animation_lookup = if (260..=263).contains(&version_num) {
538 Some(M2Array::new(0, 0))
539 } else {
540 None
541 };
542
543 let texture_flipbooks = if version_num <= 263 {
544 Some(M2Array::new(0, 0))
545 } else {
546 None
547 };
548
549 let num_skin_profiles = if version_num > 263 { Some(0) } else { None };
550
551 Self {
552 magic: M2_MAGIC,
553 version: version_num,
554 name: M2Array::new(0, 0),
555 flags: M2ModelFlags::empty(),
556 global_sequences: M2Array::new(0, 0),
557 animations: M2Array::new(0, 0),
558 animation_lookup: M2Array::new(0, 0),
559 playable_animation_lookup,
560 bones: M2Array::new(0, 0),
561 key_bone_lookup: M2Array::new(0, 0),
562 vertices: M2Array::new(0, 0),
563 views: M2Array::new(0, 0),
564 num_skin_profiles,
565 color_animations: M2Array::new(0, 0),
566 textures: M2Array::new(0, 0),
567 transparency_lookup: M2Array::new(0, 0),
568 transparency_animations: M2Array::new(0, 0),
569 texture_flipbooks,
570 texture_animations: M2Array::new(0, 0),
571 color_replacements: M2Array::new(0, 0),
572 render_flags: M2Array::new(0, 0),
573 bone_lookup_table: M2Array::new(0, 0),
574 texture_lookup_table: M2Array::new(0, 0),
575 texture_units: M2Array::new(0, 0),
576 transparency_lookup_table: M2Array::new(0, 0),
577 texture_animation_lookup: M2Array::new(0, 0),
578 bounding_box_min: [0.0, 0.0, 0.0],
579 bounding_box_max: [0.0, 0.0, 0.0],
580 bounding_sphere_radius: 0.0,
581 collision_box_min: [0.0, 0.0, 0.0],
582 collision_box_max: [0.0, 0.0, 0.0],
583 collision_sphere_radius: 0.0,
584 bounding_triangles: M2Array::new(0, 0),
585 bounding_vertices: M2Array::new(0, 0),
586 bounding_normals: M2Array::new(0, 0),
587 attachments: M2Array::new(0, 0),
588 attachment_lookup_table: M2Array::new(0, 0),
589 events: M2Array::new(0, 0),
590 lights: M2Array::new(0, 0),
591 cameras: M2Array::new(0, 0),
592 camera_lookup_table: M2Array::new(0, 0),
593 ribbon_emitters: M2Array::new(0, 0),
594 particle_emitters: M2Array::new(0, 0),
595 blend_map_overrides: None,
596 texture_combiner_combos,
597 texture_transforms,
598 }
599 }
600
601 pub fn convert(&self, target_version: M2Version) -> Result<Self> {
603 let source_version = self.version().ok_or(M2Error::ConversionError {
604 from: self.version,
605 to: target_version.to_header_version(),
606 reason: "Unknown source version".to_string(),
607 })?;
608
609 if source_version == target_version {
610 return Ok(self.clone());
611 }
612
613 let mut new_header = self.clone();
614 new_header.version = target_version.to_header_version();
615
616 if target_version >= M2Version::Cataclysm && source_version < M2Version::Cataclysm {
618 new_header.texture_combiner_combos = Some(M2Array::new(0, 0));
620 } else if target_version < M2Version::Cataclysm && source_version >= M2Version::Cataclysm {
621 new_header.texture_combiner_combos = None;
623 }
624
625 if target_version >= M2Version::Legion && source_version < M2Version::Legion {
626 new_header.texture_transforms = Some(M2Array::new(0, 0));
628 } else if target_version < M2Version::Legion && source_version >= M2Version::Legion {
629 new_header.texture_transforms = None;
631 }
632
633 Ok(new_header)
634 }
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640 use std::io::Cursor;
641
642 fn create_test_header(version: M2Version) -> Vec<u8> {
644 let mut data = Vec::new();
645
646 data.extend_from_slice(&M2_MAGIC);
648
649 data.extend_from_slice(&version.to_header_version().to_le_bytes());
651
652 data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes());
658
659 data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes()); for _ in 0..100 {
668 data.extend_from_slice(&0u32.to_le_bytes());
669 }
670
671 data
672 }
673
674 #[test]
675 fn test_header_parse_classic() {
676 let data = create_test_header(M2Version::Classic);
677 let mut cursor = Cursor::new(data);
678
679 let header = M2Header::parse(&mut cursor).unwrap();
680
681 assert_eq!(header.magic, M2_MAGIC);
682 assert_eq!(header.version, M2Version::Classic.to_header_version());
683 assert_eq!(header.texture_combiner_combos, None);
684 assert_eq!(header.texture_transforms, None);
685 }
686
687 #[test]
688 fn test_header_parse_cataclysm() {
689 let data = create_test_header(M2Version::Cataclysm);
690 let mut cursor = Cursor::new(data);
691
692 let header = M2Header::parse(&mut cursor).unwrap();
693
694 assert_eq!(header.magic, M2_MAGIC);
695 assert_eq!(header.version, M2Version::Cataclysm.to_header_version());
696 assert!(header.texture_combiner_combos.is_some());
697 assert_eq!(header.texture_transforms, None);
698 }
699
700 #[test]
701 fn test_header_parse_legion() {
702 let data = create_test_header(M2Version::Legion);
703 let mut cursor = Cursor::new(data);
704
705 let header = M2Header::parse(&mut cursor).unwrap();
706
707 assert_eq!(header.magic, M2_MAGIC);
708 assert_eq!(header.version, M2Version::Legion.to_header_version());
709 assert!(header.texture_combiner_combos.is_some());
710 assert!(header.texture_transforms.is_some());
711 }
712
713 #[test]
714 fn test_header_conversion() {
715 let classic_header = M2Header::new(M2Version::Classic);
716
717 let cataclysm_header = classic_header.convert(M2Version::Cataclysm).unwrap();
719 assert_eq!(
720 cataclysm_header.version,
721 M2Version::Cataclysm.to_header_version()
722 );
723 assert!(cataclysm_header.texture_combiner_combos.is_some());
724 assert_eq!(cataclysm_header.texture_transforms, None);
725
726 let legion_header = cataclysm_header.convert(M2Version::Legion).unwrap();
728 assert_eq!(legion_header.version, M2Version::Legion.to_header_version());
729 assert!(legion_header.texture_combiner_combos.is_some());
730 assert!(legion_header.texture_transforms.is_some());
731
732 let classic_header_2 = legion_header.convert(M2Version::Classic).unwrap();
734 assert_eq!(
735 classic_header_2.version,
736 M2Version::Classic.to_header_version()
737 );
738 assert_eq!(classic_header_2.texture_combiner_combos, None);
739 assert_eq!(classic_header_2.texture_transforms, None);
740 }
741}