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 legacy M2 files ("MD20")
10pub const M2_MAGIC_LEGACY: [u8; 4] = *b"MD20";
11
12/// Magic signature for chunked M2 files ("MD21")
13pub const M2_MAGIC_CHUNKED: [u8; 4] = *b"MD21";
14
15/// Legacy magic signature for compatibility ("MD20")
16pub const M2_MAGIC: [u8; 4] = M2_MAGIC_LEGACY;
17
18bitflags! {
19    /// Model flags as defined in the M2 format
20    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
21    pub struct M2ModelFlags: u32 {
22        /// Tilt on X axis
23        const TILT_X = 0x0001;
24        /// Tilt on Y axis
25        const TILT_Y = 0x0002;
26        /// Add a back-reference to the model
27        const ADD_BACK_REFERENCE = 0x0004;
28        /// Use texture combiners
29        const USE_TEXTURE_COMBINERS = 0x0008;
30        /// Is it a camera?
31        const IS_CAMERA = 0x0010;
32        /// Unused flag
33        const UNUSED = 0x0020;
34        /// No particle trails
35        const NO_PARTICLE_TRAILS = 0x0040;
36        /// Unknown
37        const UNKNOWN_0x80 = 0x0080;
38        /// Load phys data
39        const LOAD_PHYS_DATA = 0x0100;
40        /// Unknown
41        const UNKNOWN_0x200 = 0x0200;
42        /// Has bones
43        const HAS_BONES = 0x0400;
44        /// Unused 0x800
45        const UNUSED_0x800 = 0x0800;
46        /// Unknown
47        const UNKNOWN_0x1000 = 0x1000;
48        /// Use texture IDs
49        const USE_TEXTURE_IDS = 0x2000;
50        /// Camera can be modified
51        const CAMERA_MODIFIABLE = 0x4000;
52        /// New particle system
53        const NEW_PARTICLE_SYSTEM = 0x8000;
54        /// Unknown
55        const UNKNOWN_0x10000 = 0x10000;
56        /// Unknown
57        const UNKNOWN_0x20000 = 0x20000;
58        /// Unknown
59        const UNKNOWN_0x40000 = 0x40000;
60        /// Unknown
61        const UNKNOWN_0x80000 = 0x80000;
62        /// Unknown
63        const UNKNOWN_0x100000 = 0x100000;
64        /// Unknown
65        const UNKNOWN_0x200000 = 0x200000;
66        /// Unknown
67        const UNKNOWN_0x400000 = 0x400000;
68        /// Unknown
69        const UNKNOWN_0x800000 = 0x800000;
70        /// Unknown
71        const UNKNOWN_0x1000000 = 0x1000000;
72        /// Unknown
73        const UNKNOWN_0x2000000 = 0x2000000;
74        /// Unknown
75        const UNKNOWN_0x4000000 = 0x4000000;
76        /// Unknown
77        const UNKNOWN_0x8000000 = 0x8000000;
78        /// Unknown
79        const UNKNOWN_0x10000000 = 0x10000000;
80        /// Unknown
81        const UNKNOWN_0x20000000 = 0x20000000;
82        /// Unknown
83        const UNKNOWN_0x40000000 = 0x40000000;
84        /// Unknown
85        const UNKNOWN_0x80000000 = 0x80000000;
86    }
87}
88
89/// M2 model header structure
90/// Based on: <https://wowdev.wiki/M2#Header>
91#[derive(Debug, Clone)]
92pub struct M2Header {
93    /// Magic signature ("MD20")
94    pub magic: [u8; 4],
95    /// Version of the M2 file
96    pub version: u32,
97    /// Name of the model
98    pub name: M2Array<u8>,
99    /// Flags
100    pub flags: M2ModelFlags,
101
102    // Sequence-related fields
103    /// Global sequences
104    pub global_sequences: M2Array<u32>,
105    /// Animations
106    pub animations: M2Array<u32>,
107    /// Animation lookups (C in Classic)
108    pub animation_lookup: M2Array<u16>,
109    /// Playable animation lookup - only present in versions <= 263
110    pub playable_animation_lookup: Option<M2Array<u16>>,
111
112    // Bone-related fields
113    /// Bones
114    pub bones: M2Array<u32>,
115    /// Key bone lookup
116    pub key_bone_lookup: M2Array<u16>,
117
118    // Geometry data
119    /// Vertices
120    pub vertices: M2Array<u32>,
121    /// Views (LOD levels) - M2Array for BC and earlier, count for later versions
122    pub views: M2Array<u32>,
123    /// Number of skin profiles for WotLK+ (when views becomes a count)
124    pub num_skin_profiles: Option<u32>,
125
126    // Color data
127    /// Color animations
128    pub color_animations: M2Array<u32>,
129
130    // Texture-related fields
131    /// Textures
132    pub textures: M2Array<u32>,
133    /// Transparency lookups
134    pub transparency_lookup: M2Array<u16>,
135    /// Texture flipbooks - only present in BC and earlier
136    pub texture_flipbooks: Option<M2Array<u32>>,
137    /// Texture animations
138    pub texture_animations: M2Array<u32>,
139
140    // Material data
141    /// Color replacements
142    pub color_replacements: M2Array<u32>,
143    /// Render flags
144    pub render_flags: M2Array<u32>,
145    /// Bone lookup table
146    pub bone_lookup_table: M2Array<u16>,
147    /// Texture lookup table
148    pub texture_lookup_table: M2Array<u16>,
149    /// Texture units
150    pub texture_units: M2Array<u16>,
151    /// Transparency lookup table
152    pub transparency_lookup_table: M2Array<u16>,
153    /// Texture animation lookup table
154    pub texture_animation_lookup: M2Array<u16>,
155
156    // Bounding box data
157    /// Bounding box min corner
158    pub bounding_box_min: [f32; 3],
159    /// Bounding box max corner
160    pub bounding_box_max: [f32; 3],
161    /// Bounding sphere radius
162    pub bounding_sphere_radius: f32,
163    /// Collision bounding box min corner
164    pub collision_box_min: [f32; 3],
165    /// Collision bounding box max corner
166    pub collision_box_max: [f32; 3],
167    /// Collision bounding sphere radius
168    pub collision_sphere_radius: f32,
169
170    // Additional geometry data
171    /// Bounding triangles
172    pub bounding_triangles: M2Array<u32>,
173    /// Bounding vertices
174    pub bounding_vertices: M2Array<u32>,
175    /// Bounding normals
176    pub bounding_normals: M2Array<u32>,
177
178    // Attachments and events
179    /// Attachments
180    pub attachments: M2Array<u32>,
181    /// Attachment lookup table
182    pub attachment_lookup_table: M2Array<u16>,
183    /// Events
184    pub events: M2Array<u32>,
185    /// Lights
186    pub lights: M2Array<u32>,
187    /// Cameras
188    pub cameras: M2Array<u32>,
189    /// Camera lookup table
190    pub camera_lookup_table: M2Array<u16>,
191
192    // Ribbon emitters
193    /// Ribbon emitters
194    pub ribbon_emitters: M2Array<u32>,
195
196    // Particle systems
197    /// Particle emitters
198    pub particle_emitters: M2Array<u32>,
199
200    // Additional fields for newer versions
201    /// Blend map overrides (BC+ with specific flag)
202    pub blend_map_overrides: Option<M2Array<u32>>,
203    /// Texture combiner combos (added in Cataclysm)
204    pub texture_combiner_combos: Option<M2Array<u32>>,
205
206    // Fields added in Legion
207    /// Texture transforms
208    pub texture_transforms: Option<M2Array<u32>>,
209}
210
211impl M2Header {
212    /// Parse the M2 header from a reader
213    pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
214        // Read and check magic
215        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        // Read version
226        let version = reader.read_u32_le()?;
227
228        // Check if version is supported
229        if M2Version::from_header_version(version).is_none() {
230            return Err(M2Error::UnsupportedVersion(version.to_string()));
231        }
232
233        // Read the common fields present in all versions
234        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        // FIXED: Vanilla (256) DOES have playable animation lookup, contrary to previous assumption
242        // This field exists in vanilla, BC, and was removed in Wrath (264+)
243        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        // Views field changes between versions
255        let (views, num_skin_profiles) = if version <= 263 {
256            // BC and earlier: views is M2Array
257            (M2Array::parse(reader)?, None)
258        } else {
259            // WotLK+: views becomes a count (num_skin_profiles)
260            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        // Texture flipbooks only exist in BC and earlier
270        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        // Workaround: Some M2 files have corrupted texture_animation_lookup fields
287        // This may be due to field alignment differences across versions or file corruption
288        // If the count is extremely large, treat as empty to prevent crashes
289        if texture_animation_lookup.count > 1_000_000 {
290            texture_animation_lookup = M2Array::new(0, 0);
291        }
292
293        // Read bounding box
294        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        // Read collision box
308        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        // Version-specific fields
336        let m2_version = M2Version::from_header_version(version).unwrap();
337
338        // Blend map overrides (BC+ with specific flag)
339        let blend_map_overrides = if version >= 260 && (flags.bits() & 0x8000000 != 0) {
340            // USE_BLEND_MAP_OVERRIDES flag - using hex value as we don't have the flag defined
341            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    /// Write the M2 header to a writer
408    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
409        // Write magic and version
410        writer.write_all(&self.magic)?;
411        writer.write_u32_le(self.version)?;
412
413        // Write common fields
414        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        // Vanilla/TBC versions have playable animation lookup
422        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        // Views field changes between versions
434        if self.version <= 263 {
435            // BC and earlier: views is M2Array
436            self.views.write(writer)?;
437        } else {
438            // WotLK+: write num_skin_profiles as u32
439            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        // Texture flipbooks only exist in BC and earlier
449        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        // Write bounding box
466        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        // Write collision box
477        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        // Version-specific fields
502        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    /// Get the version of the M2 file
518    pub fn version(&self) -> Option<M2Version> {
519        M2Version::from_header_version(self.version)
520    }
521
522    /// Create a new M2 header for a specific version
523    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        // Version-specific fields
539        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    /// Convert this header to a different version
603    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        // Handle views <-> num_skin_profiles transition at WotLK boundary
618        // Pre-WotLK (version <= 263): views is M2Array containing embedded skin data
619        // WotLK+ (version >= 264): num_skin_profiles is a u32 count of external .skin files
620        if target_version >= M2Version::WotLK && source_version < M2Version::WotLK {
621            // Upgrading to WotLK+: convert views M2Array count to num_skin_profiles
622            // Use the views count as the number of skin profiles (or 1 if views was empty)
623            let count = if self.views.count > 0 {
624                self.views.count
625            } else {
626                1 // Default to 1 skin profile
627            };
628            new_header.num_skin_profiles = Some(count);
629            new_header.views = M2Array::new(0, 0); // Clear views array
630        } else if target_version < M2Version::WotLK && source_version >= M2Version::WotLK {
631            // Downgrading from WotLK+: convert num_skin_profiles to views M2Array
632            // The views M2Array would normally point to embedded skin data, but we can't
633            // recreate that, so we set it to empty (skin data would need separate handling)
634            new_header.num_skin_profiles = None;
635            new_header.views = M2Array::new(0, 0);
636        }
637
638        // Handle playable_animation_lookup (removed in WotLK+)
639        if target_version >= M2Version::WotLK && source_version < M2Version::WotLK {
640            // Remove playable_animation_lookup when upgrading to WotLK+
641            new_header.playable_animation_lookup = None;
642        } else if target_version < M2Version::WotLK && source_version >= M2Version::WotLK {
643            // Add empty playable_animation_lookup when downgrading to pre-WotLK
644            new_header.playable_animation_lookup = Some(M2Array::new(0, 0));
645        }
646
647        // Handle texture_flipbooks (removed in WotLK+)
648        if target_version >= M2Version::WotLK && source_version < M2Version::WotLK {
649            // Remove texture_flipbooks when upgrading to WotLK+
650            new_header.texture_flipbooks = None;
651        } else if target_version < M2Version::WotLK && source_version >= M2Version::WotLK {
652            // Add empty texture_flipbooks when downgrading to pre-WotLK
653            new_header.texture_flipbooks = Some(M2Array::new(0, 0));
654        }
655
656        // Handle texture_combiner_combos (added in Cataclysm)
657        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        // Handle texture_transforms (added in Legion)
664        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    // Helper function to create a basic test header
680    fn create_test_header(version: M2Version) -> Vec<u8> {
681        let mut data = Vec::new();
682
683        // Magic "MD20"
684        data.extend_from_slice(&M2_MAGIC_LEGACY);
685
686        // Version
687        data.extend_from_slice(&version.to_header_version().to_le_bytes());
688
689        // Name
690        data.extend_from_slice(&0u32.to_le_bytes()); // count = 0
691        data.extend_from_slice(&0u32.to_le_bytes()); // offset = 0
692
693        // Flags
694        data.extend_from_slice(&0u32.to_le_bytes());
695
696        // Global sequences
697        data.extend_from_slice(&0u32.to_le_bytes()); // count = 0
698        data.extend_from_slice(&0u32.to_le_bytes()); // offset = 0
699
700        // ... continue for all required fields
701        // This is simplified for brevity - in a real test, we'd populate all fields
702
703        // For brevity, let's just add enough bytes to cover the base header
704        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        // Convert Classic to Cataclysm
755        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        // Convert Cataclysm to Legion
764        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        // Convert Legion back to Classic
770        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}