wow_m2/
header.rs

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
9/// Magic signature for M2 files ("MD20")
10pub const M2_MAGIC: [u8; 4] = *b"MD20";
11
12bitflags! {
13    /// Model flags as defined in the M2 format
14    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
15    pub struct M2ModelFlags: u32 {
16        /// Tilt on X axis
17        const TILT_X = 0x0001;
18        /// Tilt on Y axis
19        const TILT_Y = 0x0002;
20        /// Add a back-reference to the model
21        const ADD_BACK_REFERENCE = 0x0004;
22        /// Use texture combiners
23        const USE_TEXTURE_COMBINERS = 0x0008;
24        /// Is it a camera?
25        const IS_CAMERA = 0x0010;
26        /// Unused flag
27        const UNUSED = 0x0020;
28        /// No particle trails
29        const NO_PARTICLE_TRAILS = 0x0040;
30        /// Unknown
31        const UNKNOWN_0x80 = 0x0080;
32        /// Load phys data
33        const LOAD_PHYS_DATA = 0x0100;
34        /// Unknown
35        const UNKNOWN_0x200 = 0x0200;
36        /// Has bones
37        const HAS_BONES = 0x0400;
38        /// Unused 0x800
39        const UNUSED_0x800 = 0x0800;
40        /// Unknown
41        const UNKNOWN_0x1000 = 0x1000;
42        /// Use texture IDs
43        const USE_TEXTURE_IDS = 0x2000;
44        /// Camera can be modified
45        const CAMERA_MODIFIABLE = 0x4000;
46        /// New particle system
47        const NEW_PARTICLE_SYSTEM = 0x8000;
48        /// Unknown
49        const UNKNOWN_0x10000 = 0x10000;
50        /// Unknown
51        const UNKNOWN_0x20000 = 0x20000;
52        /// Unknown
53        const UNKNOWN_0x40000 = 0x40000;
54        /// Unknown
55        const UNKNOWN_0x80000 = 0x80000;
56        /// Unknown
57        const UNKNOWN_0x100000 = 0x100000;
58        /// Unknown
59        const UNKNOWN_0x200000 = 0x200000;
60        /// Unknown
61        const UNKNOWN_0x400000 = 0x400000;
62        /// Unknown
63        const UNKNOWN_0x800000 = 0x800000;
64        /// Unknown
65        const UNKNOWN_0x1000000 = 0x1000000;
66        /// Unknown
67        const UNKNOWN_0x2000000 = 0x2000000;
68        /// Unknown
69        const UNKNOWN_0x4000000 = 0x4000000;
70        /// Unknown
71        const UNKNOWN_0x8000000 = 0x8000000;
72        /// Unknown
73        const UNKNOWN_0x10000000 = 0x10000000;
74        /// Unknown
75        const UNKNOWN_0x20000000 = 0x20000000;
76        /// Unknown
77        const UNKNOWN_0x40000000 = 0x40000000;
78        /// Unknown
79        const UNKNOWN_0x80000000 = 0x80000000;
80    }
81}
82
83/// M2 model header structure
84/// Based on: <https://wowdev.wiki/M2#Header>
85#[derive(Debug, Clone)]
86pub struct M2Header {
87    /// Magic signature ("MD20")
88    pub magic: [u8; 4],
89    /// Version of the M2 file
90    pub version: u32,
91    /// Name of the model
92    pub name: M2Array<u8>,
93    /// Flags
94    pub flags: M2ModelFlags,
95
96    // Sequence-related fields
97    /// Global sequences
98    pub global_sequences: M2Array<u32>,
99    /// Animations
100    pub animations: M2Array<u32>,
101    /// Animation lookups (C in Classic)
102    pub animation_lookup: M2Array<u16>,
103    /// Playable animation lookup - only present in versions <= 263
104    pub playable_animation_lookup: Option<M2Array<u16>>,
105
106    // Bone-related fields
107    /// Bones
108    pub bones: M2Array<u32>,
109    /// Key bone lookup
110    pub key_bone_lookup: M2Array<u16>,
111
112    // Geometry data
113    /// Vertices
114    pub vertices: M2Array<u32>,
115    /// Views (LOD levels) - M2Array for BC and earlier, count for later versions
116    pub views: M2Array<u32>,
117    /// Number of skin profiles for WotLK+ (when views becomes a count)
118    pub num_skin_profiles: Option<u32>,
119
120    // Color data
121    /// Color animations
122    pub color_animations: M2Array<u32>,
123
124    // Texture-related fields
125    /// Textures
126    pub textures: M2Array<u32>,
127    /// Transparency lookups
128    pub transparency_lookup: M2Array<u16>,
129    /// Transparency animations
130    pub transparency_animations: M2Array<u32>,
131    /// Texture flipbooks - only present in BC and earlier
132    pub texture_flipbooks: Option<M2Array<u32>>,
133    /// Texture animations
134    pub texture_animations: M2Array<u32>,
135
136    // Material data
137    /// Color replacements
138    pub color_replacements: M2Array<u32>,
139    /// Render flags
140    pub render_flags: M2Array<u32>,
141    /// Bone lookup table
142    pub bone_lookup_table: M2Array<u16>,
143    /// Texture lookup table
144    pub texture_lookup_table: M2Array<u16>,
145    /// Texture units
146    pub texture_units: M2Array<u16>,
147    /// Transparency lookup table
148    pub transparency_lookup_table: M2Array<u16>,
149    /// Texture animation lookup table
150    pub texture_animation_lookup: M2Array<u16>,
151
152    // Bounding box data
153    /// Bounding box min corner
154    pub bounding_box_min: [f32; 3],
155    /// Bounding box max corner
156    pub bounding_box_max: [f32; 3],
157    /// Bounding sphere radius
158    pub bounding_sphere_radius: f32,
159    /// Collision bounding box min corner
160    pub collision_box_min: [f32; 3],
161    /// Collision bounding box max corner
162    pub collision_box_max: [f32; 3],
163    /// Collision bounding sphere radius
164    pub collision_sphere_radius: f32,
165
166    // Additional geometry data
167    /// Bounding triangles
168    pub bounding_triangles: M2Array<u32>,
169    /// Bounding vertices
170    pub bounding_vertices: M2Array<u32>,
171    /// Bounding normals
172    pub bounding_normals: M2Array<u32>,
173
174    // Attachments and events
175    /// Attachments
176    pub attachments: M2Array<u32>,
177    /// Attachment lookup table
178    pub attachment_lookup_table: M2Array<u16>,
179    /// Events
180    pub events: M2Array<u32>,
181    /// Lights
182    pub lights: M2Array<u32>,
183    /// Cameras
184    pub cameras: M2Array<u32>,
185    /// Camera lookup table
186    pub camera_lookup_table: M2Array<u16>,
187
188    // Ribbon emitters
189    /// Ribbon emitters
190    pub ribbon_emitters: M2Array<u32>,
191
192    // Particle systems
193    /// Particle emitters
194    pub particle_emitters: M2Array<u32>,
195
196    // Additional fields for newer versions
197    /// Blend map overrides (BC+ with specific flag)
198    pub blend_map_overrides: Option<M2Array<u32>>,
199    /// Texture combiner combos (added in Cataclysm)
200    pub texture_combiner_combos: Option<M2Array<u32>>,
201
202    // Fields added in Legion
203    /// Texture transforms
204    pub texture_transforms: Option<M2Array<u32>>,
205}
206
207impl M2Header {
208    /// Parse the M2 header from a reader
209    pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
210        // Read and check magic
211        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        // Read version
222        let version = reader.read_u32_le()?;
223
224        // Check if version is supported
225        if M2Version::from_header_version(version).is_none() {
226            return Err(M2Error::UnsupportedVersion(version.to_string()));
227        }
228
229        // Read the common fields present in all versions
230        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        // Vanilla/TBC versions have playable animation lookup
238        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        // Views field changes between versions
250        let (views, num_skin_profiles) = if version <= 263 {
251            // BC and earlier: views is M2Array
252            (M2Array::parse(reader)?, None)
253        } else {
254            // WotLK+: views becomes a count (num_skin_profiles)
255            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        // Texture flipbooks only exist in BC and earlier
266        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        // Workaround: Some M2 files have corrupted texture_animation_lookup fields
283        // This may be due to field alignment differences across versions or file corruption
284        // If the count is extremely large, treat as empty to prevent crashes
285        if texture_animation_lookup.count > 1_000_000 {
286            texture_animation_lookup = M2Array::new(0, 0);
287        }
288
289        // Read bounding box
290        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        // Read collision box
304        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        // Version-specific fields
332        let m2_version = M2Version::from_header_version(version).unwrap();
333
334        // Blend map overrides (BC+ with specific flag)
335        let blend_map_overrides = if version >= 260 && (flags.bits() & 0x8000000 != 0) {
336            // USE_BLEND_MAP_OVERRIDES flag - using hex value as we don't have the flag defined
337            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    /// Write the M2 header to a writer
405    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
406        // Write magic and version
407        writer.write_all(&self.magic)?;
408        writer.write_u32_le(self.version)?;
409
410        // Write common fields
411        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        // Vanilla/TBC versions have playable animation lookup
419        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        // Views field changes between versions
431        if self.version <= 263 {
432            // BC and earlier: views is M2Array
433            self.views.write(writer)?;
434        } else {
435            // WotLK+: write num_skin_profiles as u32
436            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        // Texture flipbooks only exist in BC and earlier
447        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        // Write bounding box
464        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        // Write collision box
475        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        // Version-specific fields
500        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    /// Get the version of the M2 file
516    pub fn version(&self) -> Option<M2Version> {
517        M2Version::from_header_version(self.version)
518    }
519
520    /// Create a new M2 header for a specific version
521    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        // Version-specific fields
537        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    /// Convert this header to a different version
602    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        // Handle version-specific fields
617        if target_version >= M2Version::Cataclysm && source_version < M2Version::Cataclysm {
618            // Add texture_combiner_combos when upgrading to Cataclysm or later
619            new_header.texture_combiner_combos = Some(M2Array::new(0, 0));
620        } else if target_version < M2Version::Cataclysm && source_version >= M2Version::Cataclysm {
621            // Remove texture_combiner_combos when downgrading to pre-Cataclysm
622            new_header.texture_combiner_combos = None;
623        }
624
625        if target_version >= M2Version::Legion && source_version < M2Version::Legion {
626            // Add texture_transforms when upgrading to Legion or later
627            new_header.texture_transforms = Some(M2Array::new(0, 0));
628        } else if target_version < M2Version::Legion && source_version >= M2Version::Legion {
629            // Remove texture_transforms when downgrading to pre-Legion
630            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    // Helper function to create a basic test header
643    fn create_test_header(version: M2Version) -> Vec<u8> {
644        let mut data = Vec::new();
645
646        // Magic "MD20"
647        data.extend_from_slice(&M2_MAGIC);
648
649        // Version
650        data.extend_from_slice(&version.to_header_version().to_le_bytes());
651
652        // Name
653        data.extend_from_slice(&0u32.to_le_bytes()); // count = 0
654        data.extend_from_slice(&0u32.to_le_bytes()); // offset = 0
655
656        // Flags
657        data.extend_from_slice(&0u32.to_le_bytes());
658
659        // Global sequences
660        data.extend_from_slice(&0u32.to_le_bytes()); // count = 0
661        data.extend_from_slice(&0u32.to_le_bytes()); // offset = 0
662
663        // ... continue for all required fields
664        // This is simplified for brevity - in a real test, we'd populate all fields
665
666        // For brevity, let's just add enough bytes to cover the base header
667        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        // Convert Classic to Cataclysm
718        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        // Convert Cataclysm to Legion
727        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        // Convert Legion back to Classic
733        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}