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}