wow_m2/
model.rs

1use std::fs::File;
2use std::io::{Read, Seek, SeekFrom, Write};
3
4use crate::io_ext::ReadExt;
5use std::path::Path;
6
7use crate::chunks::animation::M2Animation;
8use crate::chunks::bone::M2Bone;
9use crate::chunks::material::M2Material;
10use crate::chunks::{M2Texture, M2Vertex};
11use crate::common::{M2Array, read_array};
12use crate::error::{M2Error, Result};
13use crate::header::M2Header;
14use crate::version::M2Version;
15
16/// Main M2 model structure
17#[derive(Debug, Clone)]
18pub struct M2Model {
19    /// M2 header
20    pub header: M2Header,
21    /// Model name
22    pub name: Option<String>,
23    /// Global sequences
24    pub global_sequences: Vec<u32>,
25    /// Animations
26    pub animations: Vec<M2Animation>,
27    /// Animation lookups
28    pub animation_lookup: Vec<u16>,
29    /// Bones
30    pub bones: Vec<M2Bone>,
31    /// Key bone lookups
32    pub key_bone_lookup: Vec<u16>,
33    /// Vertices
34    pub vertices: Vec<M2Vertex>,
35    /// Textures
36    pub textures: Vec<M2Texture>,
37    /// Materials (render flags)
38    pub materials: Vec<M2Material>,
39    /// Raw data for other sections
40    /// This is used to preserve data that we don't fully parse yet
41    pub raw_data: M2RawData,
42}
43
44/// Raw data for sections that are not fully parsed
45#[derive(Debug, Clone, Default)]
46pub struct M2RawData {
47    /// Transparency data
48    pub transparency: Vec<u8>,
49    /// Texture animations
50    pub texture_animations: Vec<u8>,
51    /// Color animations
52    pub color_animations: Vec<u8>,
53    /// Render flags
54    pub render_flags: Vec<u8>,
55    /// Bone lookup table
56    pub bone_lookup_table: Vec<u16>,
57    /// Texture lookup table
58    pub texture_lookup_table: Vec<u16>,
59    /// Texture units
60    pub texture_units: Vec<u16>,
61    /// Transparency lookup table
62    pub transparency_lookup_table: Vec<u16>,
63    /// Texture animation lookup
64    pub texture_animation_lookup: Vec<u16>,
65    /// Bounding triangles
66    pub bounding_triangles: Vec<u8>,
67    /// Bounding vertices
68    pub bounding_vertices: Vec<u8>,
69    /// Bounding normals
70    pub bounding_normals: Vec<u8>,
71    /// Attachments
72    pub attachments: Vec<u8>,
73    /// Attachment lookup table
74    pub attachment_lookup_table: Vec<u16>,
75    /// Events
76    pub events: Vec<u8>,
77    /// Lights
78    pub lights: Vec<u8>,
79    /// Cameras
80    pub cameras: Vec<u8>,
81    /// Camera lookup table
82    pub camera_lookup_table: Vec<u16>,
83    /// Ribbon emitters
84    pub ribbon_emitters: Vec<u8>,
85    /// Particle emitters
86    pub particle_emitters: Vec<u8>,
87    /// Texture combiner combos (added in Cataclysm)
88    pub texture_combiner_combos: Option<Vec<u8>>,
89}
90
91impl M2Model {
92    /// Parse an M2 model from a reader
93    pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
94        // Parse the header first
95        let header = M2Header::parse(reader)?;
96
97        // Get the version
98        let _version = header
99            .version()
100            .ok_or(M2Error::UnsupportedVersion(header.version.to_string()))?;
101
102        // Parse the name
103        let name = if header.name.count > 0 {
104            // Seek to the name
105            reader.seek(SeekFrom::Start(header.name.offset as u64))?;
106
107            // Read the name (null-terminated string)
108            let name_bytes = read_array(reader, &header.name, |r| Ok(r.read_u8()?))?;
109
110            // Convert to string, stopping at null terminator
111            let name_end = name_bytes
112                .iter()
113                .position(|&b| b == 0)
114                .unwrap_or(name_bytes.len());
115            let name_str = String::from_utf8_lossy(&name_bytes[..name_end]).to_string();
116            Some(name_str)
117        } else {
118            None
119        };
120
121        // Parse global sequences
122        let global_sequences =
123            read_array(reader, &header.global_sequences, |r| Ok(r.read_u32_le()?))?;
124
125        // Parse animations
126        let animations = read_array(reader, &header.animations.convert(), |r| {
127            M2Animation::parse(r, header.version)
128        })?;
129
130        // Parse animation lookups
131        let animation_lookup =
132            read_array(reader, &header.animation_lookup, |r| Ok(r.read_u16_le()?))?;
133
134        // Parse bones
135        // Special handling for BC item files with 203 bones
136        let bones = if header.version == 260 && header.bones.count == 203 {
137            // Check if this might be an item file with bone indices instead of bone structures
138            let current_pos = reader.stream_position()?;
139            let file_size = reader.seek(SeekFrom::End(0))?;
140            reader.seek(SeekFrom::Start(current_pos))?; // Restore position
141
142            let bone_size = 92; // BC bone size
143            let expected_end = header.bones.offset as u64 + (header.bones.count as u64 * bone_size);
144
145            if expected_end > file_size {
146                // File is too small to contain 203 bone structures
147                // This is likely a BC item file where "bones" is actually a bone lookup table
148
149                // Skip the bone lookup table for now - we'll handle it differently
150                Vec::new()
151            } else {
152                // File is large enough, parse normally
153                read_array(reader, &header.bones.convert(), |r| {
154                    M2Bone::parse(r, header.version)
155                })?
156            }
157        } else {
158            // Normal bone parsing for other versions
159            read_array(reader, &header.bones.convert(), |r| {
160                M2Bone::parse(r, header.version)
161            })?
162        };
163
164        // Parse key bone lookups
165        let key_bone_lookup =
166            read_array(reader, &header.key_bone_lookup, |r| Ok(r.read_u16_le()?))?;
167
168        // Parse vertices
169        let vertices = read_array(reader, &header.vertices.convert(), |r| {
170            M2Vertex::parse(r, header.version)
171        })?;
172
173        // Parse textures
174        let textures = read_array(reader, &header.textures.convert(), |r| {
175            M2Texture::parse(r, header.version)
176        })?;
177
178        // Parse materials (render flags)
179        let materials = read_array(reader, &header.render_flags.convert(), |r| {
180            M2Material::parse(r, header.version)
181        })?;
182
183        // Parse raw data for other sections
184        // These are sections we won't fully parse yet but want to preserve
185        let mut raw_data = M2RawData::default();
186
187        // Read transparency animations data
188        if header.transparency_animations.count > 0 {
189            reader.seek(SeekFrom::Start(
190                header.transparency_animations.offset as u64,
191            ))?;
192            let mut transparency = vec![
193                0u8;
194                header.transparency_animations.count as usize
195                    * std::mem::size_of::<u32>()
196            ];
197            reader.read_exact(&mut transparency)?;
198            raw_data.transparency = transparency;
199        }
200
201        // Read transparency lookup table
202        raw_data.transparency_lookup_table =
203            read_array(reader, &header.transparency_lookup_table, |r| {
204                Ok(r.read_u16_le()?)
205            })?;
206
207        // Read texture animation lookup
208        raw_data.texture_animation_lookup =
209            read_array(reader, &header.texture_animation_lookup, |r| {
210                Ok(r.read_u16_le()?)
211            })?;
212
213        // Read bone lookup table
214        raw_data.bone_lookup_table =
215            read_array(reader, &header.bone_lookup_table, |r| Ok(r.read_u16_le()?))?;
216
217        // Read texture lookup table
218        raw_data.texture_lookup_table = read_array(reader, &header.texture_lookup_table, |r| {
219            Ok(r.read_u16_le()?)
220        })?;
221
222        // Read texture units
223        raw_data.texture_units =
224            read_array(reader, &header.texture_units, |r| Ok(r.read_u16_le()?))?;
225
226        // Read camera lookup table
227        raw_data.camera_lookup_table =
228            read_array(
229                reader,
230                &header.camera_lookup_table,
231                |r| Ok(r.read_u16_le()?),
232            )?;
233
234        Ok(Self {
235            header,
236            name,
237            global_sequences,
238            animations,
239            animation_lookup,
240            bones,
241            key_bone_lookup,
242            vertices,
243            textures,
244            materials,
245            raw_data,
246        })
247    }
248
249    /// Load an M2 model from a file
250    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
251        let mut file = File::open(path)?;
252        Self::parse(&mut file)
253    }
254
255    /// Save an M2 model to a file
256    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
257        let mut file = File::create(path)?;
258        self.write(&mut file)
259    }
260
261    /// Write an M2 model to a writer
262    pub fn write<W: Write + Seek>(&self, writer: &mut W) -> Result<()> {
263        // We need to recalculate all offsets and build the file in memory
264        let mut data_section = Vec::new();
265        let mut header = self.header.clone();
266
267        // Start with header size (will be written last)
268        let header_size = self.calculate_header_size();
269        let mut current_offset = header_size as u32;
270
271        // Write name
272        if let Some(ref name) = self.name {
273            let name_bytes = name.as_bytes();
274            let name_len = name_bytes.len() as u32 + 1; // +1 for null terminator
275            header.name = M2Array::new(name_len, current_offset);
276
277            data_section.extend_from_slice(name_bytes);
278            data_section.push(0); // Null terminator
279            current_offset += name_len;
280        } else {
281            header.name = M2Array::new(0, 0);
282        }
283
284        // Write global sequences
285        if !self.global_sequences.is_empty() {
286            header.global_sequences =
287                M2Array::new(self.global_sequences.len() as u32, current_offset);
288
289            for &seq in &self.global_sequences {
290                data_section.extend_from_slice(&seq.to_le_bytes());
291            }
292
293            current_offset += (self.global_sequences.len() * std::mem::size_of::<u32>()) as u32;
294        } else {
295            header.global_sequences = M2Array::new(0, 0);
296        }
297
298        // Write animations
299        if !self.animations.is_empty() {
300            header.animations = M2Array::new(self.animations.len() as u32, current_offset);
301
302            for anim in &self.animations {
303                // For each animation, write its data
304                let mut anim_data = Vec::new();
305                anim.write(&mut anim_data, header.version)?;
306                data_section.extend_from_slice(&anim_data);
307            }
308
309            // Animation size depends on version: 24 bytes for Classic, 52 bytes for BC+
310            let anim_size = if header.version <= 256 { 24 } else { 52 };
311            current_offset += (self.animations.len() * anim_size) as u32;
312        } else {
313            header.animations = M2Array::new(0, 0);
314        }
315
316        // Write animation lookups
317        if !self.animation_lookup.is_empty() {
318            header.animation_lookup =
319                M2Array::new(self.animation_lookup.len() as u32, current_offset);
320
321            for &lookup in &self.animation_lookup {
322                data_section.extend_from_slice(&lookup.to_le_bytes());
323            }
324
325            current_offset += (self.animation_lookup.len() * std::mem::size_of::<u16>()) as u32;
326        } else {
327            header.animation_lookup = M2Array::new(0, 0);
328        }
329
330        // Write bones
331        if !self.bones.is_empty() {
332            header.bones = M2Array::new(self.bones.len() as u32, current_offset);
333
334            for bone in &self.bones {
335                let mut bone_data = Vec::new();
336                bone.write(&mut bone_data, self.header.version)?;
337                data_section.extend_from_slice(&bone_data);
338            }
339
340            // Bone size is 92 bytes for all versions we support
341            let bone_size = 92;
342            current_offset += (self.bones.len() * bone_size) as u32;
343        } else {
344            header.bones = M2Array::new(0, 0);
345        }
346
347        // Write key bone lookups
348        if !self.key_bone_lookup.is_empty() {
349            header.key_bone_lookup =
350                M2Array::new(self.key_bone_lookup.len() as u32, current_offset);
351
352            for &lookup in &self.key_bone_lookup {
353                data_section.extend_from_slice(&lookup.to_le_bytes());
354            }
355
356            current_offset += (self.key_bone_lookup.len() * std::mem::size_of::<u16>()) as u32;
357        } else {
358            header.key_bone_lookup = M2Array::new(0, 0);
359        }
360
361        // Write vertices
362        if !self.vertices.is_empty() {
363            header.vertices = M2Array::new(self.vertices.len() as u32, current_offset);
364
365            let vertex_size =
366                if self.header.version().unwrap_or(M2Version::Classic) >= M2Version::Cataclysm {
367                    // Cataclysm and later have secondary texture coordinates
368                    44
369                } else {
370                    // Pre-Cataclysm don't have secondary texture coordinates
371                    36
372                };
373
374            for vertex in &self.vertices {
375                let mut vertex_data = Vec::new();
376                vertex.write(&mut vertex_data, self.header.version)?;
377                data_section.extend_from_slice(&vertex_data);
378            }
379
380            current_offset += (self.vertices.len() * vertex_size) as u32;
381        } else {
382            header.vertices = M2Array::new(0, 0);
383        }
384
385        // Write textures
386        if !self.textures.is_empty() {
387            header.textures = M2Array::new(self.textures.len() as u32, current_offset);
388
389            // First, we need to write the texture definitions
390            let mut texture_name_offsets = Vec::new();
391            let texture_def_size = 16; // Each texture definition is 16 bytes
392
393            for texture in &self.textures {
394                // Save the current offset for this texture's filename
395                texture_name_offsets
396                    .push(current_offset + (self.textures.len() * texture_def_size) as u32);
397
398                // Write the texture definition (without the actual filename)
399                let mut texture_def = Vec::new();
400
401                // Write texture type
402                texture_def.extend_from_slice(&(texture.texture_type as u32).to_le_bytes());
403
404                // Write flags
405                texture_def.extend_from_slice(&texture.flags.bits().to_le_bytes());
406
407                // Write filename offset and length (will be filled in later)
408                texture_def.extend_from_slice(&0u32.to_le_bytes()); // Count
409                texture_def.extend_from_slice(&0u32.to_le_bytes()); // Offset
410
411                data_section.extend_from_slice(&texture_def);
412            }
413
414            // Now write the filenames
415            current_offset += (self.textures.len() * texture_def_size) as u32;
416
417            // For each texture, update the offset in the definition and write the filename
418            for (i, texture) in self.textures.iter().enumerate() {
419                // Get the filename
420                let filename_offset = texture.filename.array.offset as usize;
421                let filename_len = texture.filename.array.count as usize;
422                // Not every texture has a filename (some are hardcoded)
423                if filename_offset == 0 || filename_len == 0 {
424                    continue;
425                }
426
427                // Calculate the offset in the data section where this texture's definition was written
428                // The texture definitions start at (header.textures.offset - base_data_offset)
429                let base_data_offset = std::mem::size_of::<M2Header>();
430                let def_offset_in_data = (header.textures.offset as usize - base_data_offset)
431                    + (i * texture_def_size)
432                    + 8;
433
434                // Update the count and offset for the filename
435                data_section[def_offset_in_data..def_offset_in_data + 4]
436                    .copy_from_slice(&(filename_len as u32).to_le_bytes());
437                data_section[def_offset_in_data + 4..def_offset_in_data + 8]
438                    .copy_from_slice(&current_offset.to_le_bytes());
439
440                // Write the filename
441                data_section.extend_from_slice(&texture.filename.string.data);
442                data_section.push(0); // Null terminator
443
444                current_offset += filename_len as u32;
445            }
446        } else {
447            header.textures = M2Array::new(0, 0);
448        }
449
450        // Write materials (render flags)
451        if !self.materials.is_empty() {
452            header.render_flags = M2Array::new(self.materials.len() as u32, current_offset);
453
454            for material in &self.materials {
455                let mut material_data = Vec::new();
456                material.write(&mut material_data, self.header.version)?;
457                data_section.extend_from_slice(&material_data);
458            }
459
460            let material_size = match self.header.version().unwrap_or(M2Version::Classic) {
461                v if v >= M2Version::WoD => 18, // WoD and later have color animation lookup
462                v if v >= M2Version::Cataclysm => 16, // Cataclysm and later have shader ID and secondary texture unit
463                _ => 12,                              // Classic to WotLK
464            };
465
466            current_offset += (self.materials.len() * material_size) as u32;
467        } else {
468            header.render_flags = M2Array::new(0, 0);
469        }
470
471        // Write bone lookup table
472        if !self.raw_data.bone_lookup_table.is_empty() {
473            header.bone_lookup_table =
474                M2Array::new(self.raw_data.bone_lookup_table.len() as u32, current_offset);
475
476            for &lookup in &self.raw_data.bone_lookup_table {
477                data_section.extend_from_slice(&lookup.to_le_bytes());
478            }
479
480            current_offset +=
481                (self.raw_data.bone_lookup_table.len() * std::mem::size_of::<u16>()) as u32;
482        } else {
483            header.bone_lookup_table = M2Array::new(0, 0);
484        }
485
486        // Write texture lookup table
487        if !self.raw_data.texture_lookup_table.is_empty() {
488            header.texture_lookup_table = M2Array::new(
489                self.raw_data.texture_lookup_table.len() as u32,
490                current_offset,
491            );
492
493            for &lookup in &self.raw_data.texture_lookup_table {
494                data_section.extend_from_slice(&lookup.to_le_bytes());
495            }
496
497            current_offset +=
498                (self.raw_data.texture_lookup_table.len() * std::mem::size_of::<u16>()) as u32;
499        } else {
500            header.texture_lookup_table = M2Array::new(0, 0);
501        }
502
503        // Write texture units
504        if !self.raw_data.texture_units.is_empty() {
505            header.texture_units =
506                M2Array::new(self.raw_data.texture_units.len() as u32, current_offset);
507
508            for &unit in &self.raw_data.texture_units {
509                data_section.extend_from_slice(&unit.to_le_bytes());
510            }
511
512            current_offset +=
513                (self.raw_data.texture_units.len() * std::mem::size_of::<u16>()) as u32;
514        } else {
515            header.texture_units = M2Array::new(0, 0);
516        }
517
518        // Write transparency lookup table
519        if !self.raw_data.transparency_lookup_table.is_empty() {
520            header.transparency_lookup_table = M2Array::new(
521                self.raw_data.transparency_lookup_table.len() as u32,
522                current_offset,
523            );
524
525            for &lookup in &self.raw_data.transparency_lookup_table {
526                data_section.extend_from_slice(&lookup.to_le_bytes());
527            }
528
529            current_offset +=
530                (self.raw_data.transparency_lookup_table.len() * std::mem::size_of::<u16>()) as u32;
531        } else {
532            header.transparency_lookup_table = M2Array::new(0, 0);
533        }
534
535        // Write texture animation lookup
536        if !self.raw_data.texture_animation_lookup.is_empty() {
537            header.texture_animation_lookup = M2Array::new(
538                self.raw_data.texture_animation_lookup.len() as u32,
539                current_offset,
540            );
541
542            for &lookup in &self.raw_data.texture_animation_lookup {
543                data_section.extend_from_slice(&lookup.to_le_bytes());
544            }
545
546            // current_offset +=
547            //     (self.raw_data.texture_animation_lookup.len() * std::mem::size_of::<u16>()) as u32;
548        } else {
549            header.texture_animation_lookup = M2Array::new(0, 0);
550        }
551
552        // Finally, write the header followed by the data section
553        header.write(writer)?;
554        writer.write_all(&data_section)?;
555
556        Ok(())
557    }
558
559    /// Convert this model to a different version
560    pub fn convert(&self, target_version: M2Version) -> Result<Self> {
561        let source_version = self.header.version().ok_or(M2Error::ConversionError {
562            from: self.header.version,
563            to: target_version.to_header_version(),
564            reason: "Unknown source version".to_string(),
565        })?;
566
567        if source_version == target_version {
568            return Ok(self.clone());
569        }
570
571        // Convert the header
572        let header = self.header.convert(target_version)?;
573
574        // Convert vertices
575        let vertices = self
576            .vertices
577            .iter()
578            .map(|v| v.convert(target_version))
579            .collect();
580
581        // Convert textures
582        let textures = self
583            .textures
584            .iter()
585            .map(|t| t.convert(target_version))
586            .collect();
587
588        // Convert bones
589        let bones = self
590            .bones
591            .iter()
592            .map(|b| b.convert(target_version))
593            .collect();
594
595        // Convert materials
596        let materials = self
597            .materials
598            .iter()
599            .map(|m| m.convert(target_version))
600            .collect();
601
602        // Create the new model
603        let mut new_model = self.clone();
604        new_model.header = header;
605        new_model.vertices = vertices;
606        new_model.textures = textures;
607        new_model.bones = bones;
608        new_model.materials = materials;
609
610        Ok(new_model)
611    }
612
613    /// Calculate the size of the header for this model version
614    fn calculate_header_size(&self) -> usize {
615        let version = self.header.version().unwrap_or(M2Version::Classic);
616
617        let mut size = 4 + 4; // Magic + version
618
619        // Common fields
620        size += 2 * 4; // name
621        size += 4; // flags
622
623        size += 2 * 4; // global_sequences
624        size += 2 * 4; // animations
625        size += 2 * 4; // animation_lookup
626
627        size += 2 * 4; // bones
628        size += 2 * 4; // key_bone_lookup
629
630        size += 2 * 4; // vertices
631        size += 2 * 4; // views
632
633        size += 2 * 4; // color_animations
634
635        size += 2 * 4; // textures
636        size += 2 * 4; // transparency_lookup
637        size += 2 * 4; // transparency_animations
638        size += 2 * 4; // texture_animations
639
640        size += 2 * 4; // color_replacements
641        size += 2 * 4; // render_flags
642        size += 2 * 4; // bone_lookup_table
643        size += 2 * 4; // texture_lookup_table
644        size += 2 * 4; // texture_units
645        size += 2 * 4; // transparency_lookup_table
646        size += 2 * 4; // texture_animation_lookup
647
648        size += 3 * 4; // bounding_box_min
649        size += 3 * 4; // bounding_box_max
650        size += 4; // bounding_sphere_radius
651
652        size += 3 * 4; // collision_box_min
653        size += 3 * 4; // collision_box_max
654        size += 4; // collision_sphere_radius
655
656        size += 2 * 4; // bounding_triangles
657        size += 2 * 4; // bounding_vertices
658        size += 2 * 4; // bounding_normals
659
660        size += 2 * 4; // attachments
661        size += 2 * 4; // attachment_lookup_table
662        size += 2 * 4; // events
663        size += 2 * 4; // lights
664        size += 2 * 4; // cameras
665        size += 2 * 4; // camera_lookup_table
666
667        size += 2 * 4; // ribbon_emitters
668        size += 2 * 4; // particle_emitters
669
670        // Version-specific fields
671        if version >= M2Version::Cataclysm {
672            size += 2 * 4; // texture_combiner_combos
673        }
674
675        if version >= M2Version::Legion {
676            size += 2 * 4; // texture_transforms
677        }
678
679        size
680    }
681
682    /// Validate the model structure
683    pub fn validate(&self) -> Result<()> {
684        // Check if the version is supported
685        if self.header.version().is_none() {
686            return Err(M2Error::UnsupportedVersion(self.header.version.to_string()));
687        }
688
689        // Validate vertices
690        if self.vertices.is_empty() {
691            return Err(M2Error::ValidationError(
692                "Model has no vertices".to_string(),
693            ));
694        }
695
696        // Validate bones
697        for (i, bone) in self.bones.iter().enumerate() {
698            // Check if parent bone is valid
699            if bone.parent_bone >= 0 && bone.parent_bone as usize >= self.bones.len() {
700                return Err(M2Error::ValidationError(format!(
701                    "Bone {} has invalid parent bone {}",
702                    i, bone.parent_bone
703                )));
704            }
705        }
706
707        // Validate textures
708        for (i, texture) in self.textures.iter().enumerate() {
709            // Check if the texture has a valid filename
710            if texture.filename.array.count > 0 && texture.filename.array.offset == 0 {
711                return Err(M2Error::ValidationError(format!(
712                    "Texture {i} has invalid filename offset"
713                )));
714            }
715        }
716
717        // Validate materials (simplified - just check basic structure)
718        for (i, _material) in self.materials.iter().enumerate() {
719            // Materials now only contain render flags and blend modes
720            // No direct texture references to validate here
721            let _material_index = i; // Just to acknowledge we're iterating
722        }
723
724        Ok(())
725    }
726}