wow_m2/
skin.rs

1use crate::io_ext::{ReadExt, WriteExt};
2use std::fs::File;
3use std::io::{Read, Seek, SeekFrom, Write};
4use std::path::Path;
5
6use crate::common::M2Array;
7use crate::error::{M2Error, Result};
8use crate::version::M2Version;
9
10/// Magic signature for Skin files ("SKIN")
11pub const SKIN_MAGIC: [u8; 4] = *b"SKIN";
12
13/// Detect SKIN format variant based on the second u32 field
14/// Returns true for new format (camera files with version), false for old format (character models)
15fn detect_skin_format<R: Read + Seek>(reader: &mut R) -> Result<bool> {
16    let start_pos = reader.stream_position()?;
17
18    // Skip magic
19    reader.seek(SeekFrom::Current(4))?;
20
21    // Read the second u32 field
22    let second_field = reader.read_u32_le()?;
23
24    // Reset position
25    reader.seek(SeekFrom::Start(start_pos))?;
26
27    // If <= 4, it's likely a version field (new format)
28    // If > 4, it's likely an indices count (old format)
29    Ok(second_field <= 4)
30}
31
32/// Parse a SKIN file with automatic format detection
33pub fn parse_skin<R: Read + Seek>(reader: &mut R) -> Result<SkinFile> {
34    let is_new_format = detect_skin_format(reader)?;
35
36    if is_new_format {
37        let skin = SkinG::<SkinHeader>::parse(reader)?;
38        Ok(SkinFile::New(skin))
39    } else {
40        let skin = SkinG::<OldSkinHeader>::parse(reader)?;
41        Ok(SkinFile::Old(skin))
42    }
43}
44
45/// Parse embedded skin data from pre-WotLK M2 models (no SKIN magic)
46pub fn parse_embedded_skin<R: Read + Seek>(reader: &mut R, m2_version: u32) -> Result<SkinFile> {
47    // Parse the header without expecting SKIN magic
48    let header = OldSkinHeader::parse_embedded(reader)?;
49
50    // Parse indices
51    let mut indices = Vec::with_capacity(header.indices.count as usize);
52    if header.indices.count > 0 && header.indices.offset > 0 {
53        reader.seek(SeekFrom::Start(header.indices.offset as u64))?;
54        for _ in 0..header.indices.count {
55            indices.push(reader.read_u16_le()?);
56        }
57    }
58
59    // Parse triangles
60    let mut triangles = Vec::with_capacity(header.triangles.count as usize);
61    if header.triangles.count > 0 && header.triangles.offset > 0 {
62        reader.seek(SeekFrom::Start(header.triangles.offset as u64))?;
63        for _ in 0..header.triangles.count {
64            triangles.push(reader.read_u16_le()?);
65        }
66    }
67
68    // Parse bone indices
69    // Note: count is number of vertices, each with 4 bone indices (ubyte4)
70    let total_bone_bytes = (header.bone_indices.count as usize) * 4;
71    let mut bone_indices = Vec::with_capacity(total_bone_bytes);
72    if header.bone_indices.count > 0 && header.bone_indices.offset > 0 {
73        reader.seek(SeekFrom::Start(header.bone_indices.offset as u64))?;
74        for _ in 0..total_bone_bytes {
75            bone_indices.push(reader.read_u8()?);
76        }
77    }
78
79    // Parse submeshes
80    let mut submeshes = Vec::with_capacity(header.submeshes.count as usize);
81    if header.submeshes.count > 0 && header.submeshes.offset > 0 {
82        reader.seek(SeekFrom::Start(header.submeshes.offset as u64))?;
83        for _ in 0..header.submeshes.count {
84            submeshes.push(SkinSubmesh::parse_with_version(reader, m2_version)?);
85        }
86    }
87
88    // Parse batches
89    let mut batches = Vec::with_capacity(header.batches.count as usize);
90    if header.batches.count > 0 && header.batches.offset > 0 {
91        reader.seek(SeekFrom::Start(header.batches.offset as u64))?;
92        for _ in 0..header.batches.count {
93            batches.push(SkinBatch::parse(reader)?);
94        }
95    }
96
97    let skin = SkinG::<OldSkinHeader> {
98        header,
99        indices,
100        triangles,
101        bone_indices,
102        submeshes,
103        batches,
104    };
105
106    Ok(SkinFile::Old(skin))
107}
108
109/// Load a SKIN file from a path with automatic format detection
110pub fn load_skin<P: AsRef<Path>>(path: P) -> Result<SkinFile> {
111    let mut file = File::open(path)?;
112    parse_skin(&mut file)
113}
114
115pub trait SkinHeaderT: Sized {
116    fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self>;
117    fn write<W: Write>(&self, writer: &mut W) -> Result<()>;
118    fn calculate_size(&self) -> usize;
119    fn set_array_fields(
120        &mut self,
121        indices: M2Array<u16>,
122        triangles: M2Array<u16>,
123        bone_indices: M2Array<u8>,
124        submeshes: M2Array<SkinSubmesh>,
125        batches: M2Array<SkinBatch>,
126    );
127    fn indices(&self) -> &M2Array<u16>;
128    fn triangles(&self) -> &M2Array<u16>;
129    fn bone_indices(&self) -> &M2Array<u8>;
130    fn submeshes(&self) -> &M2Array<SkinSubmesh>;
131    fn batches(&self) -> &M2Array<SkinBatch>;
132}
133
134/// Skin file header
135#[derive(Debug, Clone)]
136pub struct SkinHeader {
137    /// Magic signature ("SKIN")
138    pub magic: [u8; 4],
139    /// Version of the file
140    pub version: u32,
141    /// Name of the parent model
142    pub name: M2Array<u8>,
143    /// Total number of vertices
144    pub vertex_count: u32,
145    /// Indices
146    pub indices: M2Array<u16>,
147    /// Triangles
148    pub triangles: M2Array<u16>,
149    /// Bone indices
150    pub bone_indices: M2Array<u8>,
151    /// Submeshes
152    pub submeshes: M2Array<SkinSubmesh>,
153    /// Batches
154    pub batches: M2Array<SkinBatch>,
155    /// Center position (BfA and later)
156    pub center_position: Option<[f32; 3]>,
157    /// Center bounds (BfA and later)
158    pub center_bounds: Option<f32>,
159}
160
161impl SkinHeaderT for SkinHeader {
162    /// Parse a Skin header from a reader (new format with version field)
163    fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
164        // Read and check magic
165        let mut magic = [0u8; 4];
166        reader.read_exact(&mut magic)?;
167
168        if magic != SKIN_MAGIC {
169            return Err(M2Error::InvalidMagic {
170                expected: String::from_utf8_lossy(&SKIN_MAGIC).to_string(),
171                actual: String::from_utf8_lossy(&magic).to_string(),
172            });
173        }
174
175        // Read version
176        let version = reader.read_u32_le()?;
177
178        // Validate version for new format (should be 0-4)
179        if version > 4 {
180            return Err(M2Error::UnsupportedVersion(format!(
181                "New format version {} is too high, expected 0-4. This might be an old format file.",
182                version
183            )));
184        }
185
186        // Create the appropriate version
187        let _m2_version = match version {
188            0 => M2Version::Vanilla,
189            1 => M2Version::Cataclysm,
190            2 => M2Version::MoP,
191            3 => M2Version::WoD,
192            4 => M2Version::Legion,
193            v => {
194                return Err(M2Error::UnsupportedVersion(v.to_string()));
195            }
196        };
197
198        // Read name
199        let name = M2Array::parse(reader)?;
200
201        // Read vertex count
202        let vertex_count = reader.read_u32_le()?;
203
204        // Read array references
205        let indices = M2Array::parse(reader)?;
206        let triangles = M2Array::parse(reader)?;
207        let bone_indices = M2Array::parse(reader)?;
208        let submeshes = M2Array::parse(reader)?;
209        let batches = M2Array::parse(reader)?;
210
211        // For BfA and later, we have additional fields
212        let (center_position, center_bounds) = if version >= 4 {
213            let file_size = reader.seek(SeekFrom::End(0))?;
214
215            // If we have more data, it's probably BfA or later
216            if file_size > reader.stream_position()? {
217                let mut center_pos = [0.0; 3];
218                for item in &mut center_pos {
219                    *item = reader.read_f32_le()?;
220                }
221                let center_bound = reader.read_f32_le()?;
222
223                (Some(center_pos), Some(center_bound))
224            } else {
225                (None, None)
226            }
227        } else {
228            (None, None)
229        };
230
231        Ok(Self {
232            magic,
233            version,
234            name,
235            vertex_count,
236            indices,
237            triangles,
238            bone_indices,
239            submeshes,
240            batches,
241            center_position,
242            center_bounds,
243        })
244    }
245
246    /// Write a Skin header to a writer
247    fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
248        // Write magic and version
249        writer.write_all(&self.magic)?;
250        writer.write_u32_le(self.version)?;
251
252        // Write name
253        self.name.write(writer)?;
254
255        // Write vertex count
256        writer.write_u32_le(self.vertex_count)?;
257
258        // Write array references
259        self.indices.write(writer)?;
260        self.triangles.write(writer)?;
261        self.bone_indices.write(writer)?;
262        self.submeshes.write(writer)?;
263        self.batches.write(writer)?;
264
265        // Write BfA+ fields if present
266        if let Some(center_pos) = self.center_position {
267            for &value in &center_pos {
268                writer.write_f32_le(value)?;
269            }
270
271            if let Some(center_bound) = self.center_bounds {
272                writer.write_f32_le(center_bound)?;
273            } else {
274                writer.write_f32_le(0.0)?;
275            }
276        }
277
278        Ok(())
279    }
280
281    /// Calculate the size of the header for this skin version
282    fn calculate_size(&self) -> usize {
283        let mut size = 4 + 4; // Magic + version
284
285        // Name
286        size += 2 * 4;
287
288        // Vertex count
289        size += 4;
290
291        // Array references
292        size += 5 * (2 * 4); // 5 arrays, each with count and offset (8 bytes)
293
294        // BfA and later have additional fields
295        if self.center_position.is_some() {
296            size += 3 * 4; // Center position (3 floats)
297            size += 4; // Center bounds (1 float)
298        }
299
300        size
301    }
302
303    fn set_array_fields(
304        &mut self,
305        indices: M2Array<u16>,
306        triangles: M2Array<u16>,
307        bone_indices: M2Array<u8>,
308        submeshes: M2Array<SkinSubmesh>,
309        batches: M2Array<SkinBatch>,
310    ) {
311        self.indices = indices;
312        self.triangles = triangles;
313        self.bone_indices = bone_indices;
314        self.submeshes = submeshes;
315        self.batches = batches;
316    }
317
318    fn indices(&self) -> &M2Array<u16> {
319        &self.indices
320    }
321
322    fn triangles(&self) -> &M2Array<u16> {
323        &self.triangles
324    }
325
326    fn bone_indices(&self) -> &M2Array<u8> {
327        &self.bone_indices
328    }
329
330    fn submeshes(&self) -> &M2Array<SkinSubmesh> {
331        &self.submeshes
332    }
333
334    fn batches(&self) -> &M2Array<SkinBatch> {
335        &self.batches
336    }
337}
338
339impl SkinHeader {
340    /// Get the M2 version for this skin
341    pub fn get_m2_version(&self) -> Option<M2Version> {
342        match self.version {
343            0 => Some(M2Version::Vanilla),
344            1 => Some(M2Version::Cataclysm),
345            2 => Some(M2Version::MoP),
346            3 => Some(M2Version::WoD),
347            4 => {
348                // BfA and later have additional fields
349                if self.center_position.is_some() {
350                    Some(M2Version::BfA)
351                } else {
352                    Some(M2Version::Legion)
353                }
354            }
355            _ => None,
356        }
357    }
358
359    /// Create a new Skin header for a specific version
360    pub fn new(m2_version: M2Version) -> Self {
361        let version = match m2_version {
362            M2Version::Vanilla | M2Version::TBC | M2Version::WotLK => 0,
363            M2Version::Cataclysm => 1,
364            M2Version::MoP => 2,
365            M2Version::WoD => 3,
366            M2Version::Legion => 4,
367            M2Version::BfA
368            | M2Version::Shadowlands
369            | M2Version::Dragonflight
370            | M2Version::TheWarWithin => 4,
371        };
372
373        let center_position = if m2_version >= M2Version::BfA {
374            Some([0.0, 0.0, 0.0])
375        } else {
376            None
377        };
378
379        let center_bounds = if m2_version >= M2Version::BfA {
380            Some(0.0)
381        } else {
382            None
383        };
384
385        Self {
386            magic: SKIN_MAGIC,
387            version,
388            name: M2Array::new(0, 0),
389            vertex_count: 0,
390            indices: M2Array::new(0, 0),
391            triangles: M2Array::new(0, 0),
392            bone_indices: M2Array::new(0, 0),
393            submeshes: M2Array::new(0, 0),
394            batches: M2Array::new(0, 0),
395            center_position,
396            center_bounds,
397        }
398    }
399}
400
401/// OldSkin file header
402#[derive(Debug, Clone)]
403pub struct OldSkinHeader {
404    /// Magic signature ("SKIN")
405    pub magic: [u8; 4],
406    /// Indices (vertex lookup table)
407    pub indices: M2Array<u16>,
408    /// Triangles (index buffer, groups of 3)
409    pub triangles: M2Array<u16>,
410    /// Bone indices (4 bytes per vertex - ubyte4)
411    /// Note: The count is number of vertices, actual data is count * 4 bytes
412    pub bone_indices: M2Array<u8>,
413    /// Submeshes
414    pub submeshes: M2Array<SkinSubmesh>,
415    /// Batches
416    pub batches: M2Array<SkinBatch>,
417    /// Maximum bones per draw call
418    pub bone_count_max: u32,
419}
420
421impl OldSkinHeader {
422    /// Parse embedded skin data from pre-WotLK M2 models (no SKIN magic)
423    pub fn parse_embedded<R: Read + Seek>(reader: &mut R) -> Result<Self> {
424        // Embedded skins don't have the SKIN magic signature
425        // They start directly with the array references
426        let indices = M2Array::parse(reader)?;
427        let triangles = M2Array::parse(reader)?;
428        let bone_indices = M2Array::parse(reader)?;
429        let submeshes = M2Array::parse(reader)?;
430        let batches = M2Array::parse(reader)?;
431
432        Ok(Self {
433            magic: SKIN_MAGIC, // Set magic for compatibility
434            indices,
435            triangles,
436            bone_indices,
437            submeshes,
438            batches,
439            bone_count_max: 0, // Default for embedded skins
440        })
441    }
442}
443
444impl SkinHeaderT for OldSkinHeader {
445    /// Parse a Skin header from a reader (old format without version field)
446    fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
447        // Read and check magic
448        let mut magic = [0u8; 4];
449        reader.read_exact(&mut magic)?;
450
451        if magic != SKIN_MAGIC {
452            return Err(M2Error::InvalidMagic {
453                expected: String::from_utf8_lossy(&SKIN_MAGIC).to_string(),
454                actual: String::from_utf8_lossy(&magic).to_string(),
455            });
456        }
457
458        // Read array references directly (no version field in old format)
459        let indices = M2Array::parse(reader)?;
460        let triangles = M2Array::parse(reader)?;
461        let bone_indices = M2Array::parse(reader)?;
462        let submeshes = M2Array::parse(reader)?;
463        let batches = M2Array::parse(reader)?;
464
465        // Read bone_count_max (maximum bones per draw call)
466        let bone_count_max = reader.read_u32_le()?;
467
468        Ok(Self {
469            magic,
470            indices,
471            triangles,
472            bone_indices,
473            submeshes,
474            batches,
475            bone_count_max,
476        })
477    }
478
479    /// Write a Skin header to a writer
480    fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
481        // Write magic
482        writer.write_all(&self.magic)?;
483
484        // Write array references
485        self.indices.write(writer)?;
486        self.triangles.write(writer)?;
487        self.bone_indices.write(writer)?;
488        self.submeshes.write(writer)?;
489        self.batches.write(writer)?;
490
491        // Write bone_count_max
492        writer.write_u32_le(self.bone_count_max)?;
493
494        Ok(())
495    }
496
497    /// Calculate the size of the header for this skin version
498    fn calculate_size(&self) -> usize {
499        let mut size = 4; // Magic only (no version in old format)
500
501        // Array references
502        size += 5 * (2 * 4); // 5 arrays, each with count and offset (8 bytes)
503
504        // bone_count_max field
505        size += 4;
506
507        size
508    }
509
510    fn set_array_fields(
511        &mut self,
512        indices: M2Array<u16>,
513        triangles: M2Array<u16>,
514        bone_indices: M2Array<u8>,
515        submeshes: M2Array<SkinSubmesh>,
516        batches: M2Array<SkinBatch>,
517    ) {
518        self.indices = indices;
519        self.triangles = triangles;
520        self.bone_indices = bone_indices;
521        self.submeshes = submeshes;
522        self.batches = batches;
523    }
524
525    fn indices(&self) -> &M2Array<u16> {
526        &self.indices
527    }
528
529    fn triangles(&self) -> &M2Array<u16> {
530        &self.triangles
531    }
532
533    fn bone_indices(&self) -> &M2Array<u8> {
534        &self.bone_indices
535    }
536
537    fn submeshes(&self) -> &M2Array<SkinSubmesh> {
538        &self.submeshes
539    }
540
541    fn batches(&self) -> &M2Array<SkinBatch> {
542        &self.batches
543    }
544}
545
546impl OldSkinHeader {
547    /// Create a new Skin header for a specific version
548    pub fn new() -> Self {
549        Self {
550            magic: SKIN_MAGIC,
551            indices: M2Array::new(0, 0),
552            triangles: M2Array::new(0, 0),
553            bone_indices: M2Array::new(0, 0),
554            submeshes: M2Array::new(0, 0),
555            batches: M2Array::new(0, 0),
556            bone_count_max: 0,
557        }
558    }
559}
560
561impl Default for OldSkinHeader {
562    fn default() -> Self {
563        Self::new()
564    }
565}
566
567/// Submesh structure
568#[derive(Debug, Clone)]
569pub struct SkinSubmesh {
570    /// Submesh ID
571    pub id: u16,
572    /// Level of detail
573    pub level: u16,
574    /// Start vertex index
575    pub vertex_start: u16,
576    /// Vertex count
577    pub vertex_count: u16,
578    /// Start triangle index
579    pub triangle_start: u16,
580    /// Triangle count
581    pub triangle_count: u16,
582    /// Bone count
583    pub bone_count: u16,
584    /// Start bone index
585    pub bone_start: u16,
586    /// Bone influence count (max bones per vertex)
587    pub bone_influence: u16,
588    /// Center of mass
589    pub center: [f32; 3],
590    /// Sort center
591    pub sort_center: [f32; 3],
592    /// Bounding sphere radius
593    pub bounding_radius: f32,
594}
595
596impl SkinSubmesh {
597    /// Parse a submesh from a reader with version-aware structure size
598    pub fn parse_with_version<R: Read>(reader: &mut R, m2_version: u32) -> Result<Self> {
599        if m2_version < 260 {
600            // Vanilla/classic format: 32-byte aligned structure
601            Self::parse_vanilla(reader)
602        } else {
603            // Modern format: full 48-byte structure
604            Self::parse(reader)
605        }
606    }
607
608    /// Parse a vanilla submesh (32-byte structure) - empirically validated
609    pub fn parse_vanilla<R: Read>(reader: &mut R) -> Result<Self> {
610        let id = reader.read_u16_le()?;
611        let level = reader.read_u16_le()?;
612        let vertex_start = reader.read_u16_le()?;
613        let vertex_count = reader.read_u16_le()?;
614        let triangle_start = reader.read_u16_le()?;
615        let triangle_count = reader.read_u16_le()?;
616        let bone_count = reader.read_u16_le()?;
617        let bone_start = reader.read_u16_le()?;
618
619        // Read 4 float32 values (16 bytes) - structure verified by Python parser
620        let float1 = reader.read_f32_le()?;
621        let float2 = reader.read_f32_le()?;
622        let float3 = reader.read_f32_le()?;
623        let float4 = reader.read_f32_le()?;
624
625        // Map the 4 floats to center coordinates (first 3) and use defaults for the rest
626        let center = [float1, float2, float3];
627
628        Ok(Self {
629            id,
630            level,
631            vertex_start,
632            vertex_count,
633            triangle_start,
634            triangle_count,
635            bone_count,
636            bone_start,
637            bone_influence: 0, // Default for vanilla
638            center,
639            sort_center: [0.0, 0.0, 0.0], // Default for vanilla
640            bounding_radius: float4,      // Use 4th float as bounding radius
641        })
642    }
643
644    /// Parse a submesh from a reader
645    pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
646        let id = reader.read_u16_le()?;
647        let level = reader.read_u16_le()?;
648        let vertex_start = reader.read_u16_le()?;
649        let vertex_count = reader.read_u16_le()?;
650        let triangle_start = reader.read_u16_le()?;
651        let triangle_count = reader.read_u16_le()?;
652        let bone_count = reader.read_u16_le()?;
653        let bone_start = reader.read_u16_le()?;
654        let bone_influence = reader.read_u16_le()?;
655
656        // Skip 1 u16 of padding
657        reader.read_u16_le()?;
658
659        let mut center = [0.0; 3];
660        let mut sort_center = [0.0; 3];
661
662        for item in &mut center {
663            *item = reader.read_f32_le()?;
664        }
665
666        for item in &mut sort_center {
667            *item = reader.read_f32_le()?;
668        }
669
670        let bounding_radius = reader.read_f32_le()?;
671
672        Ok(Self {
673            id,
674            level,
675            vertex_start,
676            vertex_count,
677            triangle_start,
678            triangle_count,
679            bone_count,
680            bone_start,
681            bone_influence,
682            center,
683            sort_center,
684            bounding_radius,
685        })
686    }
687
688    /// Write a submesh to a writer
689    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
690        writer.write_u16_le(self.id)?;
691        writer.write_u16_le(self.level)?;
692        writer.write_u16_le(self.vertex_start)?;
693        writer.write_u16_le(self.vertex_count)?;
694        writer.write_u16_le(self.triangle_start)?;
695        writer.write_u16_le(self.triangle_count)?;
696        writer.write_u16_le(self.bone_count)?;
697        writer.write_u16_le(self.bone_start)?;
698        writer.write_u16_le(self.bone_influence)?;
699
700        // Write 1 u16 of padding
701        writer.write_u16_le(0)?;
702
703        for &value in &self.center {
704            writer.write_f32_le(value)?;
705        }
706
707        for &value in &self.sort_center {
708            writer.write_f32_le(value)?;
709        }
710
711        writer.write_f32_le(self.bounding_radius)?;
712
713        Ok(())
714    }
715}
716
717/// Main Skin structure
718#[derive(Debug, Clone)]
719pub struct SkinG<H>
720where
721    H: SkinHeaderT,
722{
723    /// Skin header
724    pub header: H,
725    /// Indices
726    pub indices: Vec<u16>,
727    /// Triangles (each is 3 indices)
728    pub triangles: Vec<u16>,
729    /// Bone indices
730    pub bone_indices: Vec<u8>,
731    /// Submeshes
732    pub submeshes: Vec<SkinSubmesh>,
733    /// Batches
734    pub batches: Vec<SkinBatch>,
735}
736
737impl<H> SkinG<H>
738where
739    H: SkinHeaderT + Clone,
740{
741    /// Parse a Skin from a reader
742    pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
743        // Parse the header
744        let header = H::parse(reader)?;
745
746        // Parse indices
747        let header_indices = header.indices();
748        reader.seek(SeekFrom::Start(header_indices.offset as u64))?;
749        let mut indices = Vec::with_capacity(header_indices.count as usize);
750        for _ in 0..header_indices.count {
751            indices.push(reader.read_u16_le()?);
752        }
753
754        // Parse triangles
755        let header_triangles = header.triangles();
756        reader.seek(SeekFrom::Start(header_triangles.offset as u64))?;
757        let mut triangles = Vec::with_capacity(header_triangles.count as usize);
758        for _ in 0..header_triangles.count {
759            triangles.push(reader.read_u16_le()?);
760        }
761
762        // Parse bone indices
763        // Note: The count in M2Array is the number of vertices, but each vertex has 4 bone indices
764        // (ubyte4 structure), so we read count * 4 bytes
765        let header_bone_indices = header.bone_indices();
766        reader.seek(SeekFrom::Start(header_bone_indices.offset as u64))?;
767        let total_bone_bytes = (header_bone_indices.count as usize) * 4;
768        let mut bone_indices = Vec::with_capacity(total_bone_bytes);
769        for _ in 0..total_bone_bytes {
770            bone_indices.push(reader.read_u8()?);
771        }
772
773        // Parse submeshes
774        let header_submeshes = header.submeshes();
775        reader.seek(SeekFrom::Start(header_submeshes.offset as u64))?;
776        let mut submeshes = Vec::with_capacity(header_submeshes.count as usize);
777        for _ in 0..header_submeshes.count {
778            submeshes.push(SkinSubmesh::parse(reader)?);
779        }
780
781        // Parse batches
782        let header_batches = header.batches();
783        reader.seek(SeekFrom::Start(header_batches.offset as u64))?;
784        let mut batches = Vec::with_capacity(header_batches.count as usize);
785        for _ in 0..header_batches.count {
786            batches.push(SkinBatch::parse(reader)?);
787        }
788
789        Ok(Self {
790            header,
791            indices,
792            triangles,
793            bone_indices,
794            submeshes,
795            batches,
796        })
797    }
798
799    /// Load a Skin from a file
800    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
801        let mut file = File::open(path)?;
802        Self::parse(&mut file)
803    }
804
805    /// Save a Skin to a file
806    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
807        let mut file = File::create(path)?;
808        self.write(&mut file)
809    }
810
811    /// Write a Skin to a writer
812    pub fn write<W: Write + Seek>(&self, writer: &mut W) -> Result<()> {
813        // We need to recalculate all offsets and build the file in memory
814        let mut data_section = Vec::new();
815        let mut header = self.header.clone();
816
817        // Start with header size (will be written last)
818        let header_size = header.calculate_size();
819        let mut current_offset = header_size as u32;
820
821        // Write indices
822        let indices = if !self.indices.is_empty() {
823            let indices = M2Array::new(self.indices.len() as u32, current_offset);
824
825            for &index in &self.indices {
826                data_section.extend_from_slice(&index.to_le_bytes());
827            }
828
829            current_offset += (self.indices.len() * std::mem::size_of::<u16>()) as u32;
830            indices
831        } else {
832            M2Array::new(0, 0)
833        };
834
835        // Write triangles
836        let triangles = if !self.triangles.is_empty() {
837            let triangles = M2Array::new(self.triangles.len() as u32, current_offset);
838
839            for &triangle in &self.triangles {
840                data_section.extend_from_slice(&triangle.to_le_bytes());
841            }
842
843            current_offset += (self.triangles.len() * std::mem::size_of::<u16>()) as u32;
844
845            triangles
846        } else {
847            M2Array::new(0, 0)
848        };
849
850        // Write bone indices
851        // Note: M2Array count is vertex count, but we store 4 bytes per vertex (ubyte4)
852        let bone_indices = if !self.bone_indices.is_empty() {
853            // Count is number of vertices (len / 4), not number of bytes
854            let vertex_count = (self.bone_indices.len() / 4) as u32;
855            let bone_indices = M2Array::new(vertex_count, current_offset);
856
857            for &bone_index in &self.bone_indices {
858                data_section.push(bone_index);
859            }
860
861            current_offset += self.bone_indices.len() as u32;
862
863            bone_indices
864        } else {
865            M2Array::new(0, 0)
866        };
867
868        // Write submeshes
869        let submeshes = if !self.submeshes.is_empty() {
870            let submeshes = M2Array::new(self.submeshes.len() as u32, current_offset);
871
872            for submesh in &self.submeshes {
873                let mut submesh_data = Vec::new();
874                submesh.write(&mut submesh_data)?;
875                data_section.extend_from_slice(&submesh_data);
876            }
877
878            current_offset += (self.submeshes.len() * 40) as u32; // Each submesh is 40 bytes
879            submeshes
880        } else {
881            M2Array::new(0, 0)
882        };
883
884        // Write material lookup
885        let batches = if !self.batches.is_empty() {
886            let batches = M2Array::new(self.batches.len() as u32, current_offset);
887
888            for material in &self.batches {
889                let mut material_data = Vec::new();
890                material.write(&mut material_data)?;
891                data_section.extend_from_slice(&material_data);
892            }
893
894            // current_offset += (self.batches.len() * std::mem::size_of::<u16>()) as u32;
895            batches
896        } else {
897            M2Array::new(0, 0)
898        };
899
900        header.set_array_fields(indices, triangles, bone_indices, submeshes, batches);
901
902        // Finally, write the header followed by the data section
903        header.write(writer)?;
904        writer.write_all(&data_section)?;
905
906        Ok(())
907    }
908}
909
910impl SkinG<SkinHeader> {
911    /// Convert this skin to a different version
912    pub fn convert(&self, target_version: M2Version) -> Result<Self> {
913        let source_version = self
914            .header
915            .get_m2_version()
916            .ok_or(M2Error::ConversionError {
917                from: self.header.version,
918                to: target_version.to_header_version(),
919                reason: "Unknown source version".to_string(),
920            })?;
921
922        if source_version == target_version {
923            return Ok(self.clone());
924        }
925
926        // Create a new skin with the target version
927        let mut new_skin = self.clone();
928
929        // Update header version
930        let mut header = SkinHeader::new(target_version);
931        header.name = self.header.name;
932        header.vertex_count = self.header.vertex_count;
933
934        // Handle version-specific conversions
935        if target_version >= M2Version::BfA && source_version < M2Version::BfA {
936            // When upgrading to BfA or later, add center position and bounds if missing
937            if header.center_position.is_none() {
938                // Calculate center of mass from submeshes
939                let mut center = [0.0, 0.0, 0.0];
940                let mut max_radius = 0.0;
941
942                if !self.submeshes.is_empty() {
943                    for submesh in &self.submeshes {
944                        for (i, center_val) in center.iter_mut().enumerate() {
945                            *center_val += submesh.center[i];
946                        }
947
948                        if submesh.bounding_radius > max_radius {
949                            max_radius = submesh.bounding_radius;
950                        }
951                    }
952
953                    // Average the center
954                    let count = self.submeshes.len() as f32;
955                    for item in &mut center {
956                        *item /= count;
957                    }
958                }
959
960                header.center_position = Some(center);
961                header.center_bounds = Some(max_radius);
962            }
963        } else if target_version < M2Version::BfA && source_version >= M2Version::BfA {
964            // When downgrading from BfA or later, remove center position and bounds
965            header.center_position = None;
966            header.center_bounds = None;
967        }
968
969        new_skin.header = header;
970
971        Ok(new_skin)
972    }
973
974    /// Convert this new-format skin to old format (for WotLK and earlier)
975    ///
976    /// The old format lacks version field, name, and vertex_count fields.
977    /// This conversion preserves all mesh data (indices, triangles, submeshes, batches).
978    pub fn to_old_format(&self) -> OldSkin {
979        // Calculate bone_count_max from submeshes if available, otherwise use sensible default
980        let bone_count_max = self
981            .submeshes
982            .iter()
983            .map(|s| s.bone_count as u32)
984            .max()
985            .unwrap_or(64);
986
987        OldSkin {
988            header: OldSkinHeader {
989                magic: SKIN_MAGIC,
990                indices: self.header.indices,
991                triangles: self.header.triangles,
992                bone_indices: self.header.bone_indices,
993                submeshes: self.header.submeshes.clone(),
994                batches: self.header.batches.clone(),
995                bone_count_max,
996            },
997            indices: self.indices.clone(),
998            triangles: self.triangles.clone(),
999            bone_indices: self.bone_indices.clone(),
1000            submeshes: self.submeshes.clone(),
1001            batches: self.batches.clone(),
1002        }
1003    }
1004}
1005
1006impl SkinG<OldSkinHeader> {
1007    /// Convert this old-format skin to new format (for Cataclysm and later)
1008    ///
1009    /// The new format adds version field, name, and vertex_count fields.
1010    /// This conversion preserves all mesh data and initializes new fields with defaults.
1011    pub fn to_new_format(&self, target_version: M2Version) -> Skin {
1012        let mut header = SkinHeader::new(target_version);
1013        header.indices = self.header.indices;
1014        header.triangles = self.header.triangles;
1015        header.bone_indices = self.header.bone_indices;
1016        header.submeshes = self.header.submeshes.clone();
1017        header.batches = self.header.batches.clone();
1018
1019        // Calculate vertex count from indices if available
1020        if !self.indices.is_empty() {
1021            header.vertex_count = self.indices.iter().copied().max().unwrap_or(0) as u32 + 1;
1022        }
1023
1024        Skin {
1025            header,
1026            indices: self.indices.clone(),
1027            triangles: self.triangles.clone(),
1028            bone_indices: self.bone_indices.clone(),
1029            submeshes: self.submeshes.clone(),
1030            batches: self.batches.clone(),
1031        }
1032    }
1033}
1034
1035pub type Skin = SkinG<SkinHeader>;
1036pub type OldSkin = SkinG<OldSkinHeader>;
1037
1038/// Enum to represent either format variant
1039#[derive(Debug, Clone)]
1040pub enum SkinFile {
1041    /// New format with version field (camera files)
1042    New(Skin),
1043    /// Old format without version field (character models)
1044    Old(OldSkin),
1045}
1046
1047impl SkinFile {
1048    /// Parse a SKIN file with automatic format detection
1049    pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
1050        parse_skin(reader)
1051    }
1052
1053    /// Load a SKIN file from a path with automatic format detection
1054    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
1055        load_skin(path)
1056    }
1057
1058    /// Save the SKIN file
1059    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
1060        match self {
1061            SkinFile::New(skin) => skin.save(path),
1062            SkinFile::Old(skin) => skin.save(path),
1063        }
1064    }
1065
1066    /// Write the SKIN file to a writer
1067    pub fn write<W: Write + Seek>(&self, writer: &mut W) -> Result<()> {
1068        match self {
1069            SkinFile::New(skin) => skin.write(writer),
1070            SkinFile::Old(skin) => skin.write(writer),
1071        }
1072    }
1073
1074    /// Get resolved vertex indices for rendering
1075    ///
1076    /// CRITICAL CORRECTION: After empirical analysis, the triangles array already contains
1077    /// the final vertex indices for rendering. The previous assumption about two-level
1078    /// indirection was incorrect.
1079    ///
1080    /// For both embedded skins (pre-WotLK) and external .skin files (WotLK+):
1081    /// - The triangles array contains the direct vertex indices for mesh connectivity
1082    /// - No additional indirection is needed
1083    /// - Values like [76, 21, 23] are the actual vertex indices to use for triangles
1084    pub fn get_resolved_indices(&self) -> Vec<u16> {
1085        // FIXED: triangles array already contains the correct vertex indices
1086        // No two-level indirection needed - triangles are ready for rendering
1087        self.triangles().clone()
1088    }
1089
1090    /// Get raw indices array (vertex mapping/lookup table)
1091    ///
1092    /// Note: For triangle rendering, use `get_resolved_indices()` instead.
1093    /// This method returns the indices array which serves as a vertex mapping table.
1094    /// In practice, this array typically contains sequential values [0,1,2,3...]
1095    /// and is used internally by the M2 format for vertex organization.
1096    pub fn indices(&self) -> &Vec<u16> {
1097        match self {
1098            SkinFile::New(skin) => &skin.indices,
1099            SkinFile::Old(skin) => &skin.indices,
1100        }
1101    }
1102
1103    /// Get triangles regardless of format
1104    pub fn triangles(&self) -> &Vec<u16> {
1105        match self {
1106            SkinFile::New(skin) => &skin.triangles,
1107            SkinFile::Old(skin) => &skin.triangles,
1108        }
1109    }
1110
1111    /// Get submeshes regardless of format
1112    pub fn submeshes(&self) -> &Vec<SkinSubmesh> {
1113        match self {
1114            SkinFile::New(skin) => &skin.submeshes,
1115            SkinFile::Old(skin) => &skin.submeshes,
1116        }
1117    }
1118
1119    /// Get material lookup table regardless of format
1120    pub fn batches(&self) -> &Vec<SkinBatch> {
1121        match self {
1122            SkinFile::New(skin) => &skin.batches,
1123            SkinFile::Old(skin) => &skin.batches,
1124        }
1125    }
1126
1127    /// Convert this skin file to a target version
1128    ///
1129    /// This handles cross-format conversion automatically:
1130    /// - WotLK and earlier use old format (no version field)
1131    /// - Cataclysm and later use new format (with version field)
1132    ///
1133    /// The conversion preserves all mesh data while adjusting the header structure
1134    /// as needed for the target version.
1135    pub fn convert(&self, target_version: M2Version) -> Result<Self> {
1136        let uses_new_format = target_version.uses_new_skin_format();
1137
1138        match (self, uses_new_format) {
1139            // New format -> New format: use existing conversion
1140            (SkinFile::New(skin), true) => {
1141                let converted = skin.convert(target_version)?;
1142                Ok(SkinFile::New(converted))
1143            }
1144
1145            // New format -> Old format: cross-format conversion
1146            (SkinFile::New(skin), false) => {
1147                let old_skin = skin.to_old_format();
1148                Ok(SkinFile::Old(old_skin))
1149            }
1150
1151            // Old format -> New format: cross-format conversion
1152            (SkinFile::Old(skin), true) => {
1153                let new_skin = skin.to_new_format(target_version);
1154                Ok(SkinFile::New(new_skin))
1155            }
1156
1157            // Old format -> Old format: no conversion needed (format is the same)
1158            (SkinFile::Old(skin), false) => Ok(SkinFile::Old(skin.clone())),
1159        }
1160    }
1161
1162    /// Get the bone indices regardless of format
1163    pub fn bone_indices(&self) -> &Vec<u8> {
1164        match self {
1165            SkinFile::New(skin) => &skin.bone_indices,
1166            SkinFile::Old(skin) => &skin.bone_indices,
1167        }
1168    }
1169
1170    /// Check if this is a new format SKIN file
1171    pub fn is_new_format(&self) -> bool {
1172        matches!(self, SkinFile::New(_))
1173    }
1174
1175    /// Check if this is an old format SKIN file
1176    pub fn is_old_format(&self) -> bool {
1177        matches!(self, SkinFile::Old(_))
1178    }
1179}
1180
1181#[derive(Debug, Clone)]
1182pub struct SkinBatch {
1183    pub flags: u8,
1184    pub priority_plane: i8,
1185    pub shader_id: u16,
1186    /// Submesh index
1187    pub skin_section_index: u16,
1188    /// Submesh index 2
1189    pub geoset_index: u16,
1190    /// Index into color lookup table
1191    pub color_index: u16,
1192    /// Index into render flags lookup table
1193    pub material_index: u16,
1194    /// Texture unit index
1195    pub material_layer: u16,
1196    pub texture_count: u16,
1197    /// Index into Texture lookup table
1198    pub texture_combo_index: u16,
1199    /// Index into the texture mapping lookup table.
1200    pub texture_coord_combo_index: u16,
1201    /// Index into transparency lookup table.
1202    pub texture_weight_combo_index: u16,
1203    /// Index into uvanimation lookup table.
1204    pub texture_transform_combo_index: u16,
1205}
1206
1207impl SkinBatch {
1208    /// Parse a submesh from a reader with version-aware structure size
1209    pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
1210        let flags = reader.read_u8()?;
1211        let priority_lane = reader.read_i8()?;
1212        let shader_id = reader.read_u16_le()?;
1213        let skin_section_index = reader.read_u16_le()?;
1214        let geoset_index = reader.read_u16_le()?;
1215        let color_index = reader.read_u16_le()?;
1216        let material_index = reader.read_u16_le()?;
1217        let material_layer = reader.read_u16_le()?;
1218        let texture_count = reader.read_u16_le()?;
1219        let texture_combo_index = reader.read_u16_le()?;
1220        let texture_coord_combo_index = reader.read_u16_le()?;
1221        let texture_weight_combo_index = reader.read_u16_le()?;
1222        let texture_transform_combo_index = reader.read_u16_le()?;
1223        Ok(Self {
1224            flags,
1225            priority_plane: priority_lane,
1226            shader_id,
1227            skin_section_index,
1228            geoset_index,
1229            color_index,
1230            material_index,
1231            material_layer,
1232            texture_count,
1233            texture_combo_index,
1234            texture_coord_combo_index,
1235            texture_weight_combo_index,
1236            texture_transform_combo_index,
1237        })
1238    }
1239
1240    /// Write a submesh to a writer
1241    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
1242        writer.write_u8(self.flags)?;
1243        writer.write_i8(self.priority_plane)?;
1244        writer.write_u16_le(self.shader_id)?;
1245        writer.write_u16_le(self.skin_section_index)?;
1246        writer.write_u16_le(self.geoset_index)?;
1247        writer.write_u16_le(self.color_index)?;
1248        writer.write_u16_le(self.material_index)?;
1249        writer.write_u16_le(self.material_layer)?;
1250        writer.write_u16_le(self.texture_count)?;
1251        writer.write_u16_le(self.texture_combo_index)?;
1252        writer.write_u16_le(self.texture_coord_combo_index)?;
1253        writer.write_u16_le(self.texture_weight_combo_index)?;
1254        writer.write_u16_le(self.texture_transform_combo_index)?;
1255        Ok(())
1256    }
1257}
1258
1259#[cfg(test)]
1260mod tests {
1261    use super::*;
1262    use std::io::Cursor;
1263
1264    #[test]
1265    fn test_format_detection() {
1266        // Test new format detection (version = 1)
1267        let mut data = Vec::new();
1268        data.extend_from_slice(&SKIN_MAGIC);
1269        data.extend_from_slice(&1u32.to_le_bytes()); // version = 1
1270
1271        let mut cursor = Cursor::new(&data);
1272        let is_new = detect_skin_format(&mut cursor).unwrap();
1273        assert!(is_new, "Version 1 should be detected as new format");
1274
1275        // Test old format detection (indices count = 5903)
1276        let mut data = Vec::new();
1277        data.extend_from_slice(&SKIN_MAGIC);
1278        data.extend_from_slice(&5903u32.to_le_bytes()); // large indices count
1279
1280        let mut cursor = Cursor::new(&data);
1281        let is_new = detect_skin_format(&mut cursor).unwrap();
1282        assert!(
1283            !is_new,
1284            "Large indices count should be detected as old format"
1285        );
1286
1287        // Test boundary case (version = 4, still new format)
1288        let mut data = Vec::new();
1289        data.extend_from_slice(&SKIN_MAGIC);
1290        data.extend_from_slice(&4u32.to_le_bytes()); // version = 4
1291
1292        let mut cursor = Cursor::new(&data);
1293        let is_new = detect_skin_format(&mut cursor).unwrap();
1294        assert!(is_new, "Version 4 should be detected as new format");
1295
1296        // Test boundary case (version = 5, old format)
1297        let mut data = Vec::new();
1298        data.extend_from_slice(&SKIN_MAGIC);
1299        data.extend_from_slice(&5u32.to_le_bytes()); // indices count = 5
1300
1301        let mut cursor = Cursor::new(&data);
1302        let is_new = detect_skin_format(&mut cursor).unwrap();
1303        assert!(!is_new, "Indices count 5 should be detected as old format");
1304    }
1305
1306    #[test]
1307    fn test_skin_header_parse() {
1308        let mut data = Vec::new();
1309
1310        // Magic "SKIN"
1311        data.extend_from_slice(&SKIN_MAGIC);
1312
1313        // Version
1314        data.extend_from_slice(&0u32.to_le_bytes());
1315
1316        // Name
1317        data.extend_from_slice(&0u32.to_le_bytes()); // count = 0
1318        data.extend_from_slice(&0u32.to_le_bytes()); // offset = 0
1319
1320        // Vertex count
1321        data.extend_from_slice(&100u32.to_le_bytes());
1322
1323        // Indices
1324        data.extend_from_slice(&200u32.to_le_bytes()); // count = 200
1325        data.extend_from_slice(&0x100u32.to_le_bytes()); // offset = 0x100
1326
1327        // Triangles
1328        data.extend_from_slice(&300u32.to_le_bytes()); // count = 300
1329        data.extend_from_slice(&0x200u32.to_le_bytes()); // offset = 0x200
1330
1331        // Bone indices
1332        data.extend_from_slice(&50u32.to_le_bytes()); // count = 50
1333        data.extend_from_slice(&0x300u32.to_le_bytes()); // offset = 0x300
1334
1335        // Submeshes
1336        data.extend_from_slice(&2u32.to_le_bytes()); // count = 2
1337        data.extend_from_slice(&0x400u32.to_le_bytes()); // offset = 0x400
1338
1339        // Material lookup
1340        data.extend_from_slice(&5u32.to_le_bytes()); // count = 5
1341        data.extend_from_slice(&0x500u32.to_le_bytes()); // offset = 0x500
1342
1343        let mut cursor = Cursor::new(data);
1344        let header = SkinHeader::parse(&mut cursor).unwrap();
1345
1346        assert_eq!(header.magic, SKIN_MAGIC);
1347        assert_eq!(header.version, 0);
1348        assert_eq!(header.vertex_count, 100);
1349        assert_eq!(header.indices.count, 200);
1350        assert_eq!(header.indices.offset, 0x100);
1351        assert_eq!(header.triangles.count, 300);
1352        assert_eq!(header.triangles.offset, 0x200);
1353        assert_eq!(header.bone_indices.count, 50);
1354        assert_eq!(header.bone_indices.offset, 0x300);
1355        assert_eq!(header.submeshes.count, 2);
1356        assert_eq!(header.submeshes.offset, 0x400);
1357        assert_eq!(header.batches.count, 5);
1358        assert_eq!(header.batches.offset, 0x500);
1359        assert!(header.center_position.is_none());
1360        assert!(header.center_bounds.is_none());
1361    }
1362
1363    #[test]
1364    #[ignore] // TODO: Fix test data to properly simulate old format
1365    fn test_skin_file_api() {
1366        // Test format detection first
1367        let new_format_data = create_new_format_test_data();
1368        let old_format_data = create_old_format_test_data();
1369
1370        // Test format detection
1371        let mut cursor = Cursor::new(&new_format_data);
1372        let is_new = detect_skin_format(&mut cursor).unwrap();
1373        assert!(is_new, "New format should be detected");
1374
1375        let mut cursor = Cursor::new(&old_format_data);
1376        let is_new = detect_skin_format(&mut cursor).unwrap();
1377        assert!(!is_new, "Old format should be detected");
1378
1379        // Parse new format
1380        let mut cursor = Cursor::new(new_format_data);
1381        let skin_file = SkinFile::parse(&mut cursor).unwrap();
1382        assert!(skin_file.is_new_format());
1383        assert!(!skin_file.is_old_format());
1384
1385        // Parse old format
1386        let mut cursor = Cursor::new(old_format_data);
1387        let skin_file = SkinFile::parse(&mut cursor).unwrap();
1388        assert!(!skin_file.is_new_format());
1389        assert!(skin_file.is_old_format());
1390
1391        // Test unified API
1392        let indices = skin_file.indices();
1393        let submeshes = skin_file.submeshes();
1394        assert_eq!(indices.len(), 3); // from test data
1395        assert_eq!(submeshes.len(), 0); // empty in test data
1396    }
1397
1398    fn create_new_format_test_data() -> Vec<u8> {
1399        let mut data = Vec::new();
1400
1401        // Magic "SKIN"
1402        data.extend_from_slice(&SKIN_MAGIC);
1403
1404        // Version = 1 (new format indicator)
1405        data.extend_from_slice(&1u32.to_le_bytes());
1406
1407        // Name (empty)
1408        data.extend_from_slice(&0u32.to_le_bytes()); // count = 0
1409        data.extend_from_slice(&0u32.to_le_bytes()); // offset = 0
1410
1411        // Vertex count
1412        data.extend_from_slice(&100u32.to_le_bytes());
1413
1414        // Indices (3 items at end of header)
1415        let indices_offset = (4 + 4 + 8 + 4 + 5 * 8) as u32; // magic + version + name + vertex_count + 5 arrays
1416        data.extend_from_slice(&3u32.to_le_bytes()); // count = 3
1417        data.extend_from_slice(&indices_offset.to_le_bytes()); // offset
1418
1419        // Other arrays (empty)
1420        for _ in 0..4 {
1421            data.extend_from_slice(&0u32.to_le_bytes()); // count = 0
1422            data.extend_from_slice(&0u32.to_le_bytes()); // offset = 0
1423        }
1424
1425        // Index data
1426        data.extend_from_slice(&10u16.to_le_bytes());
1427        data.extend_from_slice(&20u16.to_le_bytes());
1428        data.extend_from_slice(&30u16.to_le_bytes());
1429
1430        data
1431    }
1432
1433    fn create_old_format_test_data() -> Vec<u8> {
1434        let mut data = Vec::new();
1435
1436        // Magic "SKIN"
1437        data.extend_from_slice(&SKIN_MAGIC);
1438
1439        // Indices (3 items) - this is what makes it "old format" (large count)
1440        let indices_offset = (4 + 5 * 8) as u32; // magic + 5 arrays
1441        data.extend_from_slice(&3u32.to_le_bytes()); // count = 3 
1442        data.extend_from_slice(&indices_offset.to_le_bytes()); // offset
1443
1444        // Other arrays (empty)
1445        for _ in 0..4 {
1446            data.extend_from_slice(&0u32.to_le_bytes()); // count = 0
1447            data.extend_from_slice(&0u32.to_le_bytes()); // offset = 0
1448        }
1449
1450        // Index data
1451        data.extend_from_slice(&10u16.to_le_bytes());
1452        data.extend_from_slice(&20u16.to_le_bytes());
1453        data.extend_from_slice(&30u16.to_le_bytes());
1454
1455        data
1456    }
1457
1458    #[test]
1459    fn test_submesh_parse_write() {
1460        let submesh = SkinSubmesh {
1461            id: 1,
1462            level: 0,
1463            vertex_start: 0,
1464            vertex_count: 100,
1465            triangle_start: 0,
1466            triangle_count: 50,
1467            bone_count: 10,
1468            bone_start: 0,
1469            bone_influence: 4,
1470            center: [1.0, 2.0, 3.0],
1471            sort_center: [1.5, 2.5, 3.5],
1472            bounding_radius: 5.0,
1473        };
1474
1475        let mut data = Vec::new();
1476        submesh.write(&mut data).unwrap();
1477
1478        let mut cursor = Cursor::new(data);
1479        let parsed_submesh = SkinSubmesh::parse(&mut cursor).unwrap();
1480
1481        assert_eq!(parsed_submesh.id, 1);
1482        assert_eq!(parsed_submesh.vertex_count, 100);
1483        assert_eq!(parsed_submesh.triangle_count, 50);
1484        assert_eq!(parsed_submesh.bone_count, 10);
1485        assert_eq!(parsed_submesh.bone_influence, 4);
1486        assert_eq!(parsed_submesh.center, [1.0, 2.0, 3.0]);
1487        assert_eq!(parsed_submesh.sort_center, [1.5, 2.5, 3.5]);
1488        assert_eq!(parsed_submesh.bounding_radius, 5.0);
1489    }
1490
1491    #[test]
1492    fn test_skin_format_version_detection() {
1493        use crate::M2Version;
1494
1495        // WotLK and earlier should use old format
1496        assert!(!M2Version::Vanilla.uses_new_skin_format());
1497        assert!(!M2Version::TBC.uses_new_skin_format());
1498        assert!(!M2Version::WotLK.uses_new_skin_format());
1499
1500        // Cataclysm and later should use new format
1501        assert!(M2Version::Cataclysm.uses_new_skin_format());
1502        assert!(M2Version::MoP.uses_new_skin_format());
1503        assert!(M2Version::WoD.uses_new_skin_format());
1504        assert!(M2Version::Legion.uses_new_skin_format());
1505    }
1506
1507    #[test]
1508    fn test_cross_format_conversion_new_to_old() {
1509        use crate::M2Version;
1510
1511        // Create a new format skin
1512        let new_skin = Skin {
1513            header: SkinHeader::new(M2Version::Cataclysm),
1514            indices: vec![0, 1, 2, 3, 4],
1515            triangles: vec![0, 1, 2, 1, 2, 3],
1516            bone_indices: vec![0, 1],
1517            submeshes: vec![SkinSubmesh {
1518                id: 0,
1519                level: 0,
1520                vertex_start: 0,
1521                vertex_count: 5,
1522                triangle_start: 0,
1523                triangle_count: 6,
1524                bone_count: 2,
1525                bone_start: 0,
1526                bone_influence: 2,
1527                center: [0.0, 0.0, 0.0],
1528                sort_center: [0.0, 0.0, 0.0],
1529                bounding_radius: 1.0,
1530            }],
1531            batches: vec![],
1532        };
1533
1534        // Convert to old format
1535        let old_skin = new_skin.to_old_format();
1536
1537        // Verify data is preserved
1538        assert_eq!(old_skin.indices, new_skin.indices);
1539        assert_eq!(old_skin.triangles, new_skin.triangles);
1540        assert_eq!(old_skin.bone_indices, new_skin.bone_indices);
1541        assert_eq!(old_skin.submeshes.len(), new_skin.submeshes.len());
1542    }
1543
1544    #[test]
1545    fn test_cross_format_conversion_old_to_new() {
1546        use crate::M2Version;
1547        use crate::common::M2Array;
1548
1549        // Create an old format skin
1550        let old_skin = OldSkin {
1551            header: OldSkinHeader {
1552                magic: SKIN_MAGIC,
1553                indices: M2Array::new(5, 0),
1554                triangles: M2Array::new(6, 0),
1555                bone_indices: M2Array::new(2, 0),
1556                submeshes: M2Array::new(1, 0),
1557                batches: M2Array::new(0, 0),
1558                bone_count_max: 64,
1559            },
1560            indices: vec![0, 1, 2, 3, 4],
1561            triangles: vec![0, 1, 2, 1, 2, 3],
1562            bone_indices: vec![0, 1],
1563            submeshes: vec![SkinSubmesh {
1564                id: 0,
1565                level: 0,
1566                vertex_start: 0,
1567                vertex_count: 5,
1568                triangle_start: 0,
1569                triangle_count: 6,
1570                bone_count: 2,
1571                bone_start: 0,
1572                bone_influence: 2,
1573                center: [0.0, 0.0, 0.0],
1574                sort_center: [0.0, 0.0, 0.0],
1575                bounding_radius: 1.0,
1576            }],
1577            batches: vec![],
1578        };
1579
1580        // Convert to new format for Cataclysm
1581        let new_skin = old_skin.to_new_format(M2Version::Cataclysm);
1582
1583        // Verify data is preserved
1584        assert_eq!(new_skin.indices, old_skin.indices);
1585        assert_eq!(new_skin.triangles, old_skin.triangles);
1586        assert_eq!(new_skin.bone_indices, old_skin.bone_indices);
1587        assert_eq!(new_skin.submeshes.len(), old_skin.submeshes.len());
1588        assert_eq!(new_skin.header.version, 1); // Cataclysm skin version
1589    }
1590
1591    #[test]
1592    fn test_skinfile_convert_cataclysm_to_wotlk() {
1593        use crate::M2Version;
1594
1595        // Create a new format skin (Cataclysm)
1596        let cata_skin = Skin {
1597            header: SkinHeader::new(M2Version::Cataclysm),
1598            indices: vec![0, 1, 2],
1599            triangles: vec![0, 1, 2],
1600            bone_indices: vec![0],
1601            submeshes: vec![],
1602            batches: vec![],
1603        };
1604
1605        let skin_file = SkinFile::New(cata_skin);
1606
1607        // Convert to WotLK
1608        let converted = skin_file.convert(M2Version::WotLK).unwrap();
1609
1610        // Should now be old format
1611        assert!(converted.is_old_format());
1612        assert!(!converted.is_new_format());
1613    }
1614
1615    #[test]
1616    fn test_skinfile_convert_wotlk_to_cataclysm() {
1617        use crate::M2Version;
1618        use crate::common::M2Array;
1619
1620        // Create an old format skin (WotLK)
1621        let wotlk_skin = OldSkin {
1622            header: OldSkinHeader {
1623                magic: SKIN_MAGIC,
1624                indices: M2Array::new(3, 0),
1625                triangles: M2Array::new(3, 0),
1626                bone_indices: M2Array::new(1, 0),
1627                submeshes: M2Array::new(0, 0),
1628                batches: M2Array::new(0, 0),
1629                bone_count_max: 64,
1630            },
1631            indices: vec![0, 1, 2],
1632            triangles: vec![0, 1, 2],
1633            bone_indices: vec![0],
1634            submeshes: vec![],
1635            batches: vec![],
1636        };
1637
1638        let skin_file = SkinFile::Old(wotlk_skin);
1639
1640        // Convert to Cataclysm
1641        let converted = skin_file.convert(M2Version::Cataclysm).unwrap();
1642
1643        // Should now be new format
1644        assert!(converted.is_new_format());
1645        assert!(!converted.is_old_format());
1646    }
1647}