wow_m2/
embedded_skin.rs

1//! Support for parsing embedded skin data from pre-WotLK M2 models
2//!
3//! Pre-WotLK models (versions 256-260) have skin profile data embedded directly
4//! in the M2 file rather than in separate .skin files. This module provides
5//! functionality to extract and parse these embedded skin profiles.
6
7use crate::io_ext::ReadExt;
8use crate::skin::parse_embedded_skin;
9use crate::{M2Error, M2Model, Result, SkinFile};
10use std::io::{Cursor, Read, Seek, SeekFrom, Write};
11
12impl M2Model {
13    /// Parse embedded skin profiles from pre-WotLK M2 models
14    ///
15    /// For models with version <= 260, skin data is embedded in the M2 file itself.
16    /// The views array contains ModelView structures with direct offsets to skin data.
17    ///
18    /// **Note**: Many character models only have the first skin profile (index 0)
19    /// properly embedded. Additional skin profiles may contain invalid data.
20    ///
21    /// # Arguments
22    ///
23    /// * `original_m2_data` - The complete original M2 file data
24    /// * `skin_index` - Index of the skin profile to extract (0-based)
25    ///
26    /// # Returns
27    ///
28    /// Returns the parsed SkinFile for the requested skin profile index
29    ///
30    /// # Errors
31    ///
32    /// Returns an error if the skin index is out of range or contains invalid data
33    ///
34    /// # Example
35    ///
36    /// ```no_run
37    /// # use std::fs;
38    /// # use std::io::Cursor;
39    /// # use wow_m2::{M2Model, parse_m2};
40    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
41    /// // Load a pre-WotLK model
42    /// let m2_data = fs::read("HumanMale.m2")?;
43    /// let m2_format = parse_m2(&mut Cursor::new(&m2_data))?;
44    /// let model = m2_format.model();
45    ///
46    /// if model.header.version <= 260 {
47    ///     // Parse the first embedded skin profile
48    ///     let skin = model.parse_embedded_skin(&m2_data, 0)?;
49    ///     println!("Embedded skin has {} submeshes", skin.submeshes().len());
50    /// }
51    /// # Ok(())
52    /// # }
53    /// ```
54    pub fn parse_embedded_skin(
55        &self,
56        original_m2_data: &[u8],
57        skin_index: usize,
58    ) -> Result<SkinFile> {
59        // Check if this is a pre-WotLK model
60        if self.header.version > 260 {
61            return Err(M2Error::ParseError(format!(
62                "Model version {} does not have embedded skins. Use external .skin files instead.",
63                self.header.version
64            )));
65        }
66
67        // Check if views array has data
68        if self.header.views.count == 0 {
69            return Err(M2Error::ParseError(
70                "No skin profiles found in model header".to_string(),
71            ));
72        }
73
74        // Check if requested index is valid
75        if skin_index >= self.header.views.count as usize {
76            return Err(M2Error::ParseError(format!(
77                "Skin index {} out of range. Model has {} skin profiles.",
78                skin_index, self.header.views.count
79            )));
80        }
81
82        // For pre-WotLK models, the views array points to ModelView structures
83        // Based on WMVx analysis, ModelView is 44 bytes (5 M2Arrays + 1 uint32):
84        // - indices: M2Array (count + offset) = 8 bytes
85        // - triangles: M2Array (count + offset) = 8 bytes
86        // - properties: M2Array (count + offset) = 8 bytes
87        // - submeshes: M2Array (count + offset) = 8 bytes
88        // - textureUnits: M2Array (count + offset) = 8 bytes
89        // - boneCountMax: uint32 = 4 bytes
90        // Total: 44 bytes
91
92        // CRITICAL INSIGHT: Following WMVx implementation - ALL skin profiles use the SAME ModelView!
93        // WMVx only uses views[0] and ignores other skin indices. Different skin indices likely
94        // represent different LOD levels or rendering passes, not different geometry.
95        // This explains why only skin 0 worked - we should always use the first ModelView.
96
97        let model_view_size = 44; // Correct size from WMVx analysis
98        let model_view_offset = self.header.views.offset as usize; // Always use first ModelView (skin 0)
99
100        if model_view_offset + model_view_size > original_m2_data.len() {
101            return Err(M2Error::ParseError(format!(
102                "ModelView at offset {:#x} exceeds file size",
103                model_view_offset
104            )));
105        }
106
107        // Read the ModelView structure
108        let model_view_data =
109            &original_m2_data[model_view_offset..model_view_offset + model_view_size];
110
111        // Parse ModelView fields as M2Arrays (count + offset pairs)
112        // Each M2Array is 8 bytes: count (u32) + offset (u32)
113
114        // indices M2Array
115        let n_index = u32::from_le_bytes([
116            model_view_data[0],
117            model_view_data[1],
118            model_view_data[2],
119            model_view_data[3],
120        ]);
121        let ofs_index = u32::from_le_bytes([
122            model_view_data[4],
123            model_view_data[5],
124            model_view_data[6],
125            model_view_data[7],
126        ]);
127
128        // triangles M2Array
129        let n_tris = u32::from_le_bytes([
130            model_view_data[8],
131            model_view_data[9],
132            model_view_data[10],
133            model_view_data[11],
134        ]);
135        let ofs_tris = u32::from_le_bytes([
136            model_view_data[12],
137            model_view_data[13],
138            model_view_data[14],
139            model_view_data[15],
140        ]);
141
142        // properties M2Array
143        let _n_props = u32::from_le_bytes([
144            model_view_data[16],
145            model_view_data[17],
146            model_view_data[18],
147            model_view_data[19],
148        ]);
149        let _ofs_props = u32::from_le_bytes([
150            model_view_data[20],
151            model_view_data[21],
152            model_view_data[22],
153            model_view_data[23],
154        ]);
155
156        // submeshes M2Array - THIS IS WHAT WE WERE MISSING!
157        let n_sub = u32::from_le_bytes([
158            model_view_data[24],
159            model_view_data[25],
160            model_view_data[26],
161            model_view_data[27],
162        ]);
163        let ofs_sub = u32::from_le_bytes([
164            model_view_data[28],
165            model_view_data[29],
166            model_view_data[30],
167            model_view_data[31],
168        ]);
169
170        // batches M2Array
171        let n_batches = u32::from_le_bytes([
172            model_view_data[32],
173            model_view_data[33],
174            model_view_data[34],
175            model_view_data[35],
176        ]);
177        let ofs_batches = u32::from_le_bytes([
178            model_view_data[36],
179            model_view_data[37],
180            model_view_data[38],
181            model_view_data[39],
182        ]);
183
184        // boneCountMax uint32
185        let _bone_count_max = u32::from_le_bytes([
186            model_view_data[40],
187            model_view_data[41],
188            model_view_data[42],
189            model_view_data[43],
190        ]);
191
192        // Validate ModelView values (debug info removed for production use)
193
194        // Sanity check: ModelView data should be reasonable
195        // Index count should not exceed total vertices, and offsets should be within file
196        if n_index > 100000
197            || ofs_index as usize >= original_m2_data.len()
198            || n_tris > 100000
199            || ofs_tris as usize >= original_m2_data.len()
200            || n_sub > 1000 // Reasonable submesh count limit
201            || (n_sub > 0 && ofs_sub as usize >= original_m2_data.len())
202            || (n_batches > 0 && ofs_batches as usize >= original_m2_data.len())
203        {
204            return Err(M2Error::ParseError(format!(
205                "Skin {} appears to have invalid ModelView data. This may not be a valid embedded skin.",
206                skin_index
207            )));
208        }
209
210        // IMPORTANT: The ModelView field names are misleading!
211        // - nTris/ofsTris contains the INDICES array (triangle vertex indices)
212        // - nIndex/ofsIndex contains the TRIANGLES array (vertex lookup table)
213        // This is counterintuitive but confirmed by working implementations.
214
215        // Calculate actual data sizes based on corrected understanding:
216        // - indices come from nTris/ofsTris (should be the larger array - triangle connectivity)
217        // - triangles come from nIndex/ofsIndex (should be the smaller array - vertex lookup table)
218        // - submeshes come from nSub/ofsSub
219        let indices_size = (n_tris as usize) * 2; // Indices from tris field (u16 per index) 
220        let triangles_size = (n_index as usize) * 2; // Triangles from index field (u16 per triangle)
221
222        // Calculate submesh data size based on empirical validation:
223        // - Pre-TBC (versions < 260): 32 bytes aligned structure (empirically validated)
224        // - TBC+ (versions >= 260): 10 uint16_t + 2 Vector3 + 1 float = 48 bytes
225        // NOTE: We always allocate 48 bytes per submesh in our buffer for the parser,
226        // but we read the original size from the file
227        let original_submesh_size_per_entry = if self.header.version < 260 {
228            32 // Empirically validated vanilla size with proper alignment
229        } else {
230            48 // TBC+ size (same as buffer size)
231        };
232
233        let original_submeshes_size = (n_sub as usize) * original_submesh_size_per_entry;
234        let buffer_submeshes_size = original_submeshes_size; // Keep original size in buffer
235
236        let batches_size = (n_batches as usize) * 96; // 96 bytes each
237
238        // Verify offsets are within bounds
239        if ofs_tris as usize + indices_size > original_m2_data.len() {
240            return Err(M2Error::ParseError(format!(
241                "Indices data at offset {:#x} + {} exceeds file size {}",
242                ofs_tris,
243                indices_size,
244                original_m2_data.len()
245            )));
246        }
247
248        if ofs_index as usize + triangles_size > original_m2_data.len() {
249            return Err(M2Error::ParseError(format!(
250                "Triangles data at offset {:#x} + {} exceeds file size {}",
251                ofs_index,
252                triangles_size,
253                original_m2_data.len()
254            )));
255        }
256
257        // Verify submeshes offset is within bounds if we have submeshes
258        if n_sub > 0 && ofs_sub as usize + original_submeshes_size > original_m2_data.len() {
259            return Err(M2Error::ParseError(format!(
260                "Submeshes data at offset {:#x} + {} exceeds file size {}",
261                ofs_sub,
262                original_submeshes_size,
263                original_m2_data.len()
264            )));
265        }
266
267        if n_batches > 0 && ofs_batches as usize + batches_size > original_m2_data.len() {
268            return Err(M2Error::ParseError(format!(
269                "Batches data at offset {:#x} + {} exceeds file size {}",
270                ofs_batches,
271                batches_size,
272                original_m2_data.len()
273            )));
274        }
275
276        // Calculate where to place data in our buffer
277        let header_size = 40; // 5 M2Arrays * 8 bytes each
278        let indices_buffer_offset = header_size; // Indices go first (smaller array)
279        let triangles_buffer_offset = indices_buffer_offset + triangles_size; // Triangles go second (larger array)
280        let submeshes_buffer_offset = triangles_buffer_offset + indices_size;
281        let batches_buffer_offset = submeshes_buffer_offset + buffer_submeshes_size;
282        let total_buffer_size = batches_buffer_offset + batches_size;
283
284        // Allocate buffer for skin data with proper layout
285
286        let mut skin_buffer = vec![0u8; total_buffer_size];
287
288        // Write header at the beginning
289        let mut cursor = Cursor::new(&mut skin_buffer);
290
291        // Write the skin header with corrected field mapping:
292        // SWAP: Put the larger array (from tris field) into triangles field
293        // and the smaller array (from index field) into indices field
294
295        // Write indices array header (from nIndex/ofsIndex - smaller array)
296        cursor.write_all(&n_index.to_le_bytes())?; // Count of indices (from index field)
297        cursor.write_all(&(indices_buffer_offset as u32).to_le_bytes())?; // Offset to indices data
298
299        // Write triangles array header (from nTris/ofsTris - larger array)
300        cursor.write_all(&n_tris.to_le_bytes())?; // Count of triangles (from tris field)
301        cursor.write_all(&(triangles_buffer_offset as u32).to_le_bytes())?; // Offset to triangles data
302
303        // Write empty bone_indices array
304        cursor.write_all(&0u32.to_le_bytes())?;
305        cursor.write_all(&0u32.to_le_bytes())?;
306
307        // Write submeshes array header (from nSub/ofsSub in ModelView)
308        cursor.write_all(&n_sub.to_le_bytes())?; // Count of submeshes
309        cursor.write_all(&(submeshes_buffer_offset as u32).to_le_bytes())?; // Offset to submeshes data
310
311        // Write batches array
312        cursor.write_all(&n_batches.to_le_bytes())?;
313        cursor.write_all(&(batches_buffer_offset as u32).to_le_bytes())?;
314
315        // Copy actual data from the original M2 file with corrected mapping:
316        // Copy indices data (from ofsIndex in ModelView - smaller array goes to indices)
317        if n_index > 0 {
318            let src_indices =
319                &original_m2_data[ofs_index as usize..(ofs_index as usize + triangles_size)];
320            skin_buffer[indices_buffer_offset..(indices_buffer_offset + triangles_size)]
321                .copy_from_slice(src_indices);
322        }
323
324        // Copy triangles data (from ofsTris in ModelView - larger array goes to triangles)
325        if n_tris > 0 {
326            let src_triangles =
327                &original_m2_data[ofs_tris as usize..(ofs_tris as usize + indices_size)];
328            skin_buffer[triangles_buffer_offset..(triangles_buffer_offset + indices_size)]
329                .copy_from_slice(src_triangles);
330        }
331
332        // Copy submesh data (from ofsSub in ModelView)
333        if n_sub > 0 {
334            let src_submeshes =
335                &original_m2_data[ofs_sub as usize..(ofs_sub as usize + original_submeshes_size)];
336
337            // Copy submesh data directly without padding - the parse_with_version method
338            // will handle the different structure sizes correctly
339            skin_buffer[submeshes_buffer_offset..submeshes_buffer_offset + original_submeshes_size]
340                .copy_from_slice(src_submeshes);
341        }
342
343        // Copy batches data (from ofsBatches in ModelView)
344        if n_batches > 0 {
345            let src_batches =
346                &original_m2_data[ofs_batches as usize..(ofs_batches as usize + batches_size)];
347            skin_buffer[batches_buffer_offset..(batches_buffer_offset + batches_size)]
348                .copy_from_slice(src_batches);
349        }
350
351        // Create a cursor with our complete skin buffer
352        let mut cursor = Cursor::new(&skin_buffer);
353
354        // Parse as embedded skin (no SKIN magic signature)
355        parse_embedded_skin(&mut cursor, self.header.version)
356    }
357
358    /// Get the number of embedded skin profiles in a pre-WotLK model
359    ///
360    /// Returns None if the model uses external skin files (version > 260)
361    pub fn embedded_skin_count(&self) -> Option<u32> {
362        if self.header.version <= 260 {
363            Some(self.header.views.count)
364        } else {
365            None
366        }
367    }
368
369    /// Check if this model uses embedded skins (pre-WotLK) or external skin files
370    pub fn has_embedded_skins(&self) -> bool {
371        // Vanilla (256), TBC (260-263) have embedded skins
372        // WotLK (264+) introduced external .skin files
373        self.header.version <= 263 && self.header.views.count > 0
374    }
375
376    /// Parse all embedded skin profiles from a pre-WotLK model
377    ///
378    /// This is a convenience method that extracts all skin profiles at once.
379    ///
380    /// # Arguments
381    ///
382    /// * `original_m2_data` - The complete original M2 file data
383    ///
384    /// # Returns
385    ///
386    /// A vector of all parsed skin profiles
387    pub fn parse_all_embedded_skins(&self, original_m2_data: &[u8]) -> Result<Vec<SkinFile>> {
388        if !self.has_embedded_skins() {
389            return Ok(Vec::new());
390        }
391
392        let count = self.header.views.count as usize;
393        let mut skins = Vec::with_capacity(count);
394
395        for i in 0..count {
396            skins.push(self.parse_embedded_skin(original_m2_data, i)?);
397        }
398
399        Ok(skins)
400    }
401}
402
403/// Helper function to extract embedded skin data without loading the full model
404///
405/// This can be useful for tools that only need to extract skin data.
406///
407/// # Arguments
408///
409/// * `m2_data` - The complete M2 file data
410/// * `skin_index` - Index of the skin profile to extract
411///
412/// # Returns
413///
414/// The raw bytes of the skin data at the specified index
415pub fn extract_embedded_skin_bytes(m2_data: &[u8], skin_index: usize) -> Result<Vec<u8>> {
416    // Read magic and version to validate
417    let mut cursor = Cursor::new(m2_data);
418    let mut magic_bytes = [0u8; 4];
419    cursor
420        .read_exact(&mut magic_bytes)
421        .map_err(|e| M2Error::ParseError(format!("Failed to read magic: {}", e)))?;
422
423    if &magic_bytes != b"MD20" {
424        return Err(M2Error::ParseError(format!(
425            "Invalid M2 magic: expected MD20, got {:?}",
426            magic_bytes
427        )));
428    }
429
430    let version = cursor.read_u32_le()?;
431
432    if version > 260 {
433        return Err(M2Error::ParseError(format!(
434            "Version {} does not have embedded skins",
435            version
436        )));
437    }
438
439    // Skip to views array (at offset 0x2C in the header for old versions)
440    cursor.seek(SeekFrom::Start(0x2C))?;
441    let views_count = cursor.read_u32_le()?;
442    let views_offset = cursor.read_u32_le()?;
443
444    if skin_index >= views_count as usize {
445        return Err(M2Error::ParseError(format!(
446            "Skin index {} out of range (max: {})",
447            skin_index,
448            views_count - 1
449        )));
450    }
451
452    // Read the offset to the skin data
453    cursor.seek(SeekFrom::Start(
454        views_offset as u64 + (skin_index as u64 * 4),
455    ))?;
456    let skin_offset = cursor.read_u32_le()? as usize;
457
458    // We don't know the exact size, but we can estimate based on typical skin sizes
459    // or read until we hit the next structure. For now, let's read a reasonable chunk.
460    // Most skin headers are under 64KB
461    const MAX_SKIN_SIZE: usize = 65536;
462
463    let end_offset = (skin_offset + MAX_SKIN_SIZE).min(m2_data.len());
464    let skin_bytes = m2_data[skin_offset..end_offset].to_vec();
465
466    Ok(skin_bytes)
467}
468
469#[cfg(test)]
470mod tests {
471    use super::*;
472
473    #[test]
474    fn test_embedded_skin_detection() {
475        use crate::common::M2Array;
476        // Create a mock pre-WotLK model header
477        let mut model = M2Model::default();
478        model.header.version = 256; // Vanilla WoW version
479        model.header.views = M2Array::new(2, 0x1000); // 2 skin profiles at offset 0x1000
480
481        assert!(model.has_embedded_skins());
482        assert_eq!(model.embedded_skin_count(), Some(2));
483    }
484
485    #[test]
486    fn test_post_wotlk_no_embedded() {
487        use crate::common::M2Array;
488        // Create a mock WotLK+ model header
489        let mut model = M2Model::default();
490        model.header.version = 264; // WotLK version
491        model.header.views = M2Array::new(0, 0);
492
493        assert!(!model.has_embedded_skins());
494        assert_eq!(model.embedded_skin_count(), None);
495    }
496
497    #[test]
498    fn test_parse_embedded_skin_without_magic() {
499        use crate::common::M2Array;
500        use std::io::Write;
501
502        // Create a mock M2 file with embedded skin data using the ModelView structure
503        let mut m2_data = vec![0u8; 0x2000];
504
505        // Write a ModelView structure at 0x1000
506        // ModelView is 44 bytes (5 M2Arrays + 1 uint32): indices, triangles, properties, submeshes, textureUnits, boneCountMax
507        let mut cursor = std::io::Cursor::new(&mut m2_data[0x1000..]);
508
509        // Write ModelView fields (as M2Arrays: count + offset)
510        cursor.write_all(&10u32.to_le_bytes()).unwrap(); // indices.count (10 indices)
511        cursor.write_all(&0x1200u32.to_le_bytes()).unwrap(); // indices.offset
512        cursor.write_all(&6u32.to_le_bytes()).unwrap(); // triangles.count (6 triangles)  
513        cursor.write_all(&0x1220u32.to_le_bytes()).unwrap(); // triangles.offset
514        cursor.write_all(&0u32.to_le_bytes()).unwrap(); // properties.count
515        cursor.write_all(&0u32.to_le_bytes()).unwrap(); // properties.offset
516        cursor.write_all(&2u32.to_le_bytes()).unwrap(); // submeshes.count (2 submeshes)
517        cursor.write_all(&0x1240u32.to_le_bytes()).unwrap(); // submeshes.offset
518        cursor.write_all(&0u32.to_le_bytes()).unwrap(); // textureUnits.count
519        cursor.write_all(&0u32.to_le_bytes()).unwrap(); // textureUnits.offset
520        cursor.write_all(&50u32.to_le_bytes()).unwrap(); // boneCountMax
521
522        // Write some dummy index data at 0x1200 (10 indices * 2 bytes)
523        for i in 0..10u16 {
524            let idx_pos = 0x1200 + (i as usize * 2);
525            m2_data[idx_pos..idx_pos + 2].copy_from_slice(&i.to_le_bytes());
526        }
527
528        // Write some dummy triangle data at 0x1220 (6 triangles * 2 bytes)
529        for i in 0..6u16 {
530            let tri_pos = 0x1220 + (i as usize * 2);
531            m2_data[tri_pos..tri_pos + 2].copy_from_slice(&i.to_le_bytes());
532        }
533
534        // Write dummy submesh data at 0x1240 (2 submeshes * 28 bytes each for vanilla)
535        // Each vanilla submesh has 8 uint16 fields (16 bytes) and ends at 28 bytes
536        for i in 0..2u32 {
537            let submesh_pos = 0x1240 + (i as usize * 28);
538            let mut submesh_cursor = std::io::Cursor::new(&mut m2_data[submesh_pos..]);
539            submesh_cursor.write_all(&(i as u16).to_le_bytes()).unwrap(); // id
540            submesh_cursor.write_all(&0u16.to_le_bytes()).unwrap(); // level
541            submesh_cursor
542                .write_all(&(i as u16 * 5).to_le_bytes())
543                .unwrap(); // vertex_start
544            submesh_cursor.write_all(&5u16.to_le_bytes()).unwrap(); // vertex_count
545            submesh_cursor
546                .write_all(&(i as u16 * 3).to_le_bytes())
547                .unwrap(); // triangle_start
548            submesh_cursor.write_all(&3u16.to_le_bytes()).unwrap(); // triangle_count
549            submesh_cursor.write_all(&0u16.to_le_bytes()).unwrap(); // bone_start
550            submesh_cursor.write_all(&0u16.to_le_bytes()).unwrap(); // bone_count
551            // Remaining fields up to 28 bytes
552            for _ in 0..(28 - 16) {
553                submesh_cursor.write_all(&0u8.to_le_bytes()).unwrap();
554            }
555        }
556
557        // Create a model and test parsing
558        let mut model = M2Model::default();
559        model.header.version = 256;
560        model.header.views = M2Array::new(1, 0x1000); // One view at 0x1000
561
562        // Parse the embedded skin
563        let result = model.parse_embedded_skin(&m2_data, 0);
564        assert!(
565            result.is_ok(),
566            "Failed to parse embedded skin: {:?}",
567            result.err()
568        );
569
570        let skin = result.unwrap();
571        // After fix: corrected field mapping understanding
572        // - ModelView.indices (count=10) maps to triangles array (larger, connectivity)
573        // - ModelView.triangles (count=6) maps to indices array (smaller, lookup table)
574        // - ModelView.submeshes (count=2) maps to submeshes array
575        assert_eq!(skin.indices().len(), 10); // From original indices.count (now correctly mapped)
576        assert_eq!(skin.triangles().len(), 6); // From original triangles.count (now correctly mapped)
577        assert_eq!(skin.submeshes().len(), 2);
578    }
579}