Skip to main content

wow_m2/chunks/
bone.rs

1use crate::io_ext::{ReadExt, WriteExt};
2use std::io::{Read, Write};
3
4use crate::chunks::m2_track::{M2TrackQuat, M2TrackVec3};
5use crate::common::C3Vector;
6use crate::error::Result;
7use crate::version::M2Version;
8
9bitflags::bitflags! {
10    /// Bone flags as defined in the M2 format
11    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
12    pub struct M2BoneFlags: u32 {
13        /// Spherical billboard
14        const SPHERICAL_BILLBOARD = 0x8;
15        /// Cylindrical billboard lock X
16        const CYLINDRICAL_BILLBOARD_LOCK_X = 0x10;
17        /// Cylindrical billboard lock Y
18        const CYLINDRICAL_BILLBOARD_LOCK_Y = 0x20;
19        /// Cylindrical billboard lock Z
20        const CYLINDRICAL_BILLBOARD_LOCK_Z = 0x40;
21        /// Transformed
22        const TRANSFORMED = 0x200;
23        /// Kinematic bone (requires physics)
24        const KINEMATIC_BONE = 0x400;
25        /// Helper bone
26        const HELPER_BONE = 0x1000;
27        /// Has animation
28        const HAS_ANIMATION = 0x4000;
29        /// Has multiple animations at higher LODs
30        const ANIMATED_AT_HIGHER_LODS = 0x8000;
31        /// Has procedural animation
32        const HAS_PROCEDURAL_ANIMATION = 0x10000;
33        /// Has IK (inverse kinematics)
34        const HAS_IK = 0x20000;
35    }
36}
37
38/// Represents a bone in an M2 model
39#[derive(Debug, Clone)]
40pub struct M2Bone {
41    /// Bone ID
42    pub bone_id: i32,
43    /// Flags
44    pub flags: M2BoneFlags,
45    /// Parent bone ID
46    pub parent_bone: i16,
47    /// Submesh ID
48    pub submesh_id: u16,
49    /// Unknown values (may differ between versions)
50    pub unknown: [u16; 2],
51    /// TBC+ bone name CRC field for debugging/identification (wowdev.wiki: boneNameCRC)
52    /// Only valid for version >= 260. This is a CRC hash of the bone's name string.
53    pub bone_name_crc: Option<u32>,
54    /// Translation animation track
55    pub translation: M2TrackVec3,
56    /// Rotation animation track
57    pub rotation: M2TrackQuat,
58    /// Scale animation track
59    pub scale: M2TrackVec3,
60    /// Pivot point
61    pub pivot: C3Vector,
62}
63
64impl M2Bone {
65    /// Parse a bone from a reader based on the M2 version
66    pub fn parse<R: Read>(reader: &mut R, version: u32) -> Result<Self> {
67        // Read header fields properly
68        let bone_id = reader.read_i32_le()?;
69        let flags = M2BoneFlags::from_bits_retain(reader.read_u32_le()?);
70        let parent_bone = reader.read_i16_le()?;
71        let submesh_id = reader.read_u16_le()?;
72
73        // Version-specific bone name CRC field based on wowdev.wiki and WMVx M2Definitions.h:
74        // - Vanilla (< TBC_MIN=260): NO boneNameCRC field after submeshId
75        // - TBC+ (>= 260): HAS uint32 boneNameCRC field (wowdev.wiki: union with boneNameCRC for debugging)
76        let (unknown, bone_name_crc) = if version >= 260 {
77            // TBC+ format: read the boneNameCRC uint32 field (CRC hash of bone name string)
78            let bone_name_crc = reader.read_u32_le()?;
79            ([0, 0], Some(bone_name_crc)) // Store CRC for debugging/identification
80        } else {
81            // Vanilla format: NO boneNameCRC field - WMVx shows direct transition to AnimationBlocks
82            ([0, 0], None) // No CRC field in vanilla
83        };
84
85        let translation = M2TrackVec3::parse(reader, version)?;
86        let rotation = M2TrackQuat::parse(reader, version)?;
87        let scale = M2TrackVec3::parse(reader, version)?;
88
89        let mut pivot = C3Vector::parse(reader)?;
90
91        // CRITICAL FIX: Handle NaN values in bone pivot coordinates
92        // This addresses corruption where bone pivots contain NaN values
93        if pivot.x.is_nan() || pivot.y.is_nan() || pivot.z.is_nan() {
94            // Replace NaN values with zero (safe default for pivot point)
95            if pivot.x.is_nan() {
96                pivot.x = 0.0;
97            }
98            if pivot.y.is_nan() {
99                pivot.y = 0.0;
100            }
101            if pivot.z.is_nan() {
102                pivot.z = 0.0;
103            }
104        }
105
106        Ok(Self {
107            bone_id,
108            flags,
109            parent_bone,
110            submesh_id,
111            unknown,
112            bone_name_crc,
113            translation,
114            rotation,
115            scale,
116            pivot,
117        })
118    }
119
120    /// Write a bone to a writer
121    pub fn write<W: Write>(&self, writer: &mut W, version: u32) -> Result<()> {
122        writer.write_i32_le(self.bone_id)?;
123        writer.write_u32_le(self.flags.bits())?;
124        writer.write_i16_le(self.parent_bone)?;
125        writer.write_u16_le(self.submesh_id)?;
126
127        if version >= 260 {
128            // TBC+ format: write the boneNameCRC uint32 field
129            writer.write_u32_le(self.bone_name_crc.unwrap_or(0))?; // boneNameCRC field present in TBC+
130        } else {
131            // Vanilla format: NO boneNameCRC field to write based on WMVx reference
132            // WMVx M2Definitions.h shows vanilla goes directly to AnimationBlocks
133        }
134
135        self.translation.write(writer, version)?;
136        self.rotation.write(writer, version)?;
137        self.scale.write(writer, version)?;
138
139        self.pivot.write(writer)?;
140
141        Ok(())
142    }
143
144    /// Convert this bone to a different version (no version differences for bones yet)
145    pub fn convert(&self, _target_version: M2Version) -> Self {
146        self.clone()
147    }
148
149    /// Create a new bone with default values
150    pub fn new(bone_id: i32, parent_bone: i16) -> Self {
151        Self {
152            bone_id,
153            flags: M2BoneFlags::empty(),
154            parent_bone,
155            submesh_id: 0,
156            unknown: [0, 0],
157            bone_name_crc: None,
158            translation: M2TrackVec3::new(),
159            rotation: M2TrackQuat::new(),
160            scale: M2TrackVec3::new(),
161            pivot: C3Vector {
162                x: 0.0,
163                y: 0.0,
164                z: 0.0,
165            },
166        }
167    }
168
169    /// Validate that bone data is reasonable for debugging
170    /// Returns true if the bone data appears valid, false if corrupted
171    pub fn is_valid_for_model(&self, total_bone_count: u32) -> bool {
172        // Check bone_id is reasonable (-1 to 1000 is typical range)
173        if self.bone_id < -1 || self.bone_id > 1000 {
174            return false;
175        }
176
177        // Check parent_bone is reasonable (-1 or within bone count)
178        if self.parent_bone != -1
179            && (self.parent_bone < 0 || self.parent_bone as u32 >= total_bone_count)
180        {
181            return false;
182        }
183
184        // Check animation track counts are reasonable (< 100,000)
185        if self.translation.timestamps.count > 100_000 || self.translation.values.count > 100_000 {
186            return false;
187        }
188
189        if self.rotation.timestamps.count > 100_000 || self.rotation.values.count > 100_000 {
190            return false;
191        }
192
193        if self.scale.timestamps.count > 100_000 || self.scale.values.count > 100_000 {
194            return false;
195        }
196
197        true
198    }
199
200    /// Get a debug string for this bone
201    pub fn debug_info(&self) -> String {
202        let crc_info = if let Some(crc) = self.bone_name_crc {
203            format!(", name_crc=0x{:08x}", crc)
204        } else {
205            String::new()
206        };
207        format!(
208            "Bone(id={}, parent={}, flags=0x{:x}{}, trans_count={}, rot_count={}, scale_count={})",
209            self.bone_id,
210            self.parent_bone,
211            self.flags.bits(),
212            crc_info,
213            self.translation.timestamps.count,
214            self.rotation.timestamps.count,
215            self.scale.timestamps.count
216        )
217    }
218
219    /// Get the bone name CRC field value for TBC+ models
220    pub fn get_bone_name_crc(&self) -> Option<u32> {
221        self.bone_name_crc
222    }
223
224    /// Check if this bone is a billboard
225    pub fn is_billboard(&self) -> bool {
226        self.flags.contains(M2BoneFlags::SPHERICAL_BILLBOARD)
227            || self
228                .flags
229                .contains(M2BoneFlags::CYLINDRICAL_BILLBOARD_LOCK_X)
230            || self
231                .flags
232                .contains(M2BoneFlags::CYLINDRICAL_BILLBOARD_LOCK_Y)
233            || self
234                .flags
235                .contains(M2BoneFlags::CYLINDRICAL_BILLBOARD_LOCK_Z)
236    }
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use std::io::Cursor;
243
244    #[test]
245    fn test_bone_parse() {
246        let mut data = Vec::new();
247
248        // Bone ID
249        data.extend_from_slice(&1i32.to_le_bytes());
250
251        // Flags (TRANSFORMED)
252        data.extend_from_slice(&0x200u32.to_le_bytes());
253
254        // Parent bone
255        data.extend_from_slice(&(-1i16).to_le_bytes());
256
257        // Submesh ID
258        data.extend_from_slice(&0u16.to_le_bytes());
259
260        // NO unknown fields for vanilla (WMVx M2Definitions.h shows vanilla goes directly to animation blocks)
261
262        // Translation track (M2TrackVec3: interpolation_type + global_sequence + ranges + timestamps + values - includes ranges for vanilla)
263        data.extend_from_slice(&0u16.to_le_bytes()); // interpolation_type = None
264        data.extend_from_slice(&65535u16.to_le_bytes()); // global_sequence = none
265        data.extend_from_slice(&0u32.to_le_bytes()); // ranges count
266        data.extend_from_slice(&0u32.to_le_bytes()); // ranges offset
267        data.extend_from_slice(&0u32.to_le_bytes()); // timestamps count
268        data.extend_from_slice(&0u32.to_le_bytes()); // timestamps offset
269        data.extend_from_slice(&0u32.to_le_bytes()); // values count
270        data.extend_from_slice(&0u32.to_le_bytes()); // values offset
271
272        // Rotation track (M2TrackQuat: interpolation_type + global_sequence + ranges + timestamps + values - includes ranges for vanilla)
273        data.extend_from_slice(&0u16.to_le_bytes()); // interpolation_type = None
274        data.extend_from_slice(&65535u16.to_le_bytes()); // global_sequence = none
275        data.extend_from_slice(&0u32.to_le_bytes()); // ranges count
276        data.extend_from_slice(&0u32.to_le_bytes()); // ranges offset
277        data.extend_from_slice(&0u32.to_le_bytes()); // timestamps count
278        data.extend_from_slice(&0u32.to_le_bytes()); // timestamps offset
279        data.extend_from_slice(&0u32.to_le_bytes()); // values count
280        data.extend_from_slice(&0u32.to_le_bytes()); // values offset
281
282        // Scale track (M2TrackVec3: interpolation_type + global_sequence + ranges + timestamps + values - includes ranges for vanilla)
283        data.extend_from_slice(&0u16.to_le_bytes()); // interpolation_type = None
284        data.extend_from_slice(&65535u16.to_le_bytes()); // global_sequence = none
285        data.extend_from_slice(&0u32.to_le_bytes()); // ranges count
286        data.extend_from_slice(&0u32.to_le_bytes()); // ranges offset
287        data.extend_from_slice(&0u32.to_le_bytes()); // timestamps count
288        data.extend_from_slice(&0u32.to_le_bytes()); // timestamps offset
289        data.extend_from_slice(&0u32.to_le_bytes()); // values count
290        data.extend_from_slice(&0u32.to_le_bytes()); // values offset
291
292        // Pivot
293        data.extend_from_slice(&0.0f32.to_le_bytes());
294        data.extend_from_slice(&0.0f32.to_le_bytes());
295        data.extend_from_slice(&0.0f32.to_le_bytes());
296
297        let mut cursor = Cursor::new(data);
298        let bone = M2Bone::parse(&mut cursor, M2Version::Vanilla.to_header_version()).unwrap();
299
300        assert_eq!(bone.bone_id, 1);
301        assert_eq!(bone.flags, M2BoneFlags::TRANSFORMED);
302        assert_eq!(bone.parent_bone, -1);
303        assert_eq!(bone.submesh_id, 0);
304        // Test that tracks were parsed correctly
305        assert!(!bone.translation.has_data()); // No animation data
306        assert!(!bone.rotation.has_data()); // No animation data
307        assert!(!bone.scale.has_data()); // No animation data
308    }
309
310    #[test]
311    fn test_bone_validation_constraints() {
312        // Test that validates bone data constraints as described in the issue
313        let mut data = Vec::new();
314
315        // Create a bone with reasonable values
316        data.extend_from_slice(&1i32.to_le_bytes()); // bone_id: 1 (reasonable)
317        data.extend_from_slice(&0x200u32.to_le_bytes()); // flags: TRANSFORMED
318        data.extend_from_slice(&0i16.to_le_bytes()); // parent_bone: 0 (reasonable for 96-bone model)
319        data.extend_from_slice(&0u16.to_le_bytes()); // submesh_id: 0
320        // NO unknown fields for vanilla (WMVx M2Definitions.h shows vanilla goes directly to animation blocks)
321
322        // Add reasonable animation track data (includes ranges for vanilla)
323        for _ in 0..3 {
324            // translation, rotation, scale
325            data.extend_from_slice(&0u16.to_le_bytes()); // interpolation_type: None
326            data.extend_from_slice(&65535u16.to_le_bytes()); // global_sequence: none
327            data.extend_from_slice(&0u32.to_le_bytes()); // ranges count
328            data.extend_from_slice(&0u32.to_le_bytes()); // ranges offset
329            data.extend_from_slice(&5u32.to_le_bytes()); // timestamps count (reasonable)
330            data.extend_from_slice(&1000u32.to_le_bytes()); // timestamps offset (reasonable)
331            data.extend_from_slice(&5u32.to_le_bytes()); // values count (reasonable)
332            data.extend_from_slice(&1200u32.to_le_bytes()); // values offset (reasonable)
333        }
334
335        data.extend_from_slice(&0.0f32.to_le_bytes()); // pivot.x
336        data.extend_from_slice(&1.0f32.to_le_bytes()); // pivot.y
337        data.extend_from_slice(&0.0f32.to_le_bytes()); // pivot.z
338
339        // Now create a second bone with corrupted data (as described in issue)
340        data.extend_from_slice(&1000i32.to_le_bytes()); // bone_id: 1000 (high but valid - was causing test failure)
341        data.extend_from_slice(&0u32.to_le_bytes()); // flags: 0
342        data.extend_from_slice(&50i16.to_le_bytes()); // parent_bone: 50 (high but reasonable for HumanMale's ~96 bones)
343        data.extend_from_slice(&0u16.to_le_bytes()); // submesh_id: 0
344        // NO unknown fields for vanilla (WMVx M2Definitions.h shows vanilla goes directly to animation blocks)
345
346        // Add unreasonable animation track data (as seen in corruption, includes ranges for vanilla)
347        for count in [4294901760u32, 22768u32, 100u32] {
348            // translation, rotation, scale
349            data.extend_from_slice(&0u16.to_le_bytes()); // interpolation_type: None
350            data.extend_from_slice(&65535u16.to_le_bytes()); // global_sequence: none
351            data.extend_from_slice(&0u32.to_le_bytes()); // ranges count
352            data.extend_from_slice(&0u32.to_le_bytes()); // ranges offset
353            data.extend_from_slice(&count.to_le_bytes()); // timestamps count (unreasonable)
354            data.extend_from_slice(&1000u32.to_le_bytes()); // timestamps offset
355            data.extend_from_slice(&count.to_le_bytes()); // values count (unreasonable)
356            data.extend_from_slice(&1200u32.to_le_bytes()); // values offset
357        }
358
359        data.extend_from_slice(&0.0f32.to_le_bytes()); // pivot.x
360        data.extend_from_slice(&1.0f32.to_le_bytes()); // pivot.y
361        data.extend_from_slice(&0.0f32.to_le_bytes()); // pivot.z
362
363        println!(
364            "Test data created: {} bytes (2 bones * 108 = 216 expected)",
365            data.len()
366        );
367        assert_eq!(data.len(), 216); // 2 bones * 108 bytes each for vanilla with WMVx-aligned structure
368
369        let mut cursor = Cursor::new(&data);
370
371        // Parse first bone - should be reasonable
372        let bone1 = M2Bone::parse(&mut cursor, 256).unwrap();
373
374        println!(
375            "Bone 1: id={}, parent={}, translation_count={}",
376            bone1.bone_id, bone1.parent_bone, bone1.translation.timestamps.count
377        );
378
379        // Validate first bone has reasonable values
380        assert_eq!(bone1.bone_id, 1);
381        assert_eq!(bone1.parent_bone, 0);
382        assert_eq!(bone1.translation.timestamps.count, 5);
383        assert_eq!(bone1.rotation.timestamps.count, 5);
384        assert_eq!(bone1.scale.timestamps.count, 5);
385
386        // Parse second bone - this would show corruption if present
387        let bone2 = M2Bone::parse(&mut cursor, 256).unwrap();
388
389        println!(
390            "Bone 2: id={}, parent={}, translation_count={}",
391            bone2.bone_id, bone2.parent_bone, bone2.translation.timestamps.count
392        );
393
394        // This bone should have the high but valid values we inserted
395        assert_eq!(bone2.bone_id, 1000);
396        assert_eq!(bone2.parent_bone, 50);
397        assert_eq!(bone2.translation.timestamps.count, 4294901760);
398        assert_eq!(bone2.rotation.timestamps.count, 22768);
399        assert_eq!(bone2.scale.timestamps.count, 100);
400
401        // Verify cursor position (2 bones * 108 bytes each = 216 bytes total)
402        assert_eq!(cursor.position(), 216);
403
404        println!(
405            "✓ Both bones parsed with expected values (including intentionally corrupted second bone)"
406        );
407        println!("This confirms M2Bone::parse correctly reads what's in the data,");
408        println!("so the issue must be in the source data or cursor positioning.");
409    }
410
411    #[test]
412    fn test_m2track_byte_consumption_vanilla() {
413        // Test exact byte consumption of M2Track parsing for Vanilla (WITH ranges)
414        let mut data = Vec::new();
415
416        // Create M2Track data for version 256 (SHOULD include ranges per WMVx reference)
417        data.extend_from_slice(&1u16.to_le_bytes()); // interpolation_type: Linear
418        data.extend_from_slice(&65535u16.to_le_bytes()); // global_sequence: none
419        data.extend_from_slice(&1u32.to_le_bytes()); // ranges count
420        data.extend_from_slice(&800u32.to_le_bytes()); // ranges offset
421        data.extend_from_slice(&3u32.to_le_bytes()); // timestamps count
422        data.extend_from_slice(&1000u32.to_le_bytes()); // timestamps offset
423        data.extend_from_slice(&3u32.to_le_bytes()); // values count
424        data.extend_from_slice(&1200u32.to_le_bytes()); // values offset
425
426        assert_eq!(
427            data.len(),
428            28,
429            "M2Track test data should be exactly 28 bytes for Vanilla"
430        );
431
432        let mut cursor = Cursor::new(&data);
433        let pos_before = cursor.position();
434
435        // Parse M2TrackVec3 (should consume all 28 bytes)
436        let track = M2TrackVec3::parse(&mut cursor, 256).unwrap();
437
438        let pos_after = cursor.position();
439        let bytes_consumed = pos_after - pos_before;
440
441        println!(
442            "M2Track parsing: consumed {} bytes (expected 28)",
443            bytes_consumed
444        );
445        println!(
446            "Track details: interp={:?}, timestamps_count={}, values_count={}",
447            track.base.interpolation_type, track.timestamps.count, track.values.count
448        );
449
450        // Critical test: M2Track should consume exactly 28 bytes for version 256 (Vanilla) per WMVx reference
451        assert_eq!(
452            bytes_consumed, 28,
453            "M2Track should consume exactly 28 bytes for version 256, but consumed {}",
454            bytes_consumed
455        );
456
457        // Verify the ranges field WAS parsed (Vanilla should have ranges per WMVx reference)
458        assert!(
459            track.ranges.is_some(),
460            "M2Track SHOULD have ranges field for version 256 (Vanilla)"
461        );
462
463        println!("✓ M2Track consumes exactly 28 bytes as expected for Vanilla");
464    }
465
466    #[test]
467    fn test_m2track_byte_consumption_tbc() {
468        // Test exact byte consumption of M2Track parsing for TBC (has ranges)
469        let mut data = Vec::new();
470
471        // Create M2Track data for version 260 (should include ranges)
472        data.extend_from_slice(&1u16.to_le_bytes()); // interpolation_type: Linear
473        data.extend_from_slice(&65535u16.to_le_bytes()); // global_sequence: none
474        data.extend_from_slice(&0u32.to_le_bytes()); // ranges count
475        data.extend_from_slice(&0u32.to_le_bytes()); // ranges offset
476        data.extend_from_slice(&3u32.to_le_bytes()); // timestamps count
477        data.extend_from_slice(&1000u32.to_le_bytes()); // timestamps offset
478        data.extend_from_slice(&3u32.to_le_bytes()); // values count
479        data.extend_from_slice(&1200u32.to_le_bytes()); // values offset
480
481        assert_eq!(
482            data.len(),
483            28,
484            "M2Track test data should be exactly 28 bytes for TBC"
485        );
486
487        let mut cursor = Cursor::new(&data);
488        let pos_before = cursor.position();
489
490        // Parse M2TrackVec3 (should consume all 28 bytes)
491        let track = M2TrackVec3::parse(&mut cursor, 260).unwrap();
492
493        let pos_after = cursor.position();
494        let bytes_consumed = pos_after - pos_before;
495
496        println!(
497            "M2Track parsing: consumed {} bytes (expected 28)",
498            bytes_consumed
499        );
500        println!(
501            "Track details: interp={:?}, timestamps_count={}, values_count={}",
502            track.base.interpolation_type, track.timestamps.count, track.values.count
503        );
504
505        // Critical test: M2Track should consume exactly 28 bytes for version 260 (TBC)
506        assert_eq!(
507            bytes_consumed, 28,
508            "M2Track should consume exactly 28 bytes for version 260, but consumed {}",
509            bytes_consumed
510        );
511
512        // Verify the ranges field was parsed (TBC should have ranges)
513        assert!(
514            track.ranges.is_some(),
515            "M2Track should have ranges field for version 260 (TBC)"
516        );
517
518        println!("✓ M2Track consumes exactly 28 bytes as expected for TBC");
519    }
520
521    #[test]
522    fn test_sequential_bone_parsing() {
523        // Test that multiple bones can be parsed sequentially
524        let mut data = Vec::new();
525
526        // Create data for 3 bones
527        for bone_id in 0i32..3 {
528            // Bone ID
529            data.extend_from_slice(&bone_id.to_le_bytes());
530
531            // Flags (TRANSFORMED)
532            data.extend_from_slice(&0x200u32.to_le_bytes());
533
534            // Parent bone (bone 0 has no parent, others have previous as parent)
535            let parent = if bone_id == 0 { -1 } else { bone_id - 1 };
536            data.extend_from_slice(&(parent as i16).to_le_bytes());
537
538            // Submesh ID
539            data.extend_from_slice(&0u16.to_le_bytes());
540
541            // NO unknown fields for vanilla (WMVx M2Definitions.h shows vanilla goes directly to animation blocks)
542
543            // Translation track (M2TrackVec3: interpolation_type + global_sequence + ranges + timestamps + values - includes ranges for vanilla)
544            data.extend_from_slice(&0u16.to_le_bytes()); // interpolation_type = None
545            data.extend_from_slice(&65535u16.to_le_bytes()); // global_sequence = none
546            data.extend_from_slice(&0u32.to_le_bytes()); // ranges count
547            data.extend_from_slice(&0u32.to_le_bytes()); // ranges offset
548            data.extend_from_slice(&0u32.to_le_bytes()); // timestamps count
549            data.extend_from_slice(&0u32.to_le_bytes()); // timestamps offset
550            data.extend_from_slice(&0u32.to_le_bytes()); // values count
551            data.extend_from_slice(&0u32.to_le_bytes()); // values offset
552
553            // Rotation track (M2TrackQuat: interpolation_type + global_sequence + ranges + timestamps + values - includes ranges for vanilla)
554            data.extend_from_slice(&0u16.to_le_bytes()); // interpolation_type = None
555            data.extend_from_slice(&65535u16.to_le_bytes()); // global_sequence = none
556            data.extend_from_slice(&0u32.to_le_bytes()); // ranges count
557            data.extend_from_slice(&0u32.to_le_bytes()); // ranges offset
558            data.extend_from_slice(&0u32.to_le_bytes()); // timestamps count
559            data.extend_from_slice(&0u32.to_le_bytes()); // timestamps offset
560            data.extend_from_slice(&0u32.to_le_bytes()); // values count
561            data.extend_from_slice(&0u32.to_le_bytes()); // values offset
562
563            // Scale track (M2TrackVec3: interpolation_type + global_sequence + ranges + timestamps + values - includes ranges for vanilla)
564            data.extend_from_slice(&0u16.to_le_bytes()); // interpolation_type = None
565            data.extend_from_slice(&65535u16.to_le_bytes()); // global_sequence = none
566            data.extend_from_slice(&0u32.to_le_bytes()); // ranges count
567            data.extend_from_slice(&0u32.to_le_bytes()); // ranges offset
568            data.extend_from_slice(&0u32.to_le_bytes()); // timestamps count
569            data.extend_from_slice(&0u32.to_le_bytes()); // timestamps offset
570            data.extend_from_slice(&0u32.to_le_bytes()); // values count
571            data.extend_from_slice(&0u32.to_le_bytes()); // values offset
572
573            // Pivot
574            data.extend_from_slice(&0.0f32.to_le_bytes());
575            data.extend_from_slice(&0.0f32.to_le_bytes());
576            data.extend_from_slice(&0.0f32.to_le_bytes());
577        }
578
579        println!(
580            "Total test data: {} bytes (expected: 3 * 108 = 324)",
581            data.len()
582        );
583        assert_eq!(data.len(), 324); // 3 bones * 108 bytes each for vanilla
584
585        let mut cursor = Cursor::new(data);
586
587        // Parse all 3 bones sequentially
588        let mut bones = Vec::new();
589        for i in 0..3 {
590            let pos_before = cursor.position();
591            let bone = M2Bone::parse(&mut cursor, M2Version::Vanilla.to_header_version()).unwrap();
592            let pos_after = cursor.position();
593            let bytes_consumed = pos_after - pos_before;
594
595            println!(
596                "Bone {}: id={}, parent={}, consumed {} bytes (pos: {} -> {})",
597                i, bone.bone_id, bone.parent_bone, bytes_consumed, pos_before, pos_after
598            );
599
600            // Verify bone data is correct
601            assert_eq!(bone.bone_id, i);
602            assert_eq!(bone.flags, M2BoneFlags::TRANSFORMED);
603            if i == 0 {
604                assert_eq!(bone.parent_bone, -1);
605            } else {
606                assert_eq!(bone.parent_bone, (i - 1) as i16);
607            }
608            assert_eq!(bone.submesh_id, 0);
609
610            // Each bone should consume exactly 108 bytes for vanilla (WITH ranges in M2Track)
611            // Bone structure: 4+4+2+2 (header, no unknown for vanilla) + 28*3 (3 M2Tracks with ranges) + 12 (pivot) = 108 bytes
612            assert_eq!(
613                bytes_consumed, 108,
614                "Bone {} consumed {} bytes, expected 108",
615                i, bytes_consumed
616            );
617
618            bones.push(bone);
619        }
620
621        // Verify we consumed all data (3 bones * 108 bytes = 324 bytes)
622        assert_eq!(cursor.position(), 324);
623        println!("✓ All 3 bones parsed successfully with correct sequential alignment");
624    }
625
626    #[test]
627    fn test_nan_pivot_fix() {
628        // Test the fix for NaN values in bone pivot coordinates (Issue #2)
629        let mut data = Vec::new();
630
631        // Bone ID
632        data.extend_from_slice(&1i32.to_le_bytes());
633
634        // Flags (TRANSFORMED)
635        data.extend_from_slice(&0x200u32.to_le_bytes());
636
637        // Parent bone
638        data.extend_from_slice(&(-1i16).to_le_bytes());
639
640        // Submesh ID
641        data.extend_from_slice(&0u16.to_le_bytes());
642
643        // Animation tracks (empty for vanilla)
644        for _ in 0..3 {
645            // translation, rotation, scale
646            data.extend_from_slice(&0u16.to_le_bytes()); // interpolation_type = None
647            data.extend_from_slice(&65535u16.to_le_bytes()); // global_sequence = none
648            data.extend_from_slice(&0u32.to_le_bytes()); // ranges count
649            data.extend_from_slice(&0u32.to_le_bytes()); // ranges offset
650            data.extend_from_slice(&0u32.to_le_bytes()); // timestamps count
651            data.extend_from_slice(&0u32.to_le_bytes()); // timestamps offset
652            data.extend_from_slice(&0u32.to_le_bytes()); // values count
653            data.extend_from_slice(&0u32.to_le_bytes()); // values offset
654        }
655
656        // CRITICAL: Pivot with NaN values (simulating corruption)
657        data.extend_from_slice(&f32::NAN.to_le_bytes()); // pivot.x = NaN
658        data.extend_from_slice(&2.5f32.to_le_bytes()); // pivot.y = valid
659        data.extend_from_slice(&f32::NAN.to_le_bytes()); // pivot.z = NaN
660
661        let mut cursor = Cursor::new(data);
662        let bone = M2Bone::parse(&mut cursor, M2Version::Vanilla.to_header_version()).unwrap();
663
664        // Verify NaN values were fixed (replaced with 0.0)
665        assert_eq!(bone.pivot.x, 0.0, "NaN pivot.x should be replaced with 0.0");
666        assert_eq!(bone.pivot.y, 2.5, "Valid pivot.y should be preserved");
667        assert_eq!(bone.pivot.z, 0.0, "NaN pivot.z should be replaced with 0.0");
668
669        // Verify no NaN values remain
670        assert!(
671            !bone.pivot.x.is_nan(),
672            "pivot.x should not be NaN after parsing"
673        );
674        assert!(!bone.pivot.y.is_nan(), "pivot.y should not be NaN");
675        assert!(
676            !bone.pivot.z.is_nan(),
677            "pivot.z should not be NaN after parsing"
678        );
679
680        // Other bone data should be preserved
681        assert_eq!(bone.bone_id, 1);
682        assert_eq!(bone.parent_bone, -1);
683
684        println!(
685            "✓ NaN pivot coordinates fixed: (NaN, 2.5, NaN) -> ({}, {}, {})",
686            bone.pivot.x, bone.pivot.y, bone.pivot.z
687        );
688    }
689
690    #[test]
691    fn test_bone_write() {
692        let bone = M2Bone {
693            bone_id: 1,
694            flags: M2BoneFlags::TRANSFORMED,
695            parent_bone: -1,
696            submesh_id: 0,
697            unknown: [0, 0],
698            bone_name_crc: Some(0x12345678), // Test CRC value for TBC+
699            translation: M2TrackVec3::new(),
700            rotation: M2TrackQuat::new(),
701            scale: M2TrackVec3::new(),
702            pivot: C3Vector {
703                x: 0.0,
704                y: 0.0,
705                z: 0.0,
706            },
707        };
708
709        let mut data = Vec::new();
710        bone.write(&mut data, 260).unwrap(); // BC version
711
712        // Verify that the written data has the correct length
713        // BC M2Bone size: 4 + 4 + 2 + 2 + 4 (extra unknown) + 28*3 (M2Track each with ranges) + 12 (pivot) = 112 bytes
714        assert_eq!(data.len(), 112);
715
716        // Test Vanilla version too
717        let mut vanilla_data = Vec::new();
718        bone.write(&mut vanilla_data, 256).unwrap(); // Vanilla version
719
720        // Vanilla M2Bone size: 4 + 4 + 2 + 2 (no unknown fields) + 28*3 (M2Track each with ranges) + 12 (pivot) = 108 bytes
721        assert_eq!(vanilla_data.len(), 108);
722    }
723
724    #[test]
725    fn test_bone_write_wotlk() {
726        let bone = M2Bone {
727            bone_id: 1,
728            flags: M2BoneFlags::TRANSFORMED,
729            parent_bone: -1,
730            submesh_id: 0,
731            unknown: [0, 0],
732            bone_name_crc: Some(0x12345678),
733            translation: M2TrackVec3::new(),
734            rotation: M2TrackQuat::new(),
735            scale: M2TrackVec3::new(),
736            pivot: C3Vector {
737                x: 0.0,
738                y: 0.0,
739                z: 0.0,
740            },
741        };
742
743        let mut data = Vec::new();
744        bone.write(&mut data, 264).unwrap(); // WotLK version
745
746        println!("WotLK bone write size: {} bytes (expected 88)", data.len());
747        assert_eq!(
748            data.len(),
749            88,
750            "WotLK bone should be 88 bytes, got {}",
751            data.len()
752        );
753    }
754}