Skip to main content

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        // Texture combiner combos (when USE_TEXTURE_COMBINERS flag is set)
347        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    /// Write the M2 header to a writer
409    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
410        // Write magic and version
411        writer.write_all(&self.magic)?;
412        writer.write_u32_le(self.version)?;
413
414        // Write common fields
415        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        // Vanilla/TBC versions have playable animation lookup
423        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        // Views field changes between versions
435        if self.version <= 263 {
436            // BC and earlier: views is M2Array
437            self.views.write(writer)?;
438        } else {
439            // WotLK+: write num_skin_profiles as u32
440            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        // Texture flipbooks only exist in BC and earlier
450        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        // Write bounding box
467        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        // Write collision box
478        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        // Version-specific fields
503        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    /// Get the version of the M2 file
519    pub fn version(&self) -> Option<M2Version> {
520        M2Version::from_header_version(self.version)
521    }
522
523    /// Create a new M2 header for a specific version
524    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        // Version-specific fields
536        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    /// Convert this header to a different version
600    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        // Handle views <-> num_skin_profiles transition at WotLK boundary
615        // Pre-WotLK (version <= 263): views is M2Array containing embedded skin data
616        // WotLK+ (version >= 264): num_skin_profiles is a u32 count of external .skin files
617        if target_version >= M2Version::WotLK && source_version < M2Version::WotLK {
618            // Upgrading to WotLK+: convert views M2Array count to num_skin_profiles
619            // Use the views count as the number of skin profiles (or 1 if views was empty)
620            let count = if self.views.count > 0 {
621                self.views.count
622            } else {
623                1 // Default to 1 skin profile
624            };
625            new_header.num_skin_profiles = Some(count);
626            new_header.views = M2Array::new(0, 0); // Clear views array
627        } else if target_version < M2Version::WotLK && source_version >= M2Version::WotLK {
628            // Downgrading from WotLK+: convert num_skin_profiles to views M2Array
629            // The views M2Array would normally point to embedded skin data, but we can't
630            // recreate that, so we set it to empty (skin data would need separate handling)
631            new_header.num_skin_profiles = None;
632            new_header.views = M2Array::new(0, 0);
633        }
634
635        // Handle playable_animation_lookup (removed in WotLK+)
636        if target_version >= M2Version::WotLK && source_version < M2Version::WotLK {
637            // Remove playable_animation_lookup when upgrading to WotLK+
638            new_header.playable_animation_lookup = None;
639        } else if target_version < M2Version::WotLK && source_version >= M2Version::WotLK {
640            // Add empty playable_animation_lookup when downgrading to pre-WotLK
641            new_header.playable_animation_lookup = Some(M2Array::new(0, 0));
642        }
643
644        // Handle texture_flipbooks (removed in WotLK+)
645        if target_version >= M2Version::WotLK && source_version < M2Version::WotLK {
646            // Remove texture_flipbooks when upgrading to WotLK+
647            new_header.texture_flipbooks = None;
648        } else if target_version < M2Version::WotLK && source_version >= M2Version::WotLK {
649            // Add empty texture_flipbooks when downgrading to pre-WotLK
650            new_header.texture_flipbooks = Some(M2Array::new(0, 0));
651        }
652
653        // Handle texture_combiner_combos (controlled by USE_TEXTURE_COMBINERS flag)
654        // The flag determines presence of this field, not version
655        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        // Handle texture_transforms (added in Legion)
663        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    // Helper function to create a basic test header
679    fn create_test_header(version: M2Version, flags: M2ModelFlags) -> Vec<u8> {
680        let mut data = Vec::new();
681
682        // Magic "MD20"
683        data.extend_from_slice(&M2_MAGIC_LEGACY);
684
685        // Version
686        data.extend_from_slice(&version.to_header_version().to_le_bytes());
687
688        // Name
689        data.extend_from_slice(&0u32.to_le_bytes()); // count = 0
690        data.extend_from_slice(&0u32.to_le_bytes()); // offset = 0
691
692        // Flags
693        data.extend_from_slice(&flags.bits().to_le_bytes());
694
695        // Global sequences
696        data.extend_from_slice(&0u32.to_le_bytes()); // count = 0
697        data.extend_from_slice(&0u32.to_le_bytes()); // offset = 0
698
699        // ... continue for all required fields
700        // This is simplified for brevity - in a real test, we'd populate all fields
701
702        // For brevity, let's just add enough bytes to cover the base header
703        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        // Set USE_TEXTURE_COMBINERS flag for test
753        classic_header.flags = M2ModelFlags::USE_TEXTURE_COMBINERS;
754
755        // Convert Classic to Cataclysm
756        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        // Convert Cataclysm to Legion
765        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        // Convert Legion back to Classic
771        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}