wow_m2/
model.rs

1use std::collections::HashMap;
2use std::fs::File;
3use std::io::{ErrorKind, Read, Seek, SeekFrom, Write};
4
5use crate::io_ext::ReadExt;
6use std::path::Path;
7
8use crate::chunks::animation::{M2Animation, M2AnimationBlock};
9use crate::chunks::attachment::M2Attachment;
10use crate::chunks::bone::M2Bone;
11use crate::chunks::camera::M2Camera;
12use crate::chunks::color_animation::M2ColorAnimation;
13use crate::chunks::event::M2Event;
14use crate::chunks::infrastructure::{ChunkHeader, ChunkReader};
15use crate::chunks::light::M2Light;
16use crate::chunks::m2_track::{M2Track, M2TrackQuat, M2TrackVec3};
17use crate::chunks::material::M2Material;
18use crate::chunks::particle_emitter::M2ParticleEmitter;
19use crate::chunks::ribbon_emitter::M2RibbonEmitter;
20use crate::chunks::texture_animation::M2TextureAnimation;
21use crate::chunks::transparency_animation::M2TransparencyAnimation;
22use crate::chunks::{
23    AfraChunk, AnimationFileIds, BoneData, BoneFileIds, CollisionMeshData, DbocChunk, DpivChunk,
24    EdgeFadeData, ExtendedParticleData, GeometryParticleIds, LightingDetails, LodData, M2Texture,
25    M2Vertex, ModelAlphaData, ParentAnimationBlacklist, ParentAnimationData, ParentEventData,
26    ParentSequenceBounds, ParticleGeosetData, PhysicsData, PhysicsFileDataChunk, PhysicsFileId,
27    RecursiveParticleIds, SkeletonData, SkeletonFileId, SkinFileIds, TextureAnimationChunk,
28    TextureFileIds, WaterfallEffect,
29};
30use crate::common::{M2Array, M2Parse, read_array, read_raw_bytes};
31use crate::error::{M2Error, Result};
32use crate::file_resolver::FileResolver;
33use crate::header::{M2_MAGIC_CHUNKED, M2_MAGIC_LEGACY, M2Header, M2ModelFlags};
34use crate::version::M2Version;
35
36/// M2 format variants
37#[derive(Debug, Clone)]
38pub enum M2Format {
39    /// Legacy MD20 format (Pre-Legion)
40    Legacy(M2Model),
41    /// Chunked MD21 format (Legion+)
42    Chunked(M2Model),
43}
44
45/// Main M2 model structure
46#[derive(Debug, Clone)]
47pub struct M2Model {
48    /// M2 header
49    pub header: M2Header,
50    /// Model name
51    pub name: Option<String>,
52    /// Global sequences
53    pub global_sequences: Vec<u32>,
54    /// Animations
55    pub animations: Vec<M2Animation>,
56    /// Animation lookups
57    pub animation_lookup: Vec<u16>,
58    /// Bones
59    pub bones: Vec<M2Bone>,
60    /// Key bone lookups
61    pub key_bone_lookup: Vec<u16>,
62    /// Vertices
63    pub vertices: Vec<M2Vertex>,
64    /// Textures
65    pub textures: Vec<M2Texture>,
66    /// Materials (render flags)
67    pub materials: Vec<M2Material>,
68    /// Particle emitters
69    pub particle_emitters: Vec<M2ParticleEmitter>,
70    /// Ribbon emitters
71    pub ribbon_emitters: Vec<M2RibbonEmitter>,
72    /// Texture animations
73    pub texture_animations: Vec<M2TextureAnimation>,
74    /// Color animations
75    pub color_animations: Vec<M2ColorAnimation>,
76    /// Transparency animations
77    pub transparency_animations: Vec<M2TransparencyAnimation>,
78    /// Events
79    pub events: Vec<M2Event>,
80    /// Attachments
81    pub attachments: Vec<M2Attachment>,
82    /// Cameras
83    pub cameras: Vec<M2Camera>,
84    /// Lights
85    pub lights: Vec<M2Light>,
86    /// Raw data for other sections
87    /// This is used to preserve data that we don't fully parse yet
88    pub raw_data: M2RawData,
89
90    /// Chunked format data (Legion+ only)
91    /// Contains FileDataID references for external files
92    pub skin_file_ids: Option<SkinFileIds>,
93    /// Animation file IDs (Legion+ only)
94    pub animation_file_ids: Option<AnimationFileIds>,
95    /// Texture file IDs (Legion+ only)
96    pub texture_file_ids: Option<TextureFileIds>,
97    /// Physics file ID (Legion+ only)
98    pub physics_file_id: Option<PhysicsFileId>,
99    /// Skeleton file ID (Legion+ only)
100    pub skeleton_file_id: Option<SkeletonFileId>,
101    /// Bone file IDs (Legion+ only)
102    pub bone_file_ids: Option<BoneFileIds>,
103    /// Level of detail data (Legion+ only)
104    pub lod_data: Option<LodData>,
105
106    /// Advanced rendering features (Legion+ only)
107    /// Extended particle data (EXPT/EXP2 chunks)
108    pub extended_particle_data: Option<ExtendedParticleData>,
109    /// Parent animation blacklist (PABC chunk)
110    pub parent_animation_blacklist: Option<ParentAnimationBlacklist>,
111    /// Parent animation data (PADC chunk)
112    pub parent_animation_data: Option<ParentAnimationData>,
113    /// Waterfall effects (WFV1/WFV2/WFV3 chunks)
114    pub waterfall_effect: Option<WaterfallEffect>,
115    /// Edge fade rendering (EDGF chunk)
116    pub edge_fade_data: Option<EdgeFadeData>,
117    /// Model alpha calculations (NERF chunk)
118    pub model_alpha_data: Option<ModelAlphaData>,
119    /// Lighting details (DETL chunk)
120    pub lighting_details: Option<LightingDetails>,
121    /// Recursive particle model IDs (RPID chunk)
122    pub recursive_particle_ids: Option<RecursiveParticleIds>,
123    /// Geometry particle model IDs (GPID chunk)
124    pub geometry_particle_ids: Option<GeometryParticleIds>,
125
126    /// Phase 7 specialized chunks
127    /// TXAC texture animation chunk
128    pub texture_animation_chunk: Option<TextureAnimationChunk>,
129    /// PGD1 particle geoset data
130    pub particle_geoset_data: Option<ParticleGeosetData>,
131    /// DBOC chunk (purpose unknown)
132    pub dboc_chunk: Option<DbocChunk>,
133    /// AFRA chunk (purpose unknown)
134    pub afra_chunk: Option<AfraChunk>,
135    /// DPIV chunk (collision mesh for player housing)
136    pub dpiv_chunk: Option<DpivChunk>,
137    /// PSBC chunk (parent sequence bounds)
138    pub parent_sequence_bounds: Option<ParentSequenceBounds>,
139    /// PEDC chunk (parent event data)
140    pub parent_event_data: Option<ParentEventData>,
141    /// PCOL chunk (collision mesh data)
142    pub collision_mesh_data: Option<CollisionMeshData>,
143    /// PFDC chunk (physics file data)
144    pub physics_file_data: Option<PhysicsFileDataChunk>,
145}
146
147/// Type of animation track within a bone
148#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
149pub enum TrackType {
150    /// Translation (position) animation
151    #[default]
152    Translation,
153    /// Rotation animation (quaternion)
154    Rotation,
155    /// Scale animation
156    Scale,
157}
158
159/// Type of animation track within a particle emitter
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
161pub enum ParticleTrackType {
162    /// Emission speed animation
163    #[default]
164    EmissionSpeed,
165    /// Emission rate animation
166    EmissionRate,
167    /// Emission area animation
168    EmissionArea,
169    /// XY scale animation (C2Vector = 8 bytes)
170    XYScale,
171    /// Z scale animation
172    ZScale,
173    /// Color animation (M2Color = 12 bytes)
174    Color,
175    /// Transparency animation
176    Transparency,
177    /// Size animation
178    Size,
179    /// Intensity animation
180    Intensity,
181    /// Z source animation
182    ZSource,
183}
184
185impl ParticleTrackType {
186    /// Returns the size in bytes of each value for this track type
187    pub fn value_size(&self) -> usize {
188        match self {
189            ParticleTrackType::XYScale => 8, // C2Vector
190            ParticleTrackType::Color => 12,  // M2Color (3 floats)
191            _ => 4,                          // f32
192        }
193    }
194}
195
196/// Type of animation track within a ribbon emitter
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
198pub enum RibbonTrackType {
199    /// Color animation (M2Color = 12 bytes)
200    #[default]
201    Color,
202    /// Alpha (transparency) animation
203    Alpha,
204    /// Height above center animation
205    HeightAbove,
206    /// Height below center animation
207    HeightBelow,
208}
209
210impl RibbonTrackType {
211    /// Returns the size in bytes of each value for this track type
212    pub fn value_size(&self) -> usize {
213        match self {
214            RibbonTrackType::Color => 12, // M2Color (3 floats)
215            _ => 4,                       // f32
216        }
217    }
218}
219
220/// Type of animation track within a texture animation
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
222pub enum TextureTrackType {
223    /// U coordinate translation animation
224    #[default]
225    TranslationU,
226    /// V coordinate translation animation
227    TranslationV,
228    /// Rotation animation
229    Rotation,
230    /// U coordinate scale animation
231    ScaleU,
232    /// V coordinate scale animation
233    ScaleV,
234}
235
236impl TextureTrackType {
237    /// Returns the size in bytes of each value for this track type
238    pub fn value_size(&self) -> usize {
239        4 // All texture animation tracks use f32
240    }
241}
242
243/// Raw animation data for a single bone track
244///
245/// This preserves the exact bytes from the original file for animation keyframes,
246/// allowing roundtrip serialization without data loss.
247#[derive(Debug, Clone, Default)]
248pub struct BoneAnimationRaw {
249    /// Index of the bone this track belongs to
250    pub bone_index: usize,
251    /// Type of animation track (translation, rotation, scale)
252    pub track_type: TrackType,
253    /// Raw timestamp bytes (4 bytes per timestamp)
254    pub timestamps: Vec<u8>,
255    /// Raw keyframe value bytes (12 bytes for Vec3, 8 bytes for CompQuat)
256    pub values: Vec<u8>,
257    /// Raw interpolation range bytes (pre-WotLK only, 8 bytes per range)
258    pub ranges: Option<Vec<u8>>,
259    /// Original file offset for timestamps array
260    pub original_timestamps_offset: u32,
261    /// Original file offset for values array
262    pub original_values_offset: u32,
263    /// Original file offset for ranges array (pre-WotLK only)
264    pub original_ranges_offset: Option<u32>,
265}
266
267/// Raw embedded skin data for a single ModelView in pre-WotLK M2 files
268///
269/// Pre-WotLK (versions 256-263) have skin data embedded in the M2 file.
270/// Each ModelView structure (44 bytes) contains M2Arrays pointing to:
271/// - indices (vertex indices into the model's vertex buffer)
272/// - triangles (triangle indices)
273/// - submeshes (mesh subdivision info)
274/// - batches (texture unit assignments)
275#[derive(Debug, Clone, Default)]
276pub struct EmbeddedSkinRaw {
277    /// The raw ModelView structure bytes (44 bytes)
278    pub model_view: Vec<u8>,
279    /// Indices data referenced by the first M2Array
280    pub indices: Vec<u8>,
281    /// Triangles data referenced by the second M2Array
282    pub triangles: Vec<u8>,
283    /// Vertex properties data (usually empty or minimal)
284    pub properties: Vec<u8>,
285    /// Submeshes data
286    pub submeshes: Vec<u8>,
287    /// Batches/texture units data
288    pub batches: Vec<u8>,
289    /// Original offset of the ModelView structure
290    pub original_model_view_offset: u32,
291    /// Original offsets for each M2Array's data
292    pub original_indices_offset: u32,
293    pub original_triangles_offset: u32,
294    pub original_properties_offset: u32,
295    pub original_submeshes_offset: u32,
296    pub original_batches_offset: u32,
297}
298
299/// Raw animation data for a single particle emitter track
300///
301/// This preserves the exact bytes from the original file for particle animation keyframes,
302/// allowing roundtrip serialization without data loss. Particle emitters have 10 different
303/// animation tracks (emission speed, rate, color, etc.).
304#[derive(Debug, Clone, Default)]
305pub struct ParticleAnimationRaw {
306    /// Index of the particle emitter this track belongs to
307    pub emitter_index: usize,
308    /// Type of animation track
309    pub track_type: ParticleTrackType,
310    /// Raw interpolation range bytes (8 bytes per range: start u32 + end u32)
311    pub interpolation_ranges: Vec<u8>,
312    /// Raw timestamp bytes (4 bytes per timestamp)
313    pub timestamps: Vec<u8>,
314    /// Raw keyframe value bytes (size depends on track type)
315    pub values: Vec<u8>,
316    /// Original file offset for interpolation_ranges array
317    pub original_ranges_offset: u32,
318    /// Original file offset for timestamps array
319    pub original_timestamps_offset: u32,
320    /// Original file offset for values array
321    pub original_values_offset: u32,
322}
323
324/// Raw animation data for a single ribbon emitter track
325///
326/// This preserves the exact bytes from the original file for ribbon animation keyframes,
327/// allowing roundtrip serialization without data loss. Ribbon emitters have 4 different
328/// animation tracks (color, alpha, height_above, height_below).
329#[derive(Debug, Clone, Default)]
330pub struct RibbonAnimationRaw {
331    /// Index of the ribbon emitter this track belongs to
332    pub emitter_index: usize,
333    /// Type of animation track
334    pub track_type: RibbonTrackType,
335    /// Raw interpolation range bytes (8 bytes per range: start u32 + end u32)
336    pub interpolation_ranges: Vec<u8>,
337    /// Raw timestamp bytes (4 bytes per timestamp)
338    pub timestamps: Vec<u8>,
339    /// Raw keyframe value bytes (size depends on track type)
340    pub values: Vec<u8>,
341    /// Original file offset for interpolation_ranges array
342    pub original_ranges_offset: u32,
343    /// Original file offset for timestamps array
344    pub original_timestamps_offset: u32,
345    /// Original file offset for values array
346    pub original_values_offset: u32,
347}
348
349/// Raw animation data for a single texture animation track
350///
351/// This preserves the exact bytes from the original file for texture animation keyframes,
352/// allowing roundtrip serialization without data loss. Texture animations have 5 different
353/// animation tracks (translation_u, translation_v, rotation, scale_u, scale_v).
354#[derive(Debug, Clone, Default)]
355pub struct TextureAnimationRaw {
356    /// Index of the texture animation this track belongs to
357    pub animation_index: usize,
358    /// Type of animation track
359    pub track_type: TextureTrackType,
360    /// Raw interpolation range bytes (8 bytes per range: start u32 + end u32)
361    pub interpolation_ranges: Vec<u8>,
362    /// Raw timestamp bytes (4 bytes per timestamp)
363    pub timestamps: Vec<u8>,
364    /// Raw keyframe value bytes (4 bytes per value - all tracks use f32)
365    pub values: Vec<u8>,
366    /// Original file offset for interpolation_ranges array
367    pub original_ranges_offset: u32,
368    /// Original file offset for timestamps array
369    pub original_timestamps_offset: u32,
370    /// Original file offset for values array
371    pub original_values_offset: u32,
372}
373
374/// Type of animation track within a color animation
375#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
376pub enum ColorTrackType {
377    /// RGB color animation (M2Color = 12 bytes)
378    #[default]
379    Color,
380    /// Alpha animation (u16 = 2 bytes)
381    Alpha,
382}
383
384impl ColorTrackType {
385    /// Returns the size in bytes of each value for this track type
386    pub fn value_size(&self) -> usize {
387        match self {
388            ColorTrackType::Color => 12, // M2Color (3 floats)
389            ColorTrackType::Alpha => 2,  // u16
390        }
391    }
392}
393
394/// Raw animation data for a single color animation track
395///
396/// This preserves the exact bytes from the original file for color animation keyframes,
397/// allowing roundtrip serialization without data loss. Color animations have 2 different
398/// animation tracks (color RGB, alpha).
399#[derive(Debug, Clone, Default)]
400pub struct ColorAnimationRaw {
401    /// Index of the color animation this track belongs to
402    pub animation_index: usize,
403    /// Type of animation track
404    pub track_type: ColorTrackType,
405    /// Raw interpolation range bytes (8 bytes per range: start u32 + end u32)
406    pub interpolation_ranges: Vec<u8>,
407    /// Raw timestamp bytes (4 bytes per timestamp)
408    pub timestamps: Vec<u8>,
409    /// Raw keyframe value bytes (12 bytes for color, 2 bytes for alpha)
410    pub values: Vec<u8>,
411    /// Original file offset for interpolation_ranges array
412    pub original_ranges_offset: u32,
413    /// Original file offset for timestamps array
414    pub original_timestamps_offset: u32,
415    /// Original file offset for values array
416    pub original_values_offset: u32,
417}
418
419/// Type of animation track within a transparency animation
420#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
421pub enum TransparencyTrackType {
422    /// Alpha animation (f32 = 4 bytes)
423    #[default]
424    Alpha,
425}
426
427impl TransparencyTrackType {
428    /// Returns the size in bytes of each value for this track type
429    pub fn value_size(&self) -> usize {
430        4 // f32
431    }
432}
433
434/// Raw animation data for a single transparency animation track
435///
436/// This preserves the exact bytes from the original file for transparency animation keyframes,
437/// allowing roundtrip serialization without data loss. Transparency animations have 1
438/// animation track (alpha).
439#[derive(Debug, Clone, Default)]
440pub struct TransparencyAnimationRaw {
441    /// Index of the transparency animation this track belongs to
442    pub animation_index: usize,
443    /// Type of animation track
444    pub track_type: TransparencyTrackType,
445    /// Raw interpolation range bytes (8 bytes per range: start u32 + end u32)
446    pub interpolation_ranges: Vec<u8>,
447    /// Raw timestamp bytes (4 bytes per timestamp)
448    pub timestamps: Vec<u8>,
449    /// Raw keyframe value bytes (4 bytes for f32 alpha)
450    pub values: Vec<u8>,
451    /// Original file offset for interpolation_ranges array
452    pub original_ranges_offset: u32,
453    /// Original file offset for timestamps array
454    pub original_timestamps_offset: u32,
455    /// Original file offset for values array
456    pub original_values_offset: u32,
457}
458
459/// Raw event track data for a single event
460///
461/// This preserves the exact bytes from the original file for event data,
462/// allowing roundtrip serialization without data loss. Events have two M2Arrays:
463/// ranges (per-animation timing info) and times (u32 timestamps when event triggers).
464#[derive(Debug, Clone, Default)]
465pub struct EventRaw {
466    /// Index of the event this track belongs to
467    pub event_index: usize,
468    /// Raw ranges bytes (8 bytes per range: start, end timestamps)
469    pub ranges: Vec<u8>,
470    /// Original file offset for ranges array
471    pub original_ranges_offset: u32,
472    /// Raw timestamp bytes (4 bytes per timestamp)
473    pub timestamps: Vec<u8>,
474    /// Original file offset for timestamps array
475    pub original_timestamps_offset: u32,
476}
477
478/// Type of animation track within an attachment
479#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
480pub enum AttachmentTrackType {
481    /// Scale animation (f32 = 4 bytes)
482    #[default]
483    Scale,
484}
485
486impl AttachmentTrackType {
487    /// Returns the size in bytes of each value for this track type
488    pub fn value_size(&self) -> usize {
489        4 // f32
490    }
491}
492
493/// Raw animation data for a single attachment track
494///
495/// This preserves the exact bytes from the original file for attachment animation keyframes,
496/// allowing roundtrip serialization without data loss. Attachments have 1 animation track (scale).
497#[derive(Debug, Clone, Default)]
498pub struct AttachmentAnimationRaw {
499    /// Index of the attachment this track belongs to
500    pub attachment_index: usize,
501    /// Type of animation track
502    pub track_type: AttachmentTrackType,
503    /// Raw interpolation range bytes (8 bytes per range: start u32 + end u32)
504    pub interpolation_ranges: Vec<u8>,
505    /// Raw timestamp bytes (4 bytes per timestamp)
506    pub timestamps: Vec<u8>,
507    /// Raw keyframe value bytes (4 bytes for f32 scale)
508    pub values: Vec<u8>,
509    /// Original file offset for interpolation_ranges array
510    pub original_ranges_offset: u32,
511    /// Original file offset for timestamps array
512    pub original_timestamps_offset: u32,
513    /// Original file offset for values array
514    pub original_values_offset: u32,
515}
516
517/// Type of animation track for camera structures
518#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
519pub enum CameraTrackType {
520    /// Camera position animation (C3Vector = 12 bytes)
521    #[default]
522    Position,
523    /// Target position animation (C3Vector = 12 bytes)
524    TargetPosition,
525    /// Roll animation (f32 = 4 bytes)
526    Roll,
527}
528
529impl CameraTrackType {
530    /// Returns the size in bytes of a single keyframe value for this track type
531    pub fn value_size(&self) -> usize {
532        match self {
533            CameraTrackType::Position | CameraTrackType::TargetPosition => 12, // C3Vector
534            CameraTrackType::Roll => 4,                                        // f32
535        }
536    }
537}
538
539/// Raw animation data for a single camera track
540///
541/// This preserves the exact bytes from the original file for camera animation keyframes,
542/// allowing roundtrip serialization without data loss. Cameras have 3 animation tracks
543/// (position, target_position, roll).
544#[derive(Debug, Clone, Default)]
545pub struct CameraAnimationRaw {
546    /// Index of the camera this track belongs to
547    pub camera_index: usize,
548    /// Type of animation track
549    pub track_type: CameraTrackType,
550    /// Raw interpolation range bytes (8 bytes per range: start u32 + end u32)
551    pub interpolation_ranges: Vec<u8>,
552    /// Raw timestamp bytes (4 bytes per timestamp)
553    pub timestamps: Vec<u8>,
554    /// Raw keyframe value bytes (size depends on track type)
555    pub values: Vec<u8>,
556    /// Original file offset for interpolation_ranges array
557    pub original_ranges_offset: u32,
558    /// Original file offset for timestamps array
559    pub original_timestamps_offset: u32,
560    /// Original file offset for values array
561    pub original_values_offset: u32,
562}
563
564/// Type of animation track for light structures
565#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
566pub enum LightTrackType {
567    /// Ambient color animation (M2Color = 12 bytes: RGB as f32)
568    #[default]
569    AmbientColor,
570    /// Diffuse color animation (M2Color = 12 bytes: RGB as f32)
571    DiffuseColor,
572    /// Attenuation start animation (f32 = 4 bytes)
573    AttenuationStart,
574    /// Attenuation end animation (f32 = 4 bytes)
575    AttenuationEnd,
576    /// Visibility animation (f32 = 4 bytes)
577    Visibility,
578}
579
580impl LightTrackType {
581    /// Returns the size in bytes of a single keyframe value for this track type
582    pub fn value_size(&self) -> usize {
583        match self {
584            LightTrackType::AmbientColor | LightTrackType::DiffuseColor => 12, // M2Color (3 f32s)
585            LightTrackType::AttenuationStart
586            | LightTrackType::AttenuationEnd
587            | LightTrackType::Visibility => 4, // f32
588        }
589    }
590}
591
592/// Raw animation data for a single light track
593///
594/// This preserves the exact bytes from the original file for light animation keyframes,
595/// allowing roundtrip serialization without data loss. Lights have 5 animation tracks
596/// (ambient_color, diffuse_color, attenuation_start, attenuation_end, visibility).
597#[derive(Debug, Clone, Default)]
598pub struct LightAnimationRaw {
599    /// Index of the light this track belongs to
600    pub light_index: usize,
601    /// Type of animation track
602    pub track_type: LightTrackType,
603    /// Raw interpolation range bytes (8 bytes per range: start u32 + end u32)
604    pub interpolation_ranges: Vec<u8>,
605    /// Raw timestamp bytes (4 bytes per timestamp)
606    pub timestamps: Vec<u8>,
607    /// Raw keyframe value bytes (size depends on track type)
608    pub values: Vec<u8>,
609    /// Original file offset for interpolation_ranges array
610    pub original_ranges_offset: u32,
611    /// Original file offset for timestamps array
612    pub original_timestamps_offset: u32,
613    /// Original file offset for values array
614    pub original_values_offset: u32,
615}
616
617/// Raw data for sections that are not fully parsed
618#[derive(Debug, Clone, Default)]
619pub struct M2RawData {
620    /// Raw animation keyframe data for all bone tracks
621    /// Used to preserve animation data during roundtrip serialization
622    pub bone_animation_data: Vec<BoneAnimationRaw>,
623    /// Raw embedded skin data for pre-WotLK models (version <= 263)
624    /// Empty for WotLK+ models which use external .skin files
625    pub embedded_skins: Vec<EmbeddedSkinRaw>,
626    /// Raw animation keyframe data for all particle emitter tracks
627    /// Used to preserve particle animation data during roundtrip serialization
628    pub particle_animation_data: Vec<ParticleAnimationRaw>,
629    /// Raw animation keyframe data for all ribbon emitter tracks
630    /// Used to preserve ribbon animation data during roundtrip serialization
631    pub ribbon_animation_data: Vec<RibbonAnimationRaw>,
632    /// Raw animation keyframe data for all texture animation tracks
633    /// Used to preserve texture animation data during roundtrip serialization
634    pub texture_animation_data: Vec<TextureAnimationRaw>,
635    /// Raw animation keyframe data for all color animation tracks
636    /// Used to preserve color animation data during roundtrip serialization
637    pub color_animation_data: Vec<ColorAnimationRaw>,
638    /// Raw animation keyframe data for all transparency animation tracks
639    /// Used to preserve transparency animation data during roundtrip serialization
640    pub transparency_animation_data: Vec<TransparencyAnimationRaw>,
641    /// Raw event track data for all events
642    /// Used to preserve event timestamp data during roundtrip serialization
643    pub event_data: Vec<EventRaw>,
644    /// Raw animation keyframe data for all attachment tracks
645    /// Used to preserve attachment animation data during roundtrip serialization
646    pub attachment_animation_data: Vec<AttachmentAnimationRaw>,
647    /// Raw animation keyframe data for all camera tracks
648    /// Used to preserve camera animation data during roundtrip serialization
649    pub camera_animation_data: Vec<CameraAnimationRaw>,
650    /// Raw animation keyframe data for all light tracks
651    /// Used to preserve light animation data during roundtrip serialization
652    pub light_animation_data: Vec<LightAnimationRaw>,
653    /// Transparency data (the actual transparency animations, not lookups)
654    pub transparency: Vec<u8>,
655    /// Texture animations (legacy raw storage, being replaced by texture_animation_data)
656    pub texture_animations: Vec<u8>,
657    /// Color animations
658    pub color_animations: Vec<u8>,
659    /// Color replacements
660    pub color_replacements: Vec<u8>,
661    /// Render flags
662    pub render_flags: Vec<u8>,
663    /// Bone lookup table
664    pub bone_lookup_table: Vec<u16>,
665    /// Texture lookup table
666    pub texture_lookup_table: Vec<u16>,
667    /// Texture units
668    pub texture_units: Vec<u16>,
669    /// Transparency lookup table
670    pub transparency_lookup_table: Vec<u16>,
671    /// Texture animation lookup
672    pub texture_animation_lookup: Vec<u16>,
673    /// Bounding triangles
674    pub bounding_triangles: Vec<u8>,
675    /// Bounding vertices
676    pub bounding_vertices: Vec<u8>,
677    /// Bounding normals
678    pub bounding_normals: Vec<u8>,
679    /// Attachments
680    pub attachments: Vec<u8>,
681    /// Attachment lookup table
682    pub attachment_lookup_table: Vec<u16>,
683    /// Events
684    pub events: Vec<u8>,
685    /// Lights
686    pub lights: Vec<u8>,
687    /// Cameras
688    pub cameras: Vec<u8>,
689    /// Camera lookup table
690    pub camera_lookup_table: Vec<u16>,
691    /// Ribbon emitters (raw, for versions where we don't parse)
692    pub ribbon_emitters: Vec<u8>,
693    /// Particle emitters (raw, for versions where we don't parse)
694    pub particle_emitters: Vec<u8>,
695    /// Views data (embedded skins for pre-WotLK, raw bytes)
696    pub views_data: Vec<u8>,
697    /// Texture flipbooks (BC and earlier)
698    pub texture_flipbooks: Option<Vec<u8>>,
699    /// Blend map overrides (BC+ with specific flag)
700    pub blend_map_overrides: Option<Vec<u8>>,
701    /// Texture combiner combos (added in Cataclysm)
702    pub texture_combiner_combos: Option<Vec<u8>>,
703    /// Texture transforms (added in Legion)
704    pub texture_transforms: Option<Vec<u8>>,
705}
706
707/// Parse an M2 model, automatically detecting format
708pub fn parse_m2<R: Read + Seek>(reader: &mut R) -> Result<M2Format> {
709    let mut magic = [0u8; 4];
710    reader.read_exact(&mut magic)?;
711    reader.seek(SeekFrom::Start(0))?;
712
713    match &magic {
714        magic if magic == &M2_MAGIC_LEGACY => Ok(M2Format::Legacy(M2Model::parse_legacy(reader)?)),
715        magic if magic == &M2_MAGIC_CHUNKED => {
716            Ok(M2Format::Chunked(M2Model::parse_chunked(reader)?))
717        }
718        _ => Err(M2Error::InvalidMagicBytes(magic)),
719    }
720}
721
722/// Collects raw animation keyframe data for a single M2Track
723///
724/// Returns None if the track has no data, otherwise returns BoneAnimationRaw
725/// with timestamps, values, and optionally ranges (for pre-WotLK).
726fn collect_track_data<R: Read + Seek, T>(
727    reader: &mut R,
728    track: &M2Track<T>,
729    version: u32,
730    value_element_size: usize,
731    bone_index: usize,
732    track_type: TrackType,
733) -> Result<Option<BoneAnimationRaw>> {
734    // Skip empty tracks
735    if track.timestamps.is_empty() && track.values.is_empty() {
736        return Ok(None);
737    }
738
739    // Read timestamps (4 bytes per timestamp)
740    let timestamps = if !track.timestamps.is_empty() {
741        read_raw_bytes(reader, &track.timestamps.convert(), 4)?
742    } else {
743        Vec::new()
744    };
745
746    // Read values
747    let values = if !track.values.is_empty() {
748        read_raw_bytes(reader, &track.values.convert(), value_element_size)?
749    } else {
750        Vec::new()
751    };
752
753    // Read ranges for pre-WotLK (8 bytes per range: start u32 + end u32)
754    let (ranges, original_ranges_offset) = if version < 264 {
755        if let Some(ref ranges_array) = track.ranges {
756            if !ranges_array.is_empty() {
757                (
758                    Some(read_raw_bytes(reader, &ranges_array.convert(), 8)?),
759                    Some(ranges_array.offset),
760                )
761            } else {
762                (None, None)
763            }
764        } else {
765            (None, None)
766        }
767    } else {
768        (None, None)
769    };
770
771    Ok(Some(BoneAnimationRaw {
772        bone_index,
773        track_type,
774        timestamps,
775        values,
776        ranges,
777        original_timestamps_offset: track.timestamps.offset,
778        original_values_offset: track.values.offset,
779        original_ranges_offset,
780    }))
781}
782
783/// Collects raw animation keyframe data for all bones in the model
784///
785/// This reads the actual keyframe bytes (timestamps, values, ranges) from the file,
786/// storing them for later serialization with offset relocation.
787fn collect_bone_animation_data<R: Read + Seek>(
788    reader: &mut R,
789    bones: &[M2Bone],
790    version: u32,
791) -> Result<Vec<BoneAnimationRaw>> {
792    let mut animation_data = Vec::new();
793
794    for (bone_idx, bone) in bones.iter().enumerate() {
795        // Collect translation track (C3Vector = 12 bytes per value)
796        if let Some(data) = collect_track_data(
797            reader,
798            &bone.translation,
799            version,
800            12,
801            bone_idx,
802            TrackType::Translation,
803        )? {
804            animation_data.push(data);
805        }
806
807        // Collect rotation track (M2CompQuat = 8 bytes per value)
808        if let Some(data) = collect_track_data(
809            reader,
810            &bone.rotation,
811            version,
812            8,
813            bone_idx,
814            TrackType::Rotation,
815        )? {
816            animation_data.push(data);
817        }
818
819        // Collect scale track (C3Vector = 12 bytes per value)
820        if let Some(data) =
821            collect_track_data(reader, &bone.scale, version, 12, bone_idx, TrackType::Scale)?
822        {
823            animation_data.push(data);
824        }
825    }
826
827    Ok(animation_data)
828}
829
830/// Updates M2Track offsets in a bone using the relocation map
831///
832/// If a track's original offset is not in the map but the track has data,
833/// the track is zeroed out to avoid invalid references.
834fn relocate_bone_track_offsets(bone: &mut M2Bone, offset_map: &HashMap<u32, u32>) {
835    // Helper to relocate or zero a track
836    fn relocate_or_zero_track<T: Default>(track: &mut M2Track<T>, offset_map: &HashMap<u32, u32>) {
837        // Check if timestamps offset needs relocation
838        if !track.timestamps.is_empty() {
839            if let Some(&new_offset) = offset_map.get(&track.timestamps.offset) {
840                track.timestamps.offset = new_offset;
841            } else {
842                // Data not collected - zero out the track
843                *track = M2Track::default();
844                return;
845            }
846        }
847
848        // Check if values offset needs relocation
849        if !track.values.is_empty() {
850            if let Some(&new_offset) = offset_map.get(&track.values.offset) {
851                track.values.offset = new_offset;
852            } else {
853                // Data not collected - zero out the track
854                *track = M2Track::default();
855                return;
856            }
857        }
858
859        // Check if ranges offset needs relocation (pre-WotLK)
860        if let Some(ref mut ranges) = track.ranges
861            && !ranges.is_empty()
862        {
863            if let Some(&new_offset) = offset_map.get(&ranges.offset) {
864                ranges.offset = new_offset;
865            } else {
866                // Ranges data not collected - set to empty
867                *ranges = M2Array::default();
868            }
869        }
870    }
871
872    relocate_or_zero_track(&mut bone.translation, offset_map);
873    relocate_or_zero_track(&mut bone.rotation, offset_map);
874    relocate_or_zero_track(&mut bone.scale, offset_map);
875}
876
877/// Updates M2AnimationBlock offsets in a particle emitter using the relocation map
878///
879/// If a track's original offset is not in the map but the track has data,
880/// the track is zeroed out to avoid invalid references.
881fn relocate_particle_animation_offsets(
882    emitter: &mut M2ParticleEmitter,
883    offset_map: &HashMap<u32, u32>,
884) {
885    // Helper to relocate or zero an animation block
886    fn relocate_or_zero_animation_block<T: M2Parse + Default + Clone>(
887        block: &mut M2AnimationBlock<T>,
888        offset_map: &HashMap<u32, u32>,
889    ) {
890        let track = &mut block.track;
891
892        // Check if interpolation_ranges offset needs relocation
893        if !track.interpolation_ranges.is_empty() {
894            if let Some(&new_offset) = offset_map.get(&track.interpolation_ranges.offset) {
895                track.interpolation_ranges.offset = new_offset;
896            } else {
897                // Data not collected - zero out the track
898                *block = M2AnimationBlock::default();
899                return;
900            }
901        }
902
903        // Check if timestamps offset needs relocation
904        if !track.timestamps.is_empty() {
905            if let Some(&new_offset) = offset_map.get(&track.timestamps.offset) {
906                track.timestamps.offset = new_offset;
907            } else {
908                // Data not collected - zero out the track
909                *block = M2AnimationBlock::default();
910                return;
911            }
912        }
913
914        // Check if values offset needs relocation
915        if !track.values.array.is_empty() {
916            if let Some(&new_offset) = offset_map.get(&track.values.array.offset) {
917                track.values.array.offset = new_offset;
918            } else {
919                // Data not collected - zero out the track
920                *block = M2AnimationBlock::default();
921            }
922        }
923    }
924
925    relocate_or_zero_animation_block(&mut emitter.emission_speed_animation, offset_map);
926    relocate_or_zero_animation_block(&mut emitter.emission_rate_animation, offset_map);
927    relocate_or_zero_animation_block(&mut emitter.emission_area_animation, offset_map);
928    relocate_or_zero_animation_block(&mut emitter.xy_scale_animation, offset_map);
929    relocate_or_zero_animation_block(&mut emitter.z_scale_animation, offset_map);
930    relocate_or_zero_animation_block(&mut emitter.color_animation, offset_map);
931    relocate_or_zero_animation_block(&mut emitter.transparency_animation, offset_map);
932    relocate_or_zero_animation_block(&mut emitter.size_animation, offset_map);
933    relocate_or_zero_animation_block(&mut emitter.intensity_animation, offset_map);
934    relocate_or_zero_animation_block(&mut emitter.z_source_animation, offset_map);
935}
936
937/// Updates M2AnimationBlock offsets in a ribbon emitter using the relocation map
938///
939/// If a track's original offset is not in the map but the track has data,
940/// the track is zeroed out to avoid invalid references.
941fn relocate_ribbon_animation_offsets(
942    emitter: &mut M2RibbonEmitter,
943    offset_map: &HashMap<u32, u32>,
944) {
945    // Helper to relocate or zero an animation block
946    fn relocate_or_zero_animation_block<T: M2Parse + Default + Clone>(
947        block: &mut M2AnimationBlock<T>,
948        offset_map: &HashMap<u32, u32>,
949    ) {
950        let track = &mut block.track;
951
952        // Check if interpolation_ranges offset needs relocation
953        if !track.interpolation_ranges.is_empty() {
954            if let Some(&new_offset) = offset_map.get(&track.interpolation_ranges.offset) {
955                track.interpolation_ranges.offset = new_offset;
956            } else {
957                // Data not collected - zero out the track
958                *block = M2AnimationBlock::default();
959                return;
960            }
961        }
962
963        // Check if timestamps offset needs relocation
964        if !track.timestamps.is_empty() {
965            if let Some(&new_offset) = offset_map.get(&track.timestamps.offset) {
966                track.timestamps.offset = new_offset;
967            } else {
968                // Data not collected - zero out the track
969                *block = M2AnimationBlock::default();
970                return;
971            }
972        }
973
974        // Check if values offset needs relocation
975        if !track.values.array.is_empty() {
976            if let Some(&new_offset) = offset_map.get(&track.values.array.offset) {
977                track.values.array.offset = new_offset;
978            } else {
979                // Data not collected - zero out the track
980                *block = M2AnimationBlock::default();
981            }
982        }
983    }
984
985    relocate_or_zero_animation_block(&mut emitter.color_animation, offset_map);
986    relocate_or_zero_animation_block(&mut emitter.alpha_animation, offset_map);
987    relocate_or_zero_animation_block(&mut emitter.height_above_animation, offset_map);
988    relocate_or_zero_animation_block(&mut emitter.height_below_animation, offset_map);
989}
990
991/// Relocates texture animation offsets in a texture animation to new positions
992fn relocate_texture_animation_offsets(
993    animation: &mut M2TextureAnimation,
994    offset_map: &HashMap<u32, u32>,
995) {
996    // Helper to relocate or zero an animation block
997    fn relocate_or_zero_animation_block<T: M2Parse + Default + Clone>(
998        block: &mut M2AnimationBlock<T>,
999        offset_map: &HashMap<u32, u32>,
1000    ) {
1001        let track = &mut block.track;
1002
1003        // Check if interpolation_ranges offset needs relocation
1004        if !track.interpolation_ranges.is_empty() {
1005            if let Some(&new_offset) = offset_map.get(&track.interpolation_ranges.offset) {
1006                track.interpolation_ranges.offset = new_offset;
1007            } else {
1008                // Data not collected - zero out the track
1009                *block = M2AnimationBlock::default();
1010                return;
1011            }
1012        }
1013
1014        // Check if timestamps offset needs relocation
1015        if !track.timestamps.is_empty() {
1016            if let Some(&new_offset) = offset_map.get(&track.timestamps.offset) {
1017                track.timestamps.offset = new_offset;
1018            } else {
1019                // Data not collected - zero out the track
1020                *block = M2AnimationBlock::default();
1021                return;
1022            }
1023        }
1024
1025        // Check if values offset needs relocation
1026        if !track.values.array.is_empty() {
1027            if let Some(&new_offset) = offset_map.get(&track.values.array.offset) {
1028                track.values.array.offset = new_offset;
1029            } else {
1030                // Data not collected - zero out the track
1031                *block = M2AnimationBlock::default();
1032            }
1033        }
1034    }
1035
1036    relocate_or_zero_animation_block(&mut animation.translation_u, offset_map);
1037    relocate_or_zero_animation_block(&mut animation.translation_v, offset_map);
1038    relocate_or_zero_animation_block(&mut animation.rotation, offset_map);
1039    relocate_or_zero_animation_block(&mut animation.scale_u, offset_map);
1040    relocate_or_zero_animation_block(&mut animation.scale_v, offset_map);
1041}
1042
1043/// Relocates color animation offsets in a color animation to new positions
1044fn relocate_color_animation_offsets(
1045    animation: &mut M2ColorAnimation,
1046    offset_map: &HashMap<u32, u32>,
1047) {
1048    // Helper to relocate or zero an animation block
1049    fn relocate_or_zero_animation_block<T: M2Parse + Default + Clone>(
1050        block: &mut M2AnimationBlock<T>,
1051        offset_map: &HashMap<u32, u32>,
1052    ) {
1053        let track = &mut block.track;
1054
1055        // Check if interpolation_ranges offset needs relocation
1056        if !track.interpolation_ranges.is_empty() {
1057            if let Some(&new_offset) = offset_map.get(&track.interpolation_ranges.offset) {
1058                track.interpolation_ranges.offset = new_offset;
1059            } else {
1060                // Data not collected - zero out the track
1061                *block = M2AnimationBlock::default();
1062                return;
1063            }
1064        }
1065
1066        // Check if timestamps offset needs relocation
1067        if !track.timestamps.is_empty() {
1068            if let Some(&new_offset) = offset_map.get(&track.timestamps.offset) {
1069                track.timestamps.offset = new_offset;
1070            } else {
1071                // Data not collected - zero out the track
1072                *block = M2AnimationBlock::default();
1073                return;
1074            }
1075        }
1076
1077        // Check if values offset needs relocation
1078        if !track.values.array.is_empty() {
1079            if let Some(&new_offset) = offset_map.get(&track.values.array.offset) {
1080                track.values.array.offset = new_offset;
1081            } else {
1082                // Data not collected - zero out the track
1083                *block = M2AnimationBlock::default();
1084            }
1085        }
1086    }
1087
1088    relocate_or_zero_animation_block(&mut animation.color, offset_map);
1089    relocate_or_zero_animation_block(&mut animation.alpha, offset_map);
1090}
1091
1092/// Relocates transparency animation offsets in a transparency animation to new positions
1093fn relocate_transparency_animation_offsets(
1094    animation: &mut M2TransparencyAnimation,
1095    offset_map: &HashMap<u32, u32>,
1096) {
1097    // Helper to relocate or zero an animation block
1098    fn relocate_or_zero_animation_block<T: M2Parse + Default + Clone>(
1099        block: &mut M2AnimationBlock<T>,
1100        offset_map: &HashMap<u32, u32>,
1101    ) {
1102        let track = &mut block.track;
1103
1104        // Check if interpolation_ranges offset needs relocation
1105        if !track.interpolation_ranges.is_empty() {
1106            if let Some(&new_offset) = offset_map.get(&track.interpolation_ranges.offset) {
1107                track.interpolation_ranges.offset = new_offset;
1108            } else {
1109                // Data not collected - zero out the track
1110                *block = M2AnimationBlock::default();
1111                return;
1112            }
1113        }
1114
1115        // Check if timestamps offset needs relocation
1116        if !track.timestamps.is_empty() {
1117            if let Some(&new_offset) = offset_map.get(&track.timestamps.offset) {
1118                track.timestamps.offset = new_offset;
1119            } else {
1120                // Data not collected - zero out the track
1121                *block = M2AnimationBlock::default();
1122                return;
1123            }
1124        }
1125
1126        // Check if values offset needs relocation
1127        if !track.values.array.is_empty() {
1128            if let Some(&new_offset) = offset_map.get(&track.values.array.offset) {
1129                track.values.array.offset = new_offset;
1130            } else {
1131                // Data not collected - zero out the track
1132                *block = M2AnimationBlock::default();
1133            }
1134        }
1135    }
1136
1137    relocate_or_zero_animation_block(&mut animation.alpha, offset_map);
1138}
1139
1140/// Relocates event track offsets to new positions
1141///
1142/// Events have two M2Arrays: ranges and times. Both need relocation.
1143fn relocate_event_offset(event: &mut M2Event, offset_map: &HashMap<u32, u32>) {
1144    // Relocate ranges if present
1145    if event.ranges.count > 0 {
1146        if let Some(&new_offset) = offset_map.get(&event.ranges.offset) {
1147            event.ranges.offset = new_offset;
1148        } else {
1149            event.ranges = M2Array::default();
1150        }
1151    }
1152
1153    // Relocate times if present
1154    if event.times.count > 0 {
1155        if let Some(&new_offset) = offset_map.get(&event.times.offset) {
1156            event.times.offset = new_offset;
1157        } else {
1158            event.times = M2Array::default();
1159        }
1160    }
1161}
1162
1163/// Relocates attachment animation offsets to new positions
1164///
1165/// Attachments have a single scale animation track (`M2AnimationBlock<f32>`).
1166fn relocate_attachment_animation_offsets(
1167    attachment: &mut M2Attachment,
1168    offset_map: &HashMap<u32, u32>,
1169) {
1170    // Helper to relocate or zero an animation block
1171    fn relocate_or_zero_animation_block<T: M2Parse + Default + Clone>(
1172        block: &mut M2AnimationBlock<T>,
1173        offset_map: &HashMap<u32, u32>,
1174    ) {
1175        let track = &mut block.track;
1176
1177        // Check if interpolation_ranges offset needs relocation
1178        if !track.interpolation_ranges.is_empty() {
1179            if let Some(&new_offset) = offset_map.get(&track.interpolation_ranges.offset) {
1180                track.interpolation_ranges.offset = new_offset;
1181            } else {
1182                // Data not collected - zero out the track
1183                *block = M2AnimationBlock::default();
1184                return;
1185            }
1186        }
1187
1188        // Check if timestamps offset needs relocation
1189        if !track.timestamps.is_empty() {
1190            if let Some(&new_offset) = offset_map.get(&track.timestamps.offset) {
1191                track.timestamps.offset = new_offset;
1192            } else {
1193                // Data not collected - zero out the track
1194                *block = M2AnimationBlock::default();
1195                return;
1196            }
1197        }
1198
1199        // Check if values offset needs relocation
1200        if !track.values.array.is_empty() {
1201            if let Some(&new_offset) = offset_map.get(&track.values.array.offset) {
1202                track.values.array.offset = new_offset;
1203            } else {
1204                // Data not collected - zero out the track
1205                *block = M2AnimationBlock::default();
1206            }
1207        }
1208    }
1209
1210    relocate_or_zero_animation_block(&mut attachment.scale_animation, offset_map);
1211}
1212
1213/// Relocates camera animation offsets to new positions
1214///
1215/// Cameras have three animation tracks: position, target_position, and roll.
1216fn relocate_camera_animation_offsets(camera: &mut M2Camera, offset_map: &HashMap<u32, u32>) {
1217    // Helper to relocate or zero an animation block
1218    fn relocate_or_zero_animation_block<T: M2Parse + Default + Clone>(
1219        block: &mut M2AnimationBlock<T>,
1220        offset_map: &HashMap<u32, u32>,
1221    ) {
1222        let track = &mut block.track;
1223
1224        // Check if interpolation_ranges offset needs relocation
1225        if !track.interpolation_ranges.is_empty() {
1226            if let Some(&new_offset) = offset_map.get(&track.interpolation_ranges.offset) {
1227                track.interpolation_ranges.offset = new_offset;
1228            } else {
1229                // Data not collected - zero out the track
1230                *block = M2AnimationBlock::default();
1231                return;
1232            }
1233        }
1234
1235        // Check if timestamps offset needs relocation
1236        if !track.timestamps.is_empty() {
1237            if let Some(&new_offset) = offset_map.get(&track.timestamps.offset) {
1238                track.timestamps.offset = new_offset;
1239            } else {
1240                // Data not collected - zero out the track
1241                *block = M2AnimationBlock::default();
1242                return;
1243            }
1244        }
1245
1246        // Check if values offset needs relocation
1247        if !track.values.array.is_empty() {
1248            if let Some(&new_offset) = offset_map.get(&track.values.array.offset) {
1249                track.values.array.offset = new_offset;
1250            } else {
1251                // Data not collected - zero out the track
1252                *block = M2AnimationBlock::default();
1253            }
1254        }
1255    }
1256
1257    relocate_or_zero_animation_block(&mut camera.position_animation, offset_map);
1258    relocate_or_zero_animation_block(&mut camera.target_position_animation, offset_map);
1259    relocate_or_zero_animation_block(&mut camera.roll_animation, offset_map);
1260}
1261
1262/// Relocates light animation offsets to new positions
1263///
1264/// Lights have five animation tracks: ambient_color, diffuse_color, attenuation_start,
1265/// attenuation_end, and visibility.
1266fn relocate_light_animation_offsets(light: &mut M2Light, offset_map: &HashMap<u32, u32>) {
1267    // Helper to relocate or zero an animation block
1268    fn relocate_or_zero_animation_block<T: M2Parse + Default + Clone>(
1269        block: &mut M2AnimationBlock<T>,
1270        offset_map: &HashMap<u32, u32>,
1271    ) {
1272        let track = &mut block.track;
1273
1274        // Check if interpolation_ranges offset needs relocation
1275        if !track.interpolation_ranges.is_empty() {
1276            if let Some(&new_offset) = offset_map.get(&track.interpolation_ranges.offset) {
1277                track.interpolation_ranges.offset = new_offset;
1278            } else {
1279                // Data not collected - zero out the track
1280                *block = M2AnimationBlock::default();
1281                return;
1282            }
1283        }
1284
1285        // Check if timestamps offset needs relocation
1286        if !track.timestamps.is_empty() {
1287            if let Some(&new_offset) = offset_map.get(&track.timestamps.offset) {
1288                track.timestamps.offset = new_offset;
1289            } else {
1290                // Data not collected - zero out the track
1291                *block = M2AnimationBlock::default();
1292                return;
1293            }
1294        }
1295
1296        // Check if values offset needs relocation
1297        if !track.values.array.is_empty() {
1298            if let Some(&new_offset) = offset_map.get(&track.values.array.offset) {
1299                track.values.array.offset = new_offset;
1300            } else {
1301                // Data not collected - zero out the track
1302                *block = M2AnimationBlock::default();
1303            }
1304        }
1305    }
1306
1307    relocate_or_zero_animation_block(&mut light.ambient_color_animation, offset_map);
1308    relocate_or_zero_animation_block(&mut light.diffuse_color_animation, offset_map);
1309    relocate_or_zero_animation_block(&mut light.attenuation_start_animation, offset_map);
1310    relocate_or_zero_animation_block(&mut light.attenuation_end_animation, offset_map);
1311    relocate_or_zero_animation_block(&mut light.visibility_animation, offset_map);
1312}
1313
1314/// Collects raw animation keyframe data for a single M2AnimationBlock
1315///
1316/// Returns None if the track has no data, otherwise returns ParticleAnimationRaw
1317/// with interpolation_ranges, timestamps, and values.
1318fn collect_particle_track_data<R: Read + Seek, T: M2Parse>(
1319    reader: &mut R,
1320    block: &M2AnimationBlock<T>,
1321    emitter_index: usize,
1322    track_type: ParticleTrackType,
1323) -> Result<Option<ParticleAnimationRaw>> {
1324    let track = &block.track;
1325
1326    // Skip empty tracks
1327    if track.timestamps.is_empty() && track.values.array.is_empty() {
1328        return Ok(None);
1329    }
1330
1331    // Read interpolation ranges (8 bytes per range: start u32 + end u32)
1332    let interpolation_ranges = if !track.interpolation_ranges.is_empty() {
1333        read_raw_bytes(reader, &track.interpolation_ranges.convert(), 8)?
1334    } else {
1335        Vec::new()
1336    };
1337
1338    // Read timestamps (4 bytes per timestamp)
1339    let timestamps = if !track.timestamps.is_empty() {
1340        read_raw_bytes(reader, &track.timestamps.convert(), 4)?
1341    } else {
1342        Vec::new()
1343    };
1344
1345    // Read values (size depends on track type)
1346    let values = if !track.values.array.is_empty() {
1347        read_raw_bytes(
1348            reader,
1349            &track.values.array.convert(),
1350            track_type.value_size(),
1351        )?
1352    } else {
1353        Vec::new()
1354    };
1355
1356    Ok(Some(ParticleAnimationRaw {
1357        emitter_index,
1358        track_type,
1359        interpolation_ranges,
1360        timestamps,
1361        values,
1362        original_ranges_offset: track.interpolation_ranges.offset,
1363        original_timestamps_offset: track.timestamps.offset,
1364        original_values_offset: track.values.array.offset,
1365    }))
1366}
1367
1368/// Collects raw animation keyframe data for all particle emitters in the model
1369///
1370/// This reads the actual keyframe bytes (interpolation_ranges, timestamps, values) from the file,
1371/// storing them for later serialization with offset relocation.
1372fn collect_particle_animation_data<R: Read + Seek>(
1373    reader: &mut R,
1374    emitters: &[M2ParticleEmitter],
1375) -> Result<Vec<ParticleAnimationRaw>> {
1376    let mut animation_data = Vec::new();
1377
1378    for (emitter_idx, emitter) in emitters.iter().enumerate() {
1379        // Collect emission speed track (f32 = 4 bytes)
1380        if let Some(data) = collect_particle_track_data(
1381            reader,
1382            &emitter.emission_speed_animation,
1383            emitter_idx,
1384            ParticleTrackType::EmissionSpeed,
1385        )? {
1386            animation_data.push(data);
1387        }
1388
1389        // Collect emission rate track (f32 = 4 bytes)
1390        if let Some(data) = collect_particle_track_data(
1391            reader,
1392            &emitter.emission_rate_animation,
1393            emitter_idx,
1394            ParticleTrackType::EmissionRate,
1395        )? {
1396            animation_data.push(data);
1397        }
1398
1399        // Collect emission area track (f32 = 4 bytes)
1400        if let Some(data) = collect_particle_track_data(
1401            reader,
1402            &emitter.emission_area_animation,
1403            emitter_idx,
1404            ParticleTrackType::EmissionArea,
1405        )? {
1406            animation_data.push(data);
1407        }
1408
1409        // Collect XY scale track (C2Vector = 8 bytes)
1410        if let Some(data) = collect_particle_track_data(
1411            reader,
1412            &emitter.xy_scale_animation,
1413            emitter_idx,
1414            ParticleTrackType::XYScale,
1415        )? {
1416            animation_data.push(data);
1417        }
1418
1419        // Collect Z scale track (f32 = 4 bytes)
1420        if let Some(data) = collect_particle_track_data(
1421            reader,
1422            &emitter.z_scale_animation,
1423            emitter_idx,
1424            ParticleTrackType::ZScale,
1425        )? {
1426            animation_data.push(data);
1427        }
1428
1429        // Collect color track (M2Color = 12 bytes)
1430        if let Some(data) = collect_particle_track_data(
1431            reader,
1432            &emitter.color_animation,
1433            emitter_idx,
1434            ParticleTrackType::Color,
1435        )? {
1436            animation_data.push(data);
1437        }
1438
1439        // Collect transparency track (f32 = 4 bytes)
1440        if let Some(data) = collect_particle_track_data(
1441            reader,
1442            &emitter.transparency_animation,
1443            emitter_idx,
1444            ParticleTrackType::Transparency,
1445        )? {
1446            animation_data.push(data);
1447        }
1448
1449        // Collect size track (f32 = 4 bytes)
1450        if let Some(data) = collect_particle_track_data(
1451            reader,
1452            &emitter.size_animation,
1453            emitter_idx,
1454            ParticleTrackType::Size,
1455        )? {
1456            animation_data.push(data);
1457        }
1458
1459        // Collect intensity track (f32 = 4 bytes)
1460        if let Some(data) = collect_particle_track_data(
1461            reader,
1462            &emitter.intensity_animation,
1463            emitter_idx,
1464            ParticleTrackType::Intensity,
1465        )? {
1466            animation_data.push(data);
1467        }
1468
1469        // Collect Z source track (f32 = 4 bytes)
1470        if let Some(data) = collect_particle_track_data(
1471            reader,
1472            &emitter.z_source_animation,
1473            emitter_idx,
1474            ParticleTrackType::ZSource,
1475        )? {
1476            animation_data.push(data);
1477        }
1478    }
1479
1480    Ok(animation_data)
1481}
1482
1483/// Collects raw animation keyframe data for a single ribbon emitter M2AnimationBlock
1484///
1485/// Returns None if the track has no data, otherwise returns RibbonAnimationRaw
1486/// with interpolation_ranges, timestamps, and values.
1487fn collect_ribbon_track_data<R: Read + Seek, T: M2Parse>(
1488    reader: &mut R,
1489    block: &M2AnimationBlock<T>,
1490    emitter_index: usize,
1491    track_type: RibbonTrackType,
1492) -> Result<Option<RibbonAnimationRaw>> {
1493    let track = &block.track;
1494
1495    // Skip empty tracks
1496    if track.timestamps.is_empty() && track.values.array.is_empty() {
1497        return Ok(None);
1498    }
1499
1500    // Read interpolation ranges (8 bytes per range: start u32 + end u32)
1501    let interpolation_ranges = if !track.interpolation_ranges.is_empty() {
1502        read_raw_bytes(reader, &track.interpolation_ranges.convert(), 8)?
1503    } else {
1504        Vec::new()
1505    };
1506
1507    // Read timestamps (4 bytes per timestamp)
1508    let timestamps = if !track.timestamps.is_empty() {
1509        read_raw_bytes(reader, &track.timestamps.convert(), 4)?
1510    } else {
1511        Vec::new()
1512    };
1513
1514    // Read values (size depends on track type)
1515    let values = if !track.values.array.is_empty() {
1516        read_raw_bytes(
1517            reader,
1518            &track.values.array.convert(),
1519            track_type.value_size(),
1520        )?
1521    } else {
1522        Vec::new()
1523    };
1524
1525    Ok(Some(RibbonAnimationRaw {
1526        emitter_index,
1527        track_type,
1528        interpolation_ranges,
1529        timestamps,
1530        values,
1531        original_ranges_offset: track.interpolation_ranges.offset,
1532        original_timestamps_offset: track.timestamps.offset,
1533        original_values_offset: track.values.array.offset,
1534    }))
1535}
1536
1537/// Collects raw animation keyframe data for all ribbon emitters in the model
1538///
1539/// This reads the actual keyframe bytes (interpolation_ranges, timestamps, values) from the file,
1540/// storing them for later serialization with offset relocation.
1541fn collect_ribbon_animation_data<R: Read + Seek>(
1542    reader: &mut R,
1543    emitters: &[M2RibbonEmitter],
1544) -> Result<Vec<RibbonAnimationRaw>> {
1545    let mut animation_data = Vec::new();
1546
1547    for (emitter_idx, emitter) in emitters.iter().enumerate() {
1548        // Collect color track (M2Color = 12 bytes)
1549        if let Some(data) = collect_ribbon_track_data(
1550            reader,
1551            &emitter.color_animation,
1552            emitter_idx,
1553            RibbonTrackType::Color,
1554        )? {
1555            animation_data.push(data);
1556        }
1557
1558        // Collect alpha track (f32 = 4 bytes)
1559        if let Some(data) = collect_ribbon_track_data(
1560            reader,
1561            &emitter.alpha_animation,
1562            emitter_idx,
1563            RibbonTrackType::Alpha,
1564        )? {
1565            animation_data.push(data);
1566        }
1567
1568        // Collect height above track (f32 = 4 bytes)
1569        if let Some(data) = collect_ribbon_track_data(
1570            reader,
1571            &emitter.height_above_animation,
1572            emitter_idx,
1573            RibbonTrackType::HeightAbove,
1574        )? {
1575            animation_data.push(data);
1576        }
1577
1578        // Collect height below track (f32 = 4 bytes)
1579        if let Some(data) = collect_ribbon_track_data(
1580            reader,
1581            &emitter.height_below_animation,
1582            emitter_idx,
1583            RibbonTrackType::HeightBelow,
1584        )? {
1585            animation_data.push(data);
1586        }
1587    }
1588
1589    Ok(animation_data)
1590}
1591
1592/// Collects raw keyframe data for a single texture animation track
1593fn collect_texture_track_data<R: Read + Seek, T: M2Parse>(
1594    reader: &mut R,
1595    block: &M2AnimationBlock<T>,
1596    animation_index: usize,
1597    track_type: TextureTrackType,
1598) -> Result<Option<TextureAnimationRaw>> {
1599    let track = &block.track;
1600
1601    // Skip empty tracks
1602    if track.timestamps.is_empty() && track.values.array.is_empty() {
1603        return Ok(None);
1604    }
1605
1606    // Read interpolation ranges (8 bytes per range: start u32 + end u32)
1607    let interpolation_ranges = if !track.interpolation_ranges.is_empty() {
1608        read_raw_bytes(reader, &track.interpolation_ranges.convert(), 8)?
1609    } else {
1610        Vec::new()
1611    };
1612
1613    // Read timestamps (4 bytes per timestamp)
1614    let timestamps = if !track.timestamps.is_empty() {
1615        read_raw_bytes(reader, &track.timestamps.convert(), 4)?
1616    } else {
1617        Vec::new()
1618    };
1619
1620    // Read values (4 bytes per f32 value)
1621    let values = if !track.values.array.is_empty() {
1622        read_raw_bytes(
1623            reader,
1624            &track.values.array.convert(),
1625            track_type.value_size(),
1626        )?
1627    } else {
1628        Vec::new()
1629    };
1630
1631    Ok(Some(TextureAnimationRaw {
1632        animation_index,
1633        track_type,
1634        interpolation_ranges,
1635        timestamps,
1636        values,
1637        original_ranges_offset: track.interpolation_ranges.offset,
1638        original_timestamps_offset: track.timestamps.offset,
1639        original_values_offset: track.values.array.offset,
1640    }))
1641}
1642
1643/// Collects raw animation keyframe data for all texture animations in the model
1644///
1645/// This reads the actual keyframe bytes (interpolation_ranges, timestamps, values) from the file,
1646/// storing them for later serialization with offset relocation.
1647fn collect_texture_animation_data<R: Read + Seek>(
1648    reader: &mut R,
1649    animations: &[M2TextureAnimation],
1650) -> Result<Vec<TextureAnimationRaw>> {
1651    let mut animation_data = Vec::new();
1652
1653    for (anim_idx, anim) in animations.iter().enumerate() {
1654        // Collect translation_u track (f32 = 4 bytes)
1655        if let Some(data) = collect_texture_track_data(
1656            reader,
1657            &anim.translation_u,
1658            anim_idx,
1659            TextureTrackType::TranslationU,
1660        )? {
1661            animation_data.push(data);
1662        }
1663
1664        // Collect translation_v track (f32 = 4 bytes)
1665        if let Some(data) = collect_texture_track_data(
1666            reader,
1667            &anim.translation_v,
1668            anim_idx,
1669            TextureTrackType::TranslationV,
1670        )? {
1671            animation_data.push(data);
1672        }
1673
1674        // Collect rotation track (f32 = 4 bytes)
1675        if let Some(data) = collect_texture_track_data(
1676            reader,
1677            &anim.rotation,
1678            anim_idx,
1679            TextureTrackType::Rotation,
1680        )? {
1681            animation_data.push(data);
1682        }
1683
1684        // Collect scale_u track (f32 = 4 bytes)
1685        if let Some(data) =
1686            collect_texture_track_data(reader, &anim.scale_u, anim_idx, TextureTrackType::ScaleU)?
1687        {
1688            animation_data.push(data);
1689        }
1690
1691        // Collect scale_v track (f32 = 4 bytes)
1692        if let Some(data) =
1693            collect_texture_track_data(reader, &anim.scale_v, anim_idx, TextureTrackType::ScaleV)?
1694        {
1695            animation_data.push(data);
1696        }
1697    }
1698
1699    Ok(animation_data)
1700}
1701
1702/// Collects raw keyframe data for a single color animation track
1703fn collect_color_track_data<R: Read + Seek, T: M2Parse>(
1704    reader: &mut R,
1705    block: &M2AnimationBlock<T>,
1706    animation_index: usize,
1707    track_type: ColorTrackType,
1708) -> Result<Option<ColorAnimationRaw>> {
1709    let track = &block.track;
1710
1711    // Skip empty tracks
1712    if track.timestamps.is_empty() && track.values.array.is_empty() {
1713        return Ok(None);
1714    }
1715
1716    // Read interpolation ranges (8 bytes per range: start u32 + end u32)
1717    let interpolation_ranges = if !track.interpolation_ranges.is_empty() {
1718        read_raw_bytes(reader, &track.interpolation_ranges.convert(), 8)?
1719    } else {
1720        Vec::new()
1721    };
1722
1723    // Read timestamps (4 bytes per timestamp)
1724    let timestamps = if !track.timestamps.is_empty() {
1725        read_raw_bytes(reader, &track.timestamps.convert(), 4)?
1726    } else {
1727        Vec::new()
1728    };
1729
1730    // Read values (size depends on track type)
1731    let values = if !track.values.array.is_empty() {
1732        read_raw_bytes(
1733            reader,
1734            &track.values.array.convert(),
1735            track_type.value_size(),
1736        )?
1737    } else {
1738        Vec::new()
1739    };
1740
1741    Ok(Some(ColorAnimationRaw {
1742        animation_index,
1743        track_type,
1744        interpolation_ranges,
1745        timestamps,
1746        values,
1747        original_ranges_offset: track.interpolation_ranges.offset,
1748        original_timestamps_offset: track.timestamps.offset,
1749        original_values_offset: track.values.array.offset,
1750    }))
1751}
1752
1753/// Collects raw animation keyframe data for all color animations in the model
1754///
1755/// This reads the actual keyframe bytes (interpolation_ranges, timestamps, values) from the file,
1756/// storing them for later serialization with offset relocation.
1757fn collect_color_animation_data<R: Read + Seek>(
1758    reader: &mut R,
1759    animations: &[M2ColorAnimation],
1760) -> Result<Vec<ColorAnimationRaw>> {
1761    let mut animation_data = Vec::new();
1762
1763    for (anim_idx, anim) in animations.iter().enumerate() {
1764        // Collect color track (M2Color = 12 bytes)
1765        if let Some(data) =
1766            collect_color_track_data(reader, &anim.color, anim_idx, ColorTrackType::Color)?
1767        {
1768            animation_data.push(data);
1769        }
1770
1771        // Collect alpha track (u16 = 2 bytes)
1772        if let Some(data) =
1773            collect_color_track_data(reader, &anim.alpha, anim_idx, ColorTrackType::Alpha)?
1774        {
1775            animation_data.push(data);
1776        }
1777    }
1778
1779    Ok(animation_data)
1780}
1781
1782/// Collects raw keyframe data for a single transparency animation track
1783fn collect_transparency_track_data<R: Read + Seek, T: M2Parse>(
1784    reader: &mut R,
1785    block: &M2AnimationBlock<T>,
1786    animation_index: usize,
1787    track_type: TransparencyTrackType,
1788) -> Result<Option<TransparencyAnimationRaw>> {
1789    let track = &block.track;
1790
1791    // Skip empty tracks
1792    if track.timestamps.is_empty() && track.values.array.is_empty() {
1793        return Ok(None);
1794    }
1795
1796    // Read interpolation ranges (8 bytes per range: start u32 + end u32)
1797    let interpolation_ranges = if !track.interpolation_ranges.is_empty() {
1798        read_raw_bytes(reader, &track.interpolation_ranges.convert(), 8)?
1799    } else {
1800        Vec::new()
1801    };
1802
1803    // Read timestamps (4 bytes per timestamp)
1804    let timestamps = if !track.timestamps.is_empty() {
1805        read_raw_bytes(reader, &track.timestamps.convert(), 4)?
1806    } else {
1807        Vec::new()
1808    };
1809
1810    // Read values (f32 = 4 bytes per alpha value)
1811    let values = if !track.values.array.is_empty() {
1812        read_raw_bytes(
1813            reader,
1814            &track.values.array.convert(),
1815            track_type.value_size(),
1816        )?
1817    } else {
1818        Vec::new()
1819    };
1820
1821    Ok(Some(TransparencyAnimationRaw {
1822        animation_index,
1823        track_type,
1824        interpolation_ranges,
1825        timestamps,
1826        values,
1827        original_ranges_offset: track.interpolation_ranges.offset,
1828        original_timestamps_offset: track.timestamps.offset,
1829        original_values_offset: track.values.array.offset,
1830    }))
1831}
1832
1833/// Collects raw animation keyframe data for all transparency animations in the model
1834///
1835/// This reads the actual keyframe bytes (interpolation_ranges, timestamps, values) from the file,
1836/// storing them for later serialization with offset relocation.
1837fn collect_transparency_animation_data<R: Read + Seek>(
1838    reader: &mut R,
1839    animations: &[M2TransparencyAnimation],
1840) -> Result<Vec<TransparencyAnimationRaw>> {
1841    let mut animation_data = Vec::new();
1842
1843    for (anim_idx, anim) in animations.iter().enumerate() {
1844        // Collect alpha track (f32 = 4 bytes)
1845        if let Some(data) = collect_transparency_track_data(
1846            reader,
1847            &anim.alpha,
1848            anim_idx,
1849            TransparencyTrackType::Alpha,
1850        )? {
1851            animation_data.push(data);
1852        }
1853    }
1854
1855    Ok(animation_data)
1856}
1857
1858/// Collects raw event track data for all events in the model
1859///
1860/// Events have two M2Arrays: ranges (per-animation timing) and times (timestamps).
1861/// This is different from M2AnimationBlock - events are simpler trigger points.
1862fn collect_event_data<R: Read + Seek>(reader: &mut R, events: &[M2Event]) -> Result<Vec<EventRaw>> {
1863    let mut event_data = Vec::new();
1864
1865    for (event_idx, event) in events.iter().enumerate() {
1866        // Skip events with no data
1867        if event.ranges.count == 0 && event.times.count == 0 {
1868            continue;
1869        }
1870
1871        // Read ranges (8 bytes per range: start u32, end u32)
1872        let ranges = if event.ranges.count > 0 {
1873            read_raw_bytes(reader, &event.ranges.convert(), 8)?
1874        } else {
1875            Vec::new()
1876        };
1877
1878        // Read timestamps (4 bytes per u32 timestamp)
1879        let timestamps = if event.times.count > 0 {
1880            read_raw_bytes(reader, &event.times.convert(), 4)?
1881        } else {
1882            Vec::new()
1883        };
1884
1885        event_data.push(EventRaw {
1886            event_index: event_idx,
1887            ranges,
1888            original_ranges_offset: event.ranges.offset,
1889            timestamps,
1890            original_timestamps_offset: event.times.offset,
1891        });
1892    }
1893
1894    Ok(event_data)
1895}
1896
1897/// Collects raw keyframe data for a single attachment animation track
1898fn collect_attachment_track_data<R: Read + Seek, T: M2Parse>(
1899    reader: &mut R,
1900    block: &M2AnimationBlock<T>,
1901    attachment_index: usize,
1902    track_type: AttachmentTrackType,
1903) -> Result<Option<AttachmentAnimationRaw>> {
1904    let track = &block.track;
1905
1906    // Skip empty tracks
1907    if track.timestamps.is_empty() && track.values.array.is_empty() {
1908        return Ok(None);
1909    }
1910
1911    // Read interpolation ranges (8 bytes per range: start u32 + end u32)
1912    let interpolation_ranges = if !track.interpolation_ranges.is_empty() {
1913        read_raw_bytes(reader, &track.interpolation_ranges.convert(), 8)?
1914    } else {
1915        Vec::new()
1916    };
1917
1918    // Read timestamps (4 bytes per timestamp)
1919    let timestamps = if !track.timestamps.is_empty() {
1920        read_raw_bytes(reader, &track.timestamps.convert(), 4)?
1921    } else {
1922        Vec::new()
1923    };
1924
1925    // Read values (size depends on track type)
1926    let values = if !track.values.array.is_empty() {
1927        read_raw_bytes(
1928            reader,
1929            &track.values.array.convert(),
1930            track_type.value_size(),
1931        )?
1932    } else {
1933        Vec::new()
1934    };
1935
1936    Ok(Some(AttachmentAnimationRaw {
1937        attachment_index,
1938        track_type,
1939        interpolation_ranges,
1940        timestamps,
1941        values,
1942        original_ranges_offset: track.interpolation_ranges.offset,
1943        original_timestamps_offset: track.timestamps.offset,
1944        original_values_offset: track.values.array.offset,
1945    }))
1946}
1947
1948/// Collects raw animation keyframe data for all attachments in the model
1949///
1950/// This reads the actual keyframe bytes (interpolation_ranges, timestamps, values) from the file,
1951/// storing them for later serialization with offset relocation.
1952fn collect_attachment_animation_data<R: Read + Seek>(
1953    reader: &mut R,
1954    attachments: &[M2Attachment],
1955) -> Result<Vec<AttachmentAnimationRaw>> {
1956    let mut animation_data = Vec::new();
1957
1958    for (attach_idx, attachment) in attachments.iter().enumerate() {
1959        // Collect scale track (f32 = 4 bytes)
1960        if let Some(data) = collect_attachment_track_data(
1961            reader,
1962            &attachment.scale_animation,
1963            attach_idx,
1964            AttachmentTrackType::Scale,
1965        )? {
1966            animation_data.push(data);
1967        }
1968    }
1969
1970    Ok(animation_data)
1971}
1972
1973/// Collects raw keyframe data for a single camera animation track
1974fn collect_camera_track_data<R: Read + Seek, T: M2Parse>(
1975    reader: &mut R,
1976    block: &M2AnimationBlock<T>,
1977    camera_index: usize,
1978    track_type: CameraTrackType,
1979) -> Result<Option<CameraAnimationRaw>> {
1980    let track = &block.track;
1981
1982    // Skip empty tracks
1983    if track.timestamps.is_empty() && track.values.array.is_empty() {
1984        return Ok(None);
1985    }
1986
1987    // Read interpolation ranges (8 bytes per range: start u32 + end u32)
1988    let interpolation_ranges = if !track.interpolation_ranges.is_empty() {
1989        read_raw_bytes(reader, &track.interpolation_ranges.convert(), 8)?
1990    } else {
1991        Vec::new()
1992    };
1993
1994    // Read timestamps (4 bytes per timestamp)
1995    let timestamps = if !track.timestamps.is_empty() {
1996        read_raw_bytes(reader, &track.timestamps.convert(), 4)?
1997    } else {
1998        Vec::new()
1999    };
2000
2001    // Read values (size depends on track type)
2002    let values = if !track.values.array.is_empty() {
2003        read_raw_bytes(
2004            reader,
2005            &track.values.array.convert(),
2006            track_type.value_size(),
2007        )?
2008    } else {
2009        Vec::new()
2010    };
2011
2012    Ok(Some(CameraAnimationRaw {
2013        camera_index,
2014        track_type,
2015        interpolation_ranges,
2016        timestamps,
2017        values,
2018        original_ranges_offset: track.interpolation_ranges.offset,
2019        original_timestamps_offset: track.timestamps.offset,
2020        original_values_offset: track.values.array.offset,
2021    }))
2022}
2023
2024/// Collects raw animation keyframe data for all cameras in the model
2025///
2026/// This reads the actual keyframe bytes (interpolation_ranges, timestamps, values) from the file,
2027/// storing them for later serialization with offset relocation.
2028fn collect_camera_animation_data<R: Read + Seek>(
2029    reader: &mut R,
2030    cameras: &[M2Camera],
2031) -> Result<Vec<CameraAnimationRaw>> {
2032    let mut animation_data = Vec::new();
2033
2034    for (camera_idx, camera) in cameras.iter().enumerate() {
2035        // Collect position track (C3Vector = 12 bytes)
2036        if let Some(data) = collect_camera_track_data(
2037            reader,
2038            &camera.position_animation,
2039            camera_idx,
2040            CameraTrackType::Position,
2041        )? {
2042            animation_data.push(data);
2043        }
2044
2045        // Collect target position track (C3Vector = 12 bytes)
2046        if let Some(data) = collect_camera_track_data(
2047            reader,
2048            &camera.target_position_animation,
2049            camera_idx,
2050            CameraTrackType::TargetPosition,
2051        )? {
2052            animation_data.push(data);
2053        }
2054
2055        // Collect roll track (f32 = 4 bytes)
2056        if let Some(data) = collect_camera_track_data(
2057            reader,
2058            &camera.roll_animation,
2059            camera_idx,
2060            CameraTrackType::Roll,
2061        )? {
2062            animation_data.push(data);
2063        }
2064    }
2065
2066    Ok(animation_data)
2067}
2068
2069/// Collects raw keyframe data for a single light animation track
2070fn collect_light_track_data<R: Read + Seek, T: M2Parse>(
2071    reader: &mut R,
2072    block: &M2AnimationBlock<T>,
2073    light_index: usize,
2074    track_type: LightTrackType,
2075) -> Result<Option<LightAnimationRaw>> {
2076    let track = &block.track;
2077
2078    // Skip empty tracks
2079    if track.timestamps.is_empty() && track.values.array.is_empty() {
2080        return Ok(None);
2081    }
2082
2083    // Read interpolation ranges (8 bytes per range: start u32 + end u32)
2084    let interpolation_ranges = if !track.interpolation_ranges.is_empty() {
2085        read_raw_bytes(reader, &track.interpolation_ranges.convert(), 8)?
2086    } else {
2087        Vec::new()
2088    };
2089
2090    // Read timestamps (4 bytes per timestamp)
2091    let timestamps = if !track.timestamps.is_empty() {
2092        read_raw_bytes(reader, &track.timestamps.convert(), 4)?
2093    } else {
2094        Vec::new()
2095    };
2096
2097    // Read values (size depends on track type)
2098    let values = if !track.values.array.is_empty() {
2099        read_raw_bytes(
2100            reader,
2101            &track.values.array.convert(),
2102            track_type.value_size(),
2103        )?
2104    } else {
2105        Vec::new()
2106    };
2107
2108    Ok(Some(LightAnimationRaw {
2109        light_index,
2110        track_type,
2111        interpolation_ranges,
2112        timestamps,
2113        values,
2114        original_ranges_offset: track.interpolation_ranges.offset,
2115        original_timestamps_offset: track.timestamps.offset,
2116        original_values_offset: track.values.array.offset,
2117    }))
2118}
2119
2120/// Collects raw animation keyframe data for all lights in the model
2121///
2122/// This reads the actual keyframe bytes (interpolation_ranges, timestamps, values) from the file,
2123/// storing them for later serialization with offset relocation.
2124fn collect_light_animation_data<R: Read + Seek>(
2125    reader: &mut R,
2126    lights: &[M2Light],
2127) -> Result<Vec<LightAnimationRaw>> {
2128    let mut animation_data = Vec::new();
2129
2130    for (light_idx, light) in lights.iter().enumerate() {
2131        // Collect ambient color track (M2Color = 12 bytes: 3 × f32)
2132        if let Some(data) = collect_light_track_data(
2133            reader,
2134            &light.ambient_color_animation,
2135            light_idx,
2136            LightTrackType::AmbientColor,
2137        )? {
2138            animation_data.push(data);
2139        }
2140
2141        // Collect diffuse color track (M2Color = 12 bytes: 3 × f32)
2142        if let Some(data) = collect_light_track_data(
2143            reader,
2144            &light.diffuse_color_animation,
2145            light_idx,
2146            LightTrackType::DiffuseColor,
2147        )? {
2148            animation_data.push(data);
2149        }
2150
2151        // Collect attenuation start track (f32 = 4 bytes)
2152        if let Some(data) = collect_light_track_data(
2153            reader,
2154            &light.attenuation_start_animation,
2155            light_idx,
2156            LightTrackType::AttenuationStart,
2157        )? {
2158            animation_data.push(data);
2159        }
2160
2161        // Collect attenuation end track (f32 = 4 bytes)
2162        if let Some(data) = collect_light_track_data(
2163            reader,
2164            &light.attenuation_end_animation,
2165            light_idx,
2166            LightTrackType::AttenuationEnd,
2167        )? {
2168            animation_data.push(data);
2169        }
2170
2171        // Collect visibility track (f32 = 4 bytes)
2172        if let Some(data) = collect_light_track_data(
2173            reader,
2174            &light.visibility_animation,
2175            light_idx,
2176            LightTrackType::Visibility,
2177        )? {
2178            animation_data.push(data);
2179        }
2180    }
2181
2182    Ok(animation_data)
2183}
2184
2185/// Collects raw embedded skin data for pre-WotLK M2 files (version <= 263)
2186///
2187/// Pre-WotLK models have skin profile data embedded directly in the M2 file.
2188/// This function reads the ModelView structures and all data they reference.
2189fn collect_embedded_skin_data<R: Read + Seek>(
2190    reader: &mut R,
2191    header: &M2Header,
2192) -> Result<Vec<EmbeddedSkinRaw>> {
2193    // Only pre-WotLK (version <= 263) has embedded skins
2194    // Version 264+ uses external .skin files
2195    if header.version > 263 {
2196        return Ok(Vec::new());
2197    }
2198
2199    // Check if views array has data
2200    if header.views.count == 0 || header.views.offset == 0 {
2201        return Ok(Vec::new());
2202    }
2203
2204    let mut embedded_skins = Vec::new();
2205    const MODEL_VIEW_SIZE: usize = 44; // 5 M2Arrays (8 bytes each) + 1 u32 = 44 bytes
2206
2207    // Submesh sizes vary by version
2208    let submesh_size = if header.version < 260 { 32 } else { 48 };
2209
2210    // Read each ModelView structure
2211    for view_idx in 0..header.views.count {
2212        let model_view_offset = header.views.offset + (view_idx * MODEL_VIEW_SIZE as u32);
2213
2214        // Seek to and read the ModelView structure
2215        reader.seek(SeekFrom::Start(model_view_offset as u64))?;
2216        let mut model_view = vec![0u8; MODEL_VIEW_SIZE];
2217        reader.read_exact(&mut model_view)?;
2218
2219        // Parse the M2Array offsets from ModelView
2220        // Layout: indices(8) + triangles(8) + properties(8) + submeshes(8) + batches(8) + bone_count_max(4)
2221        let n_indices =
2222            u32::from_le_bytes([model_view[0], model_view[1], model_view[2], model_view[3]]);
2223        let ofs_indices =
2224            u32::from_le_bytes([model_view[4], model_view[5], model_view[6], model_view[7]]);
2225
2226        let n_triangles =
2227            u32::from_le_bytes([model_view[8], model_view[9], model_view[10], model_view[11]]);
2228        let ofs_triangles = u32::from_le_bytes([
2229            model_view[12],
2230            model_view[13],
2231            model_view[14],
2232            model_view[15],
2233        ]);
2234
2235        let n_properties = u32::from_le_bytes([
2236            model_view[16],
2237            model_view[17],
2238            model_view[18],
2239            model_view[19],
2240        ]);
2241        let ofs_properties = u32::from_le_bytes([
2242            model_view[20],
2243            model_view[21],
2244            model_view[22],
2245            model_view[23],
2246        ]);
2247
2248        let n_submeshes = u32::from_le_bytes([
2249            model_view[24],
2250            model_view[25],
2251            model_view[26],
2252            model_view[27],
2253        ]);
2254        let ofs_submeshes = u32::from_le_bytes([
2255            model_view[28],
2256            model_view[29],
2257            model_view[30],
2258            model_view[31],
2259        ]);
2260
2261        let n_batches = u32::from_le_bytes([
2262            model_view[32],
2263            model_view[33],
2264            model_view[34],
2265            model_view[35],
2266        ]);
2267        let ofs_batches = u32::from_le_bytes([
2268            model_view[36],
2269            model_view[37],
2270            model_view[38],
2271            model_view[39],
2272        ]);
2273
2274        // Read indices data (u16 per entry)
2275        let indices = if n_indices > 0 && ofs_indices > 0 {
2276            reader.seek(SeekFrom::Start(ofs_indices as u64))?;
2277            let mut data = vec![0u8; n_indices as usize * 2];
2278            reader.read_exact(&mut data)?;
2279            data
2280        } else {
2281            Vec::new()
2282        };
2283
2284        // Read triangles data (u16 per entry)
2285        let triangles = if n_triangles > 0 && ofs_triangles > 0 {
2286            reader.seek(SeekFrom::Start(ofs_triangles as u64))?;
2287            let mut data = vec![0u8; n_triangles as usize * 2];
2288            reader.read_exact(&mut data)?;
2289            data
2290        } else {
2291            Vec::new()
2292        };
2293
2294        // Read properties data (u8 or u16 per entry depending on version)
2295        let properties = if n_properties > 0 && ofs_properties > 0 {
2296            reader.seek(SeekFrom::Start(ofs_properties as u64))?;
2297            // Properties are typically 4 bytes per entry (bone indices + padding)
2298            let mut data = vec![0u8; n_properties as usize * 4];
2299            reader.read_exact(&mut data)?;
2300            data
2301        } else {
2302            Vec::new()
2303        };
2304
2305        // Read submeshes data
2306        let submeshes = if n_submeshes > 0 && ofs_submeshes > 0 {
2307            reader.seek(SeekFrom::Start(ofs_submeshes as u64))?;
2308            let mut data = vec![0u8; n_submeshes as usize * submesh_size];
2309            reader.read_exact(&mut data)?;
2310            data
2311        } else {
2312            Vec::new()
2313        };
2314
2315        // Read batches/texture units data (24 bytes per entry)
2316        // SkinBatch: 2 bytes (flags/priority) + 22 bytes (11 u16 fields) = 24 bytes
2317        let batches = if n_batches > 0 && ofs_batches > 0 {
2318            reader.seek(SeekFrom::Start(ofs_batches as u64))?;
2319            let mut data = vec![0u8; n_batches as usize * 24];
2320            reader.read_exact(&mut data)?;
2321            data
2322        } else {
2323            Vec::new()
2324        };
2325
2326        embedded_skins.push(EmbeddedSkinRaw {
2327            model_view,
2328            indices,
2329            triangles,
2330            properties,
2331            submeshes,
2332            batches,
2333            original_model_view_offset: model_view_offset,
2334            original_indices_offset: ofs_indices,
2335            original_triangles_offset: ofs_triangles,
2336            original_properties_offset: ofs_properties,
2337            original_submeshes_offset: ofs_submeshes,
2338            original_batches_offset: ofs_batches,
2339        });
2340    }
2341
2342    Ok(embedded_skins)
2343}
2344
2345impl M2Format {
2346    /// Get the underlying M2Model regardless of format
2347    pub fn model(&self) -> &M2Model {
2348        match self {
2349            M2Format::Legacy(model) => model,
2350            M2Format::Chunked(model) => model,
2351        }
2352    }
2353
2354    /// Get mutable reference to the underlying M2Model
2355    pub fn model_mut(&mut self) -> &mut M2Model {
2356        match self {
2357            M2Format::Legacy(model) => model,
2358            M2Format::Chunked(model) => model,
2359        }
2360    }
2361
2362    /// Check if this is a chunked format model
2363    pub fn is_chunked(&self) -> bool {
2364        matches!(self, M2Format::Chunked(_))
2365    }
2366
2367    /// Check if this is a legacy format model
2368    pub fn is_legacy(&self) -> bool {
2369        matches!(self, M2Format::Legacy(_))
2370    }
2371}
2372
2373impl Default for M2Model {
2374    fn default() -> Self {
2375        Self {
2376            header: M2Header {
2377                magic: *b"MD20",
2378                version: 264, // Default to WotLK version
2379                name: M2Array::default(),
2380                flags: M2ModelFlags::empty(),
2381                global_sequences: M2Array::default(),
2382                animations: M2Array::default(),
2383                animation_lookup: M2Array::default(),
2384                playable_animation_lookup: None,
2385                bones: M2Array::default(),
2386                key_bone_lookup: M2Array::default(),
2387                vertices: M2Array::default(),
2388                views: M2Array::default(),
2389                num_skin_profiles: Some(0),
2390                color_animations: M2Array::default(),
2391                textures: M2Array::default(),
2392                transparency_lookup: M2Array::default(),
2393                texture_flipbooks: None,
2394                texture_animations: M2Array::default(),
2395                color_replacements: M2Array::default(),
2396                render_flags: M2Array::default(),
2397                bone_lookup_table: M2Array::default(),
2398                texture_lookup_table: M2Array::default(),
2399                texture_units: M2Array::default(),
2400                transparency_lookup_table: M2Array::default(),
2401                texture_animation_lookup: M2Array::default(),
2402                bounding_box_min: [0.0, 0.0, 0.0],
2403                bounding_box_max: [0.0, 0.0, 0.0],
2404                bounding_sphere_radius: 0.0,
2405                collision_box_min: [0.0, 0.0, 0.0],
2406                collision_box_max: [0.0, 0.0, 0.0],
2407                collision_sphere_radius: 0.0,
2408                bounding_triangles: M2Array::default(),
2409                bounding_vertices: M2Array::default(),
2410                bounding_normals: M2Array::default(),
2411                attachments: M2Array::default(),
2412                attachment_lookup_table: M2Array::default(),
2413                events: M2Array::default(),
2414                lights: M2Array::default(),
2415                cameras: M2Array::default(),
2416                camera_lookup_table: M2Array::default(),
2417                ribbon_emitters: M2Array::default(),
2418                particle_emitters: M2Array::default(),
2419                blend_map_overrides: None,
2420                texture_combiner_combos: None,
2421                texture_transforms: None,
2422            },
2423            name: None,
2424            global_sequences: Vec::new(),
2425            animations: Vec::new(),
2426            animation_lookup: Vec::new(),
2427            bones: Vec::new(),
2428            key_bone_lookup: Vec::new(),
2429            vertices: Vec::new(),
2430            textures: Vec::new(),
2431            materials: Vec::new(),
2432            particle_emitters: Vec::new(),
2433            ribbon_emitters: Vec::new(),
2434            texture_animations: Vec::new(),
2435            color_animations: Vec::new(),
2436            transparency_animations: Vec::new(),
2437            events: Vec::new(),
2438            attachments: Vec::new(),
2439            cameras: Vec::new(),
2440            lights: Vec::new(),
2441            raw_data: M2RawData::default(),
2442            skin_file_ids: None,
2443            animation_file_ids: None,
2444            texture_file_ids: None,
2445            physics_file_id: None,
2446            skeleton_file_id: None,
2447            bone_file_ids: None,
2448            lod_data: None,
2449            extended_particle_data: None,
2450            parent_animation_blacklist: None,
2451            parent_animation_data: None,
2452            waterfall_effect: None,
2453            edge_fade_data: None,
2454            model_alpha_data: None,
2455            lighting_details: None,
2456            recursive_particle_ids: None,
2457            geometry_particle_ids: None,
2458            texture_animation_chunk: None,
2459            particle_geoset_data: None,
2460            dboc_chunk: None,
2461            afra_chunk: None,
2462            dpiv_chunk: None,
2463            parent_sequence_bounds: None,
2464            parent_event_data: None,
2465            collision_mesh_data: None,
2466            physics_file_data: None,
2467        }
2468    }
2469}
2470
2471impl M2Model {
2472    /// Parse a legacy M2 model from a reader (MD20 format)
2473    pub fn parse_legacy<R: Read + Seek>(reader: &mut R) -> Result<Self> {
2474        Self::parse(reader)
2475    }
2476
2477    /// Parse a chunked M2 model from a reader (MD21 format)
2478    pub fn parse_chunked<R: Read + Seek>(reader: &mut R) -> Result<Self> {
2479        let mut chunks = Vec::new();
2480        let mut md21_chunk = None;
2481        let mut skin_file_ids = None;
2482        let mut animation_file_ids = None;
2483        let mut texture_file_ids = None;
2484        let mut physics_file_id = None;
2485        let mut skeleton_file_id = None;
2486        let mut bone_file_ids = None;
2487        let mut lod_data = None;
2488        let mut extended_particle_data = None;
2489        let mut parent_animation_blacklist = None;
2490        let mut parent_animation_data = None;
2491        let mut waterfall_effect = None;
2492        let mut edge_fade_data = None;
2493        let mut model_alpha_data = None;
2494        let mut lighting_details = None;
2495        let mut recursive_particle_ids = None;
2496        let mut geometry_particle_ids = None;
2497        let mut texture_animation_chunk = None;
2498        let mut particle_geoset_data = None;
2499        let mut dboc_chunk = None;
2500        let mut afra_chunk = None;
2501        let mut dpiv_chunk = None;
2502        let mut parent_sequence_bounds = None;
2503        let mut parent_event_data = None;
2504        let mut collision_mesh_data = None;
2505        let mut physics_file_data = None;
2506
2507        // Read all chunks
2508        loop {
2509            let header = match ChunkHeader::read(reader) {
2510                Ok(h) => h,
2511                Err(M2Error::Io(ref e)) if e.kind() == ErrorKind::UnexpectedEof => break,
2512                Err(e) => return Err(e),
2513            };
2514
2515            chunks.push(header.clone());
2516
2517            match &header.magic {
2518                b"MD21" => {
2519                    // We need to take the reader to avoid borrowing issues
2520                    // First, get the current position for the chunk reader
2521                    let current_pos = reader.stream_position()?;
2522
2523                    // Skip the chunk for now and store the position for later parsing
2524                    reader.seek(SeekFrom::Current(header.size as i64))?;
2525
2526                    // For now, store the header and position info for later processing
2527                    // This is a simplified approach to avoid complex borrowing
2528                    md21_chunk = Some(Self::parse_md21_simple(current_pos, &header)?);
2529                }
2530                b"SFID" => {
2531                    // Parse SFID (Skin File IDs) chunk
2532                    let current_pos = reader.stream_position()?;
2533                    let end_pos = current_pos + header.size as u64;
2534
2535                    let count = header.size / 4; // Each ID is 4 bytes
2536                    let mut ids = Vec::with_capacity(count as usize);
2537
2538                    for _ in 0..count {
2539                        ids.push(reader.read_u32_le()?);
2540                    }
2541
2542                    skin_file_ids = Some(SkinFileIds { ids });
2543
2544                    // Ensure we're at the correct position
2545                    reader.seek(SeekFrom::Start(end_pos))?;
2546                }
2547                b"AFID" => {
2548                    // Parse AFID (Animation File IDs) chunk
2549                    let current_pos = reader.stream_position()?;
2550                    let end_pos = current_pos + header.size as u64;
2551
2552                    let count = header.size / 4; // Each ID is 4 bytes
2553                    let mut ids = Vec::with_capacity(count as usize);
2554
2555                    for _ in 0..count {
2556                        ids.push(reader.read_u32_le()?);
2557                    }
2558
2559                    animation_file_ids = Some(AnimationFileIds { ids });
2560
2561                    // Ensure we're at the correct position
2562                    reader.seek(SeekFrom::Start(end_pos))?;
2563                }
2564                b"TXID" => {
2565                    // Parse TXID (Texture File IDs) chunk
2566                    let current_pos = reader.stream_position()?;
2567                    let end_pos = current_pos + header.size as u64;
2568
2569                    let count = header.size / 4; // Each ID is 4 bytes
2570                    let mut ids = Vec::with_capacity(count as usize);
2571
2572                    for _ in 0..count {
2573                        ids.push(reader.read_u32_le()?);
2574                    }
2575
2576                    texture_file_ids = Some(TextureFileIds { ids });
2577
2578                    // Ensure we're at the correct position
2579                    reader.seek(SeekFrom::Start(end_pos))?;
2580                }
2581                b"PFID" => {
2582                    // Parse PFID (Physics File ID) chunk
2583                    if header.size != 4 {
2584                        return Err(M2Error::ParseError(format!(
2585                            "PFID chunk should contain exactly 4 bytes, got {}",
2586                            header.size
2587                        )));
2588                    }
2589
2590                    let id = reader.read_u32_le()?;
2591                    physics_file_id = Some(PhysicsFileId { id });
2592                }
2593                b"SKID" => {
2594                    // Parse SKID (Skeleton File ID) chunk
2595                    if header.size != 4 {
2596                        return Err(M2Error::ParseError(format!(
2597                            "SKID chunk should contain exactly 4 bytes, got {}",
2598                            header.size
2599                        )));
2600                    }
2601
2602                    let id = reader.read_u32_le()?;
2603                    skeleton_file_id = Some(SkeletonFileId { id });
2604                }
2605                b"BFID" => {
2606                    // Parse BFID (Bone File IDs) chunk
2607                    let current_pos = reader.stream_position()?;
2608                    let end_pos = current_pos + header.size as u64;
2609
2610                    let count = header.size / 4; // Each ID is 4 bytes
2611                    let mut ids = Vec::with_capacity(count as usize);
2612
2613                    for _ in 0..count {
2614                        ids.push(reader.read_u32_le()?);
2615                    }
2616
2617                    bone_file_ids = Some(BoneFileIds { ids });
2618
2619                    // Ensure we're at the correct position
2620                    reader.seek(SeekFrom::Start(end_pos))?;
2621                }
2622                b"LDV1" => {
2623                    // Parse LDV1 (Level of Detail) chunk
2624                    let current_pos = reader.stream_position()?;
2625                    let end_pos = current_pos + header.size as u64;
2626
2627                    // LDV1 format: each LOD level is 14 bytes
2628                    const LOD_LEVEL_SIZE: u32 = 14;
2629
2630                    if header.size % LOD_LEVEL_SIZE != 0 {
2631                        return Err(M2Error::ParseError(format!(
2632                            "LDV1 chunk size {} is not a multiple of LOD level size {}",
2633                            header.size, LOD_LEVEL_SIZE
2634                        )));
2635                    }
2636
2637                    let count = header.size / LOD_LEVEL_SIZE;
2638                    let mut levels = Vec::with_capacity(count as usize);
2639
2640                    for _ in 0..count {
2641                        use crate::chunks::file_references::LodLevel;
2642
2643                        let distance = reader.read_f32_le()?;
2644                        let skin_file_index = reader.read_u16_le()?;
2645                        let vertex_count = reader.read_u32_le()?;
2646                        let triangle_count = reader.read_u32_le()?;
2647
2648                        levels.push(LodLevel {
2649                            distance,
2650                            skin_file_index,
2651                            vertex_count,
2652                            triangle_count,
2653                        });
2654                    }
2655
2656                    lod_data = Some(LodData { levels });
2657
2658                    // Ensure we're at the correct position
2659                    reader.seek(SeekFrom::Start(end_pos))?;
2660                }
2661                b"EXPT" => {
2662                    // Parse EXPT (Extended Particle v1) chunk
2663                    let current_pos = reader.stream_position()?;
2664                    let _end_pos = current_pos + header.size as u64;
2665
2666                    // Create a limited reader for this chunk
2667                    let mut chunk_data = vec![0u8; header.size as usize];
2668                    reader.read_exact(&mut chunk_data)?;
2669                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2670                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2671
2672                    extended_particle_data =
2673                        Some(ExtendedParticleData::parse_expt(&mut chunk_reader)?);
2674
2675                    // Position is already correct after reading chunk_data
2676                }
2677                b"EXP2" => {
2678                    // Parse EXP2 (Extended Particle v2) chunk
2679                    let current_pos = reader.stream_position()?;
2680                    let _end_pos = current_pos + header.size as u64;
2681
2682                    // Create a limited reader for this chunk
2683                    let mut chunk_data = vec![0u8; header.size as usize];
2684                    reader.read_exact(&mut chunk_data)?;
2685                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2686                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2687
2688                    extended_particle_data =
2689                        Some(ExtendedParticleData::parse_exp2(&mut chunk_reader)?);
2690
2691                    // Position is already correct after reading chunk_data
2692                }
2693                b"PABC" => {
2694                    // Parse PABC (Parent Animation Blacklist) chunk
2695                    let current_pos = reader.stream_position()?;
2696                    let _end_pos = current_pos + header.size as u64;
2697
2698                    // Create a limited reader for this chunk
2699                    let mut chunk_data = vec![0u8; header.size as usize];
2700                    reader.read_exact(&mut chunk_data)?;
2701                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2702                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2703
2704                    parent_animation_blacklist =
2705                        Some(ParentAnimationBlacklist::parse(&mut chunk_reader)?);
2706
2707                    // Position is already correct after reading chunk_data
2708                }
2709                b"PADC" => {
2710                    // Parse PADC (Parent Animation Data) chunk
2711                    let current_pos = reader.stream_position()?;
2712                    let _end_pos = current_pos + header.size as u64;
2713
2714                    // Create a limited reader for this chunk
2715                    let mut chunk_data = vec![0u8; header.size as usize];
2716                    reader.read_exact(&mut chunk_data)?;
2717                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2718                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2719
2720                    parent_animation_data = Some(ParentAnimationData::parse(&mut chunk_reader)?);
2721
2722                    // Position is already correct after reading chunk_data
2723                }
2724                b"WFV1" => {
2725                    // Parse WFV1 (Waterfall v1) chunk
2726                    let current_pos = reader.stream_position()?;
2727                    let _end_pos = current_pos + header.size as u64;
2728
2729                    // Create a limited reader for this chunk
2730                    let mut chunk_data = vec![0u8; header.size as usize];
2731                    reader.read_exact(&mut chunk_data)?;
2732                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2733                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2734
2735                    waterfall_effect = Some(WaterfallEffect::parse(&mut chunk_reader, 1)?);
2736
2737                    // Position is already correct after reading chunk_data
2738                }
2739                b"WFV2" => {
2740                    // Parse WFV2 (Waterfall v2) chunk
2741                    let current_pos = reader.stream_position()?;
2742                    let _end_pos = current_pos + header.size as u64;
2743
2744                    // Create a limited reader for this chunk
2745                    let mut chunk_data = vec![0u8; header.size as usize];
2746                    reader.read_exact(&mut chunk_data)?;
2747                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2748                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2749
2750                    waterfall_effect = Some(WaterfallEffect::parse(&mut chunk_reader, 2)?);
2751
2752                    // Position is already correct after reading chunk_data
2753                }
2754                b"WFV3" => {
2755                    // Parse WFV3 (Waterfall v3) chunk
2756                    let current_pos = reader.stream_position()?;
2757                    let _end_pos = current_pos + header.size as u64;
2758
2759                    // Create a limited reader for this chunk
2760                    let mut chunk_data = vec![0u8; header.size as usize];
2761                    reader.read_exact(&mut chunk_data)?;
2762                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2763                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2764
2765                    waterfall_effect = Some(WaterfallEffect::parse(&mut chunk_reader, 3)?);
2766
2767                    // Position is already correct after reading chunk_data
2768                }
2769                b"EDGF" => {
2770                    // Parse EDGF (Edge Fade) chunk
2771                    let current_pos = reader.stream_position()?;
2772                    let _end_pos = current_pos + header.size as u64;
2773
2774                    // Create a limited reader for this chunk
2775                    let mut chunk_data = vec![0u8; header.size as usize];
2776                    reader.read_exact(&mut chunk_data)?;
2777                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2778                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2779
2780                    edge_fade_data = Some(EdgeFadeData::parse(&mut chunk_reader)?);
2781
2782                    // Position is already correct after reading chunk_data
2783                }
2784                b"NERF" => {
2785                    // Parse NERF (Model Alpha) chunk
2786                    let current_pos = reader.stream_position()?;
2787                    let _end_pos = current_pos + header.size as u64;
2788
2789                    // Create a limited reader for this chunk
2790                    let mut chunk_data = vec![0u8; header.size as usize];
2791                    reader.read_exact(&mut chunk_data)?;
2792                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2793                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2794
2795                    model_alpha_data = Some(ModelAlphaData::parse(&mut chunk_reader)?);
2796
2797                    // Position is already correct after reading chunk_data
2798                }
2799                b"DETL" => {
2800                    // Parse DETL (Lighting Details) chunk
2801                    let current_pos = reader.stream_position()?;
2802                    let _end_pos = current_pos + header.size as u64;
2803
2804                    // Create a limited reader for this chunk
2805                    let mut chunk_data = vec![0u8; header.size as usize];
2806                    reader.read_exact(&mut chunk_data)?;
2807                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2808                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2809
2810                    lighting_details = Some(LightingDetails::parse(&mut chunk_reader)?);
2811
2812                    // Position is already correct after reading chunk_data
2813                }
2814                b"RPID" => {
2815                    // Parse RPID (Recursive Particle IDs) chunk
2816                    let current_pos = reader.stream_position()?;
2817                    let _end_pos = current_pos + header.size as u64;
2818
2819                    // Create a limited reader for this chunk
2820                    let mut chunk_data = vec![0u8; header.size as usize];
2821                    reader.read_exact(&mut chunk_data)?;
2822                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2823                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2824
2825                    recursive_particle_ids = Some(RecursiveParticleIds::parse(&mut chunk_reader)?);
2826
2827                    // Position is already correct after reading chunk_data
2828                }
2829                b"GPID" => {
2830                    // Parse GPID (Geometry Particle IDs) chunk
2831                    let current_pos = reader.stream_position()?;
2832                    let _end_pos = current_pos + header.size as u64;
2833
2834                    // Create a limited reader for this chunk
2835                    let mut chunk_data = vec![0u8; header.size as usize];
2836                    reader.read_exact(&mut chunk_data)?;
2837                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2838                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2839
2840                    geometry_particle_ids = Some(GeometryParticleIds::parse(&mut chunk_reader)?);
2841
2842                    // Position is already correct after reading chunk_data
2843                }
2844                b"TXAC" => {
2845                    // Parse TXAC (Texture Animation) chunk
2846                    let current_pos = reader.stream_position()?;
2847                    let _end_pos = current_pos + header.size as u64;
2848
2849                    // Create a limited reader for this chunk
2850                    let mut chunk_data = vec![0u8; header.size as usize];
2851                    reader.read_exact(&mut chunk_data)?;
2852                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2853                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2854
2855                    texture_animation_chunk =
2856                        Some(TextureAnimationChunk::parse(&mut chunk_reader)?);
2857
2858                    // Position is already correct after reading chunk_data
2859                }
2860                b"PGD1" => {
2861                    // Parse PGD1 (Particle Geoset Data) chunk
2862                    let current_pos = reader.stream_position()?;
2863                    let _end_pos = current_pos + header.size as u64;
2864
2865                    // Create a limited reader for this chunk
2866                    let mut chunk_data = vec![0u8; header.size as usize];
2867                    reader.read_exact(&mut chunk_data)?;
2868                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2869                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2870
2871                    particle_geoset_data = Some(ParticleGeosetData::parse(&mut chunk_reader)?);
2872
2873                    // Position is already correct after reading chunk_data
2874                }
2875                b"DBOC" => {
2876                    // Parse DBOC (unknown purpose) chunk
2877                    let current_pos = reader.stream_position()?;
2878                    let _end_pos = current_pos + header.size as u64;
2879
2880                    // Create a limited reader for this chunk
2881                    let mut chunk_data = vec![0u8; header.size as usize];
2882                    reader.read_exact(&mut chunk_data)?;
2883                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2884                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2885
2886                    dboc_chunk = Some(DbocChunk::parse(&mut chunk_reader)?);
2887
2888                    // Position is already correct after reading chunk_data
2889                }
2890                b"AFRA" => {
2891                    // Parse AFRA (unknown purpose) chunk
2892                    let current_pos = reader.stream_position()?;
2893                    let _end_pos = current_pos + header.size as u64;
2894
2895                    // Create a limited reader for this chunk
2896                    let mut chunk_data = vec![0u8; header.size as usize];
2897                    reader.read_exact(&mut chunk_data)?;
2898                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2899                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2900
2901                    afra_chunk = Some(AfraChunk::parse(&mut chunk_reader)?);
2902
2903                    // Position is already correct after reading chunk_data
2904                }
2905                b"DPIV" => {
2906                    // Parse DPIV (collision mesh for player housing) chunk
2907                    let current_pos = reader.stream_position()?;
2908                    let _end_pos = current_pos + header.size as u64;
2909
2910                    // Create a limited reader for this chunk
2911                    let mut chunk_data = vec![0u8; header.size as usize];
2912                    reader.read_exact(&mut chunk_data)?;
2913                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2914                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2915
2916                    dpiv_chunk = Some(DpivChunk::parse(&mut chunk_reader)?);
2917
2918                    // Position is already correct after reading chunk_data
2919                }
2920                b"PSBC" => {
2921                    // Parse PSBC (Parent Sequence Bounds) chunk
2922                    let current_pos = reader.stream_position()?;
2923                    let _end_pos = current_pos + header.size as u64;
2924
2925                    // Create a limited reader for this chunk
2926                    let mut chunk_data = vec![0u8; header.size as usize];
2927                    reader.read_exact(&mut chunk_data)?;
2928                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2929                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2930
2931                    parent_sequence_bounds = Some(ParentSequenceBounds::parse(&mut chunk_reader)?);
2932
2933                    // Position is already correct after reading chunk_data
2934                }
2935                b"PEDC" => {
2936                    // Parse PEDC (Parent Event Data) chunk
2937                    let current_pos = reader.stream_position()?;
2938                    let _end_pos = current_pos + header.size as u64;
2939
2940                    // Create a limited reader for this chunk
2941                    let mut chunk_data = vec![0u8; header.size as usize];
2942                    reader.read_exact(&mut chunk_data)?;
2943                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2944                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2945
2946                    parent_event_data = Some(ParentEventData::parse(&mut chunk_reader)?);
2947
2948                    // Position is already correct after reading chunk_data
2949                }
2950                b"PCOL" => {
2951                    // Parse PCOL (Collision Mesh Data) chunk
2952                    let current_pos = reader.stream_position()?;
2953                    let _end_pos = current_pos + header.size as u64;
2954
2955                    // Create a limited reader for this chunk
2956                    let mut chunk_data = vec![0u8; header.size as usize];
2957                    reader.read_exact(&mut chunk_data)?;
2958                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2959                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2960
2961                    collision_mesh_data = Some(CollisionMeshData::parse(&mut chunk_reader)?);
2962
2963                    // Position is already correct after reading chunk_data
2964                }
2965                b"PFDC" => {
2966                    // Parse PFDC (Physics File Data) chunk
2967                    let current_pos = reader.stream_position()?;
2968                    let _end_pos = current_pos + header.size as u64;
2969
2970                    // Create a limited reader for this chunk
2971                    let mut chunk_data = vec![0u8; header.size as usize];
2972                    reader.read_exact(&mut chunk_data)?;
2973                    let chunk_cursor = std::io::Cursor::new(chunk_data);
2974                    let mut chunk_reader = ChunkReader::new(chunk_cursor, header.clone())?;
2975
2976                    physics_file_data = Some(PhysicsFileDataChunk::parse(&mut chunk_reader)?);
2977
2978                    // Position is already correct after reading chunk_data
2979                }
2980                _ => {
2981                    // Skip unknown chunks
2982                    reader.seek(SeekFrom::Current(header.size as i64))?;
2983                }
2984            }
2985        }
2986
2987        // Get the base model from MD21 chunk
2988        let mut model = md21_chunk.ok_or(M2Error::MissingMD21Chunk)?;
2989
2990        // Add the file reference data
2991        model.skin_file_ids = skin_file_ids;
2992        model.animation_file_ids = animation_file_ids;
2993        model.texture_file_ids = texture_file_ids;
2994        model.physics_file_id = physics_file_id;
2995        model.skeleton_file_id = skeleton_file_id;
2996        model.bone_file_ids = bone_file_ids;
2997        model.lod_data = lod_data;
2998        model.extended_particle_data = extended_particle_data;
2999        model.parent_animation_blacklist = parent_animation_blacklist;
3000        model.parent_animation_data = parent_animation_data;
3001        model.waterfall_effect = waterfall_effect;
3002        model.edge_fade_data = edge_fade_data;
3003        model.model_alpha_data = model_alpha_data;
3004        model.lighting_details = lighting_details;
3005        model.recursive_particle_ids = recursive_particle_ids;
3006        model.geometry_particle_ids = geometry_particle_ids;
3007        model.texture_animation_chunk = texture_animation_chunk;
3008        model.particle_geoset_data = particle_geoset_data;
3009        model.dboc_chunk = dboc_chunk;
3010        model.afra_chunk = afra_chunk;
3011        model.dpiv_chunk = dpiv_chunk;
3012        model.parent_sequence_bounds = parent_sequence_bounds;
3013        model.parent_event_data = parent_event_data;
3014        model.collision_mesh_data = collision_mesh_data;
3015        model.physics_file_data = physics_file_data;
3016
3017        Ok(model)
3018    }
3019
3020    /// Parse the MD21 chunk containing legacy M2 data (simplified version)
3021    fn parse_md21_simple(_chunk_pos: u64, _header: &ChunkHeader) -> Result<Self> {
3022        // Simplified implementation for P0-003 basic functionality
3023        // This creates a minimal model structure without full parsing
3024        Ok(Self {
3025            header: M2Header::new(M2Version::Legion),
3026            name: None,
3027            global_sequences: Vec::new(),
3028            animations: Vec::new(),
3029            animation_lookup: Vec::new(),
3030            bones: Vec::new(),
3031            key_bone_lookup: Vec::new(),
3032            vertices: Vec::new(),
3033            textures: Vec::new(),
3034            materials: Vec::new(),
3035            particle_emitters: Vec::new(),
3036            ribbon_emitters: Vec::new(),
3037            texture_animations: Vec::new(),
3038            color_animations: Vec::new(),
3039            transparency_animations: Vec::new(),
3040            events: Vec::new(),
3041            attachments: Vec::new(),
3042            cameras: Vec::new(),
3043            lights: Vec::new(),
3044            raw_data: M2RawData::default(),
3045            skin_file_ids: None,
3046            animation_file_ids: None,
3047            texture_file_ids: None,
3048            physics_file_id: None,
3049            skeleton_file_id: None,
3050            bone_file_ids: None,
3051            lod_data: None,
3052            extended_particle_data: None,
3053            parent_animation_blacklist: None,
3054            parent_animation_data: None,
3055            waterfall_effect: None,
3056            edge_fade_data: None,
3057            model_alpha_data: None,
3058            lighting_details: None,
3059            recursive_particle_ids: None,
3060            geometry_particle_ids: None,
3061            texture_animation_chunk: None,
3062            particle_geoset_data: None,
3063            dboc_chunk: None,
3064            afra_chunk: None,
3065            dpiv_chunk: None,
3066            parent_sequence_bounds: None,
3067            parent_event_data: None,
3068            collision_mesh_data: None,
3069            physics_file_data: None,
3070        })
3071    }
3072
3073    /// Parse the MD21 chunk containing legacy M2 data (full implementation - TODO)
3074    fn _parse_md21_chunk<R: Read + Seek>(mut reader: ChunkReader<R>) -> Result<Self> {
3075        // The MD21 chunk contains the legacy M2 structure with chunk-relative offsets
3076        // We need to parse it similar to the legacy format but handle offset resolution differently
3077
3078        // Parse the header from within the chunk (this doesn't include the "MD21" magic)
3079        // The MD21 chunk contains the full legacy M2 structure starting with "MD20" magic
3080        let chunk_inner = reader.inner();
3081
3082        // Read magic and version (should be MD20 within the chunk)
3083        let mut magic = [0u8; 4];
3084        chunk_inner.read_exact(&mut magic)?;
3085
3086        if magic != M2_MAGIC_LEGACY {
3087            return Err(M2Error::InvalidMagic {
3088                expected: String::from_utf8_lossy(&M2_MAGIC_LEGACY).to_string(),
3089                actual: String::from_utf8_lossy(&magic).to_string(),
3090            });
3091        }
3092
3093        // Read version
3094        let version = chunk_inner.read_u32_le()?;
3095
3096        // Check if version is supported
3097        if M2Version::from_header_version(version).is_none() {
3098            return Err(M2Error::UnsupportedVersion(version.to_string()));
3099        }
3100
3101        // Parse the rest of the header using the existing logic
3102        // But we need to construct it manually since we already read magic and version
3103        let name = M2Array::parse(chunk_inner)?;
3104        let flags = M2ModelFlags::from_bits_retain(chunk_inner.read_u32_le()?);
3105
3106        let global_sequences = M2Array::parse(chunk_inner)?;
3107        let animations = M2Array::parse(chunk_inner)?;
3108        let animation_lookup = M2Array::parse(chunk_inner)?;
3109
3110        // Vanilla/TBC versions have playable animation lookup
3111        let playable_animation_lookup = if version <= 263 {
3112            Some(M2Array::parse(chunk_inner)?)
3113        } else {
3114            None
3115        };
3116
3117        let bones = M2Array::parse(chunk_inner)?;
3118        let key_bone_lookup = M2Array::parse(chunk_inner)?;
3119
3120        let vertices = M2Array::parse(chunk_inner)?;
3121
3122        // Views field changes between versions
3123        let (views, num_skin_profiles) = if version <= 263 {
3124            // BC and earlier: views is M2Array
3125            (M2Array::parse(chunk_inner)?, None)
3126        } else {
3127            // WotLK+: views becomes a count (num_skin_profiles)
3128            let count = chunk_inner.read_u32_le()?;
3129            (M2Array::new(0, 0), Some(count))
3130        };
3131
3132        let color_animations = M2Array::parse(chunk_inner)?;
3133
3134        let textures = M2Array::parse(chunk_inner)?;
3135        let transparency_lookup = M2Array::parse(chunk_inner)?;
3136
3137        // Texture flipbooks only exist in BC and earlier
3138        let texture_flipbooks = if version <= 263 {
3139            Some(M2Array::parse(chunk_inner)?)
3140        } else {
3141            None
3142        };
3143
3144        let texture_animations = M2Array::parse(chunk_inner)?;
3145
3146        let color_replacements = M2Array::parse(chunk_inner)?;
3147        let render_flags = M2Array::parse(chunk_inner)?;
3148        let bone_lookup_table = M2Array::parse(chunk_inner)?;
3149        let texture_lookup_table = M2Array::parse(chunk_inner)?;
3150        let texture_units = M2Array::parse(chunk_inner)?;
3151        let transparency_lookup_table = M2Array::parse(chunk_inner)?;
3152        let mut texture_animation_lookup = M2Array::parse(chunk_inner)?;
3153
3154        // Workaround for corrupted texture_animation_lookup fields
3155        if texture_animation_lookup.count > 1_000_000 {
3156            texture_animation_lookup = M2Array::new(0, 0);
3157        }
3158
3159        // Read bounding box data
3160        let mut bounding_box_min = [0.0; 3];
3161        let mut bounding_box_max = [0.0; 3];
3162
3163        for item in &mut bounding_box_min {
3164            *item = chunk_inner.read_f32_le()?;
3165        }
3166
3167        for item in &mut bounding_box_max {
3168            *item = chunk_inner.read_f32_le()?;
3169        }
3170
3171        let bounding_sphere_radius = chunk_inner.read_f32_le()?;
3172
3173        // Read collision box
3174        let mut collision_box_min = [0.0; 3];
3175        let mut collision_box_max = [0.0; 3];
3176
3177        for item in &mut collision_box_min {
3178            *item = chunk_inner.read_f32_le()?;
3179        }
3180
3181        for item in &mut collision_box_max {
3182            *item = chunk_inner.read_f32_le()?;
3183        }
3184
3185        let collision_sphere_radius = chunk_inner.read_f32_le()?;
3186
3187        let bounding_triangles = M2Array::parse(chunk_inner)?;
3188        let bounding_vertices = M2Array::parse(chunk_inner)?;
3189        let bounding_normals = M2Array::parse(chunk_inner)?;
3190
3191        let attachments = M2Array::parse(chunk_inner)?;
3192        let attachment_lookup_table = M2Array::parse(chunk_inner)?;
3193        let events = M2Array::parse(chunk_inner)?;
3194        let lights = M2Array::parse(chunk_inner)?;
3195        let cameras = M2Array::parse(chunk_inner)?;
3196        let camera_lookup_table = M2Array::parse(chunk_inner)?;
3197
3198        let ribbon_emitters = M2Array::parse(chunk_inner)?;
3199        let particle_emitters = M2Array::parse(chunk_inner)?;
3200
3201        // Version-specific fields
3202        let m2_version = M2Version::from_header_version(version).unwrap();
3203
3204        let blend_map_overrides = if version >= 260 && (flags.bits() & 0x8000000 != 0) {
3205            Some(M2Array::parse(chunk_inner)?)
3206        } else {
3207            None
3208        };
3209
3210        let texture_combiner_combos = if m2_version >= M2Version::Cataclysm {
3211            Some(M2Array::parse(chunk_inner)?)
3212        } else {
3213            None
3214        };
3215
3216        let texture_transforms = if m2_version >= M2Version::Legion {
3217            Some(M2Array::parse(chunk_inner)?)
3218        } else {
3219            None
3220        };
3221
3222        // Create the header structure
3223        let header = M2Header {
3224            magic: M2_MAGIC_LEGACY,
3225            version,
3226            name,
3227            flags,
3228            global_sequences,
3229            animations,
3230            animation_lookup,
3231            playable_animation_lookup,
3232            bones,
3233            key_bone_lookup,
3234            vertices,
3235            views,
3236            num_skin_profiles,
3237            color_animations,
3238            textures,
3239            transparency_lookup,
3240            texture_flipbooks,
3241            texture_animations,
3242            color_replacements,
3243            render_flags,
3244            bone_lookup_table,
3245            texture_lookup_table,
3246            texture_units,
3247            transparency_lookup_table,
3248            texture_animation_lookup,
3249            bounding_box_min,
3250            bounding_box_max,
3251            bounding_sphere_radius,
3252            collision_box_min,
3253            collision_box_max,
3254            collision_sphere_radius,
3255            bounding_triangles,
3256            bounding_vertices,
3257            bounding_normals,
3258            attachments,
3259            attachment_lookup_table,
3260            events,
3261            lights,
3262            cameras,
3263            camera_lookup_table,
3264            ribbon_emitters,
3265            particle_emitters,
3266            blend_map_overrides,
3267            texture_combiner_combos,
3268            texture_transforms,
3269        };
3270
3271        // Now parse the actual data arrays using chunk-relative offsets
3272        // This is simplified for the basic implementation - we'll reuse the legacy parsing logic
3273        // but with the understanding that all offsets are now chunk-relative
3274
3275        // For now, we'll create a basic model structure and defer full array parsing
3276        // to maintain compatibility while implementing the chunked infrastructure
3277
3278        Ok(Self {
3279            header,
3280            name: None,                     // TODO: Parse name from chunk-relative offset
3281            global_sequences: Vec::new(),   // TODO: Parse from chunk
3282            animations: Vec::new(),         // TODO: Parse from chunk
3283            animation_lookup: Vec::new(),   // TODO: Parse from chunk
3284            bones: Vec::new(),              // TODO: Parse from chunk
3285            key_bone_lookup: Vec::new(),    // TODO: Parse from chunk
3286            vertices: Vec::new(),           // TODO: Parse from chunk
3287            textures: Vec::new(),           // TODO: Parse from chunk
3288            materials: Vec::new(),          // TODO: Parse from chunk
3289            particle_emitters: Vec::new(),  // TODO: Parse from chunk
3290            ribbon_emitters: Vec::new(),    // TODO: Parse from chunk
3291            texture_animations: Vec::new(), // TODO: Parse from chunk
3292            color_animations: Vec::new(),   // TODO: Parse from chunk
3293            transparency_animations: Vec::new(), // TODO: Parse from chunk
3294            events: Vec::new(),             // TODO: Parse from chunk
3295            attachments: Vec::new(),
3296            cameras: Vec::new(),
3297            lights: Vec::new(), // TODO: Parse from chunk
3298            raw_data: M2RawData::default(),
3299            skin_file_ids: None,              // Will be populated from SFID chunk
3300            animation_file_ids: None,         // Will be populated from AFID chunk
3301            texture_file_ids: None,           // Will be populated from TXID chunk
3302            physics_file_id: None,            // Will be populated from PFID chunk
3303            skeleton_file_id: None,           // Will be populated from SKID chunk
3304            bone_file_ids: None,              // Will be populated from BFID chunk
3305            lod_data: None,                   // Will be populated from LDV1 chunk
3306            extended_particle_data: None,     // Will be populated from EXPT/EXP2 chunks
3307            parent_animation_blacklist: None, // Will be populated from PABC chunk
3308            parent_animation_data: None,      // Will be populated from PADC chunk
3309            waterfall_effect: None,           // Will be populated from WFV1/2/3 chunks
3310            edge_fade_data: None,             // Will be populated from EDGF chunk
3311            model_alpha_data: None,           // Will be populated from NERF chunk
3312            lighting_details: None,           // Will be populated from DETL chunk
3313            recursive_particle_ids: None,     // Will be populated from RPID chunk
3314            geometry_particle_ids: None,      // Will be populated from GPID chunk
3315            texture_animation_chunk: None,    // Will be populated from TXAC chunk
3316            particle_geoset_data: None,       // Will be populated from PGD1 chunk
3317            dboc_chunk: None,                 // Will be populated from DBOC chunk
3318            afra_chunk: None,                 // Will be populated from AFRA chunk
3319            dpiv_chunk: None,                 // Will be populated from DPIV chunk
3320            parent_sequence_bounds: None,     // Will be populated from PSBC chunk
3321            parent_event_data: None,          // Will be populated from PEDC chunk
3322            collision_mesh_data: None,        // Will be populated from PCOL chunk
3323            physics_file_data: None,          // Will be populated from PFDC chunk
3324        })
3325    }
3326
3327    /// Parse an M2 model from a reader
3328    pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
3329        // Parse the header first
3330        let header = M2Header::parse(reader)?;
3331
3332        // Get the version
3333        let _version = header
3334            .version()
3335            .ok_or(M2Error::UnsupportedVersion(header.version.to_string()))?;
3336
3337        // Parse the name
3338        let name = if header.name.count > 0 {
3339            // Seek to the name
3340            reader.seek(SeekFrom::Start(header.name.offset as u64))?;
3341
3342            // Read the name (null-terminated string)
3343            let name_bytes = read_array(reader, &header.name, |r| Ok(r.read_u8()?))?;
3344
3345            // Convert to string, stopping at null terminator
3346            let name_end = name_bytes
3347                .iter()
3348                .position(|&b| b == 0)
3349                .unwrap_or(name_bytes.len());
3350            let name_str = String::from_utf8_lossy(&name_bytes[..name_end]).to_string();
3351            Some(name_str)
3352        } else {
3353            None
3354        };
3355
3356        // Parse global sequences
3357        let global_sequences =
3358            read_array(reader, &header.global_sequences, |r| Ok(r.read_u32_le()?))?;
3359
3360        // Parse animations
3361        let animations = read_array(reader, &header.animations.convert(), |r| {
3362            M2Animation::parse(r, header.version)
3363        })?;
3364
3365        // Parse animation lookups
3366        let animation_lookup =
3367            read_array(reader, &header.animation_lookup, |r| Ok(r.read_u16_le()?))?;
3368
3369        // Parse bones
3370        // Special handling for BC item files with 203 bones
3371        let bones = if header.version == 260 && header.bones.count == 203 {
3372            // Check if this might be an item file with bone indices instead of bone structures
3373            let current_pos = reader.stream_position()?;
3374            let file_size = reader.seek(SeekFrom::End(0))?;
3375            reader.seek(SeekFrom::Start(current_pos))?; // Restore position
3376
3377            let bone_size = 92; // BC bone size
3378            let expected_end = header.bones.offset as u64 + (header.bones.count as u64 * bone_size);
3379
3380            if expected_end > file_size {
3381                // File is too small to contain 203 bone structures
3382                // This is likely a BC item file where "bones" is actually a bone lookup table
3383
3384                // Skip the bone lookup table for now - we'll handle it differently
3385                Vec::new()
3386            } else {
3387                // File is large enough, parse normally
3388                read_array(reader, &header.bones.convert(), |r| {
3389                    M2Bone::parse(r, header.version)
3390                })?
3391            }
3392        } else {
3393            // Normal bone parsing for other versions
3394            read_array(reader, &header.bones.convert(), |r| {
3395                M2Bone::parse(r, header.version)
3396            })?
3397        };
3398
3399        // Parse key bone lookups
3400        let key_bone_lookup =
3401            read_array(reader, &header.key_bone_lookup, |r| Ok(r.read_u16_le()?))?;
3402
3403        // Parse vertices with bone index validation
3404        let bone_count = header.bones.count;
3405        let vertices = read_array(reader, &header.vertices.convert(), |r| {
3406            // CRITICAL FIX: Use validated parsing to prevent out-of-bounds bone references
3407            M2Vertex::parse_with_validation(
3408                r,
3409                header.version,
3410                Some(bone_count),
3411                crate::chunks::vertex::ValidationMode::default(),
3412            )
3413        })?;
3414
3415        // Parse textures
3416        let textures = read_array(reader, &header.textures.convert(), |r| {
3417            M2Texture::parse(r, header.version)
3418        })?;
3419
3420        // Parse materials (render flags)
3421        let materials = read_array(reader, &header.render_flags.convert(), |r| {
3422            M2Material::parse(r, header.version)
3423        })?;
3424
3425        // Parse particle emitters
3426        let particle_emitters = read_array(reader, &header.particle_emitters.convert(), |r| {
3427            M2ParticleEmitter::parse(r, header.version)
3428        })?;
3429
3430        // Parse ribbon emitters
3431        let ribbon_emitters = read_array(reader, &header.ribbon_emitters.convert(), |r| {
3432            M2RibbonEmitter::parse(r, header.version)
3433        })?;
3434
3435        // Parse texture animations
3436        let texture_animations = read_array(reader, &header.texture_animations.convert(), |r| {
3437            M2TextureAnimation::parse(r)
3438        })?;
3439
3440        // Parse color animations
3441        let color_animations = read_array(reader, &header.color_animations.convert(), |r| {
3442            M2ColorAnimation::parse(r)
3443        })?;
3444
3445        // Parse transparency animations (stored in header.transparency_lookup field,
3446        // which despite its name contains M2TransparencyAnimation structures, not lookup indices)
3447        let transparency_animations =
3448            read_array(reader, &header.transparency_lookup.convert(), |r| {
3449                M2TransparencyAnimation::parse(r)
3450            })?;
3451
3452        // Parse events (timeline triggers for sounds, effects, etc.)
3453        let events = read_array(reader, &header.events.convert(), |r| {
3454            M2Event::parse(r, header.version)
3455        })?;
3456
3457        // Parse attachments (attach points for weapons, effects, etc.)
3458        let attachments = read_array(reader, &header.attachments.convert(), |r| {
3459            M2Attachment::parse(r, header.version)
3460        })?;
3461
3462        // Parse cameras
3463        let cameras = read_array(reader, &header.cameras.convert(), |r| {
3464            M2Camera::parse(r, header.version)
3465        })?;
3466
3467        // Parse lights
3468        let lights = read_array(reader, &header.lights.convert(), |r| {
3469            M2Light::parse(r, header.version)
3470        })?;
3471
3472        // Collect raw animation keyframe data for bones before constructing raw_data
3473        let bone_animation_data = collect_bone_animation_data(reader, &bones, header.version)?;
3474
3475        // Collect raw animation keyframe data for particle emitters
3476        let particle_animation_data = collect_particle_animation_data(reader, &particle_emitters)?;
3477
3478        // Collect raw animation keyframe data for ribbon emitters
3479        let ribbon_animation_data = collect_ribbon_animation_data(reader, &ribbon_emitters)?;
3480
3481        // Collect raw animation keyframe data for texture animations
3482        let texture_animation_data = collect_texture_animation_data(reader, &texture_animations)?;
3483
3484        // Collect raw animation keyframe data for color animations
3485        let color_animation_data = collect_color_animation_data(reader, &color_animations)?;
3486
3487        // Collect raw animation keyframe data for transparency animations
3488        let transparency_animation_data =
3489            collect_transparency_animation_data(reader, &transparency_animations)?;
3490
3491        // Collect raw event track data
3492        let event_data = collect_event_data(reader, &events)?;
3493
3494        // Collect raw animation keyframe data for attachments
3495        let attachment_animation_data = collect_attachment_animation_data(reader, &attachments)?;
3496
3497        // Collect raw animation keyframe data for cameras
3498        let camera_animation_data = collect_camera_animation_data(reader, &cameras)?;
3499
3500        // Collect raw animation keyframe data for lights
3501        let light_animation_data = collect_light_animation_data(reader, &lights)?;
3502
3503        // Collect embedded skin data for pre-WotLK models (version <= 263)
3504        let embedded_skins = collect_embedded_skin_data(reader, &header)?;
3505
3506        // Parse raw data for other sections
3507        // These are sections we won't fully parse yet but want to preserve
3508        let raw_data = M2RawData {
3509            bone_animation_data,
3510            embedded_skins,
3511            particle_animation_data,
3512            ribbon_animation_data,
3513            texture_animation_data,
3514            color_animation_data,
3515            transparency_animation_data,
3516            event_data,
3517            attachment_animation_data,
3518            camera_animation_data,
3519            light_animation_data,
3520            transparency_lookup_table: read_array(
3521                reader,
3522                &header.transparency_lookup_table,
3523                |r| Ok(r.read_u16_le()?),
3524            )?,
3525            texture_animation_lookup: read_array(reader, &header.texture_animation_lookup, |r| {
3526                Ok(r.read_u16_le()?)
3527            })?,
3528            bone_lookup_table: read_array(reader, &header.bone_lookup_table, |r| {
3529                Ok(r.read_u16_le()?)
3530            })?,
3531            texture_lookup_table: read_array(reader, &header.texture_lookup_table, |r| {
3532                Ok(r.read_u16_le()?)
3533            })?,
3534            texture_units: read_array(reader, &header.texture_units, |r| Ok(r.read_u16_le()?))?,
3535            camera_lookup_table: read_array(reader, &header.camera_lookup_table, |r| {
3536                Ok(r.read_u16_le()?)
3537            })?,
3538            // Bounding data - read as raw bytes for preservation during conversion
3539            bounding_triangles: read_raw_bytes(reader, &header.bounding_triangles, 2)?, // u16 indices
3540            bounding_vertices: read_raw_bytes(reader, &header.bounding_vertices, 12)?,  // C3Vector
3541            bounding_normals: read_raw_bytes(reader, &header.bounding_normals, 12)?,    // C3Vector
3542            // Attachment lookup table
3543            attachment_lookup_table: read_array(reader, &header.attachment_lookup_table, |r| {
3544                Ok(r.read_u16_le()?)
3545            })?,
3546            ..Default::default()
3547        };
3548
3549        Ok(Self {
3550            header,
3551            name,
3552            global_sequences,
3553            animations,
3554            animation_lookup,
3555            bones,
3556            key_bone_lookup,
3557            vertices,
3558            textures,
3559            materials,
3560            particle_emitters,
3561            ribbon_emitters,
3562            texture_animations,
3563            color_animations,
3564            transparency_animations,
3565            events,
3566            attachments,
3567            cameras,
3568            lights,
3569            raw_data,
3570            skin_file_ids: None,
3571            animation_file_ids: None,
3572            texture_file_ids: None,
3573            physics_file_id: None,
3574            skeleton_file_id: None,
3575            bone_file_ids: None,
3576            lod_data: None,
3577            extended_particle_data: None,
3578            parent_animation_blacklist: None,
3579            parent_animation_data: None,
3580            waterfall_effect: None,
3581            edge_fade_data: None,
3582            model_alpha_data: None,
3583            lighting_details: None,
3584            recursive_particle_ids: None,
3585            geometry_particle_ids: None,
3586            texture_animation_chunk: None,
3587            particle_geoset_data: None,
3588            dboc_chunk: None,
3589            afra_chunk: None,
3590            dpiv_chunk: None,
3591            parent_sequence_bounds: None,
3592            parent_event_data: None,
3593            collision_mesh_data: None,
3594            physics_file_data: None,
3595        })
3596    }
3597
3598    /// Load an M2 model from a file with format detection
3599    pub fn load<P: AsRef<Path>>(path: P) -> Result<M2Format> {
3600        let mut file = File::open(path)?;
3601        parse_m2(&mut file)
3602    }
3603
3604    /// Load a legacy M2 model from a file
3605    pub fn load_legacy<P: AsRef<Path>>(path: P) -> Result<Self> {
3606        let mut file = File::open(path)?;
3607        Self::parse_legacy(&mut file)
3608    }
3609
3610    /// Save an M2 model to a file
3611    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
3612        let mut file = File::create(path)?;
3613        self.write(&mut file)
3614    }
3615
3616    /// Write an M2 model to a writer
3617    pub fn write<W: Write + Seek>(&self, writer: &mut W) -> Result<()> {
3618        // We need to recalculate all offsets and build the file in memory
3619        let mut data_section = Vec::new();
3620        let mut header = self.header.clone();
3621
3622        // Start with header size (will be written last)
3623        let header_size = self.calculate_header_size();
3624        let mut current_offset = header_size as u32;
3625
3626        // Write name
3627        if let Some(ref name) = self.name {
3628            let name_bytes = name.as_bytes();
3629            let name_len = name_bytes.len() as u32 + 1; // +1 for null terminator
3630            header.name = M2Array::new(name_len, current_offset);
3631
3632            data_section.extend_from_slice(name_bytes);
3633            data_section.push(0); // Null terminator
3634            current_offset += name_len;
3635        } else {
3636            header.name = M2Array::new(0, 0);
3637        }
3638
3639        // Write global sequences
3640        if !self.global_sequences.is_empty() {
3641            header.global_sequences =
3642                M2Array::new(self.global_sequences.len() as u32, current_offset);
3643
3644            for &seq in &self.global_sequences {
3645                data_section.extend_from_slice(&seq.to_le_bytes());
3646            }
3647
3648            current_offset += (self.global_sequences.len() * std::mem::size_of::<u32>()) as u32;
3649        } else {
3650            header.global_sequences = M2Array::new(0, 0);
3651        }
3652
3653        // Write animations
3654        if !self.animations.is_empty() {
3655            header.animations = M2Array::new(self.animations.len() as u32, current_offset);
3656
3657            for anim in &self.animations {
3658                // For each animation, write its data
3659                let mut anim_data = Vec::new();
3660                anim.write(&mut anim_data, header.version)?;
3661                data_section.extend_from_slice(&anim_data);
3662            }
3663
3664            // Animation size depends on version: 32 bytes for Classic, 52 bytes for BC+
3665            let anim_size = if header.version <= 256 { 32 } else { 52 };
3666            current_offset += (self.animations.len() * anim_size) as u32;
3667        } else {
3668            header.animations = M2Array::new(0, 0);
3669        }
3670
3671        // Write animation lookups
3672        if !self.animation_lookup.is_empty() {
3673            header.animation_lookup =
3674                M2Array::new(self.animation_lookup.len() as u32, current_offset);
3675
3676            for &lookup in &self.animation_lookup {
3677                data_section.extend_from_slice(&lookup.to_le_bytes());
3678            }
3679
3680            current_offset += (self.animation_lookup.len() * std::mem::size_of::<u16>()) as u32;
3681        } else {
3682            header.animation_lookup = M2Array::new(0, 0);
3683        }
3684
3685        // Write bones with animation data preservation
3686        // If we have collected bone animation data, write it with relocated offsets.
3687        // Otherwise, write static bones (zeroed animation tracks).
3688        if !self.bones.is_empty() {
3689            header.bones = M2Array::new(self.bones.len() as u32, current_offset);
3690
3691            // Calculate bone structure size based on version
3692            let bone_size = if header.version < 260 {
3693                108 // Vanilla: no boneNameCRC, 28-byte M2Tracks
3694            } else if header.version < 264 {
3695                112 // TBC: boneNameCRC, 28-byte M2Tracks
3696            } else {
3697                88 // WotLK+: boneNameCRC, 20-byte M2Tracks (no ranges)
3698            };
3699
3700            // Calculate where animation data will be written (after all bone structures)
3701            let bones_total_size = self.bones.len() * bone_size;
3702            let anim_data_start = current_offset + bones_total_size as u32;
3703
3704            // Check if we have animation data to preserve
3705            if !self.raw_data.bone_animation_data.is_empty() {
3706                // Build offset relocation map: old_offset -> new_offset
3707                // Multiple bones can share animation data (same original offset).
3708                // We only write shared data once and reuse the same new offset.
3709                let mut offset_map: HashMap<u32, u32> = HashMap::new();
3710                let mut anim_data_offset = anim_data_start;
3711
3712                use std::collections::hash_map::Entry;
3713
3714                for anim in &self.raw_data.bone_animation_data {
3715                    // Map timestamps offset (skip if already mapped - shared data)
3716                    if !anim.timestamps.is_empty()
3717                        && let Entry::Vacant(e) = offset_map.entry(anim.original_timestamps_offset)
3718                    {
3719                        e.insert(anim_data_offset);
3720                        anim_data_offset += anim.timestamps.len() as u32;
3721                    }
3722
3723                    // Map values offset (skip if already mapped - shared data)
3724                    if !anim.values.is_empty()
3725                        && let Entry::Vacant(e) = offset_map.entry(anim.original_values_offset)
3726                    {
3727                        e.insert(anim_data_offset);
3728                        anim_data_offset += anim.values.len() as u32;
3729                    }
3730
3731                    // Map ranges offset (pre-WotLK only, skip if already mapped)
3732                    if let (Some(ranges), Some(orig_offset)) =
3733                        (&anim.ranges, anim.original_ranges_offset)
3734                        && let Entry::Vacant(e) = offset_map.entry(orig_offset)
3735                    {
3736                        e.insert(anim_data_offset);
3737                        anim_data_offset += ranges.len() as u32;
3738                    }
3739                }
3740
3741                // Write bones with relocated offsets
3742                for bone in &self.bones {
3743                    let mut relocated_bone = bone.clone();
3744                    relocate_bone_track_offsets(&mut relocated_bone, &offset_map);
3745
3746                    let mut bone_data = Vec::new();
3747                    relocated_bone.write(&mut bone_data, header.version)?;
3748                    data_section.extend_from_slice(&bone_data);
3749                }
3750
3751                // Write animation keyframe data (only write each unique offset once)
3752                let mut written_offsets: std::collections::HashSet<u32> =
3753                    std::collections::HashSet::new();
3754
3755                for anim in &self.raw_data.bone_animation_data {
3756                    // Write timestamps only if not already written
3757                    if !anim.timestamps.is_empty()
3758                        && written_offsets.insert(anim.original_timestamps_offset)
3759                    {
3760                        data_section.extend_from_slice(&anim.timestamps);
3761                    }
3762
3763                    // Write values only if not already written
3764                    if !anim.values.is_empty()
3765                        && written_offsets.insert(anim.original_values_offset)
3766                    {
3767                        data_section.extend_from_slice(&anim.values);
3768                    }
3769
3770                    // Write ranges only if not already written
3771                    if let (Some(ranges), Some(orig_offset)) =
3772                        (&anim.ranges, anim.original_ranges_offset)
3773                        && written_offsets.insert(orig_offset)
3774                    {
3775                        data_section.extend_from_slice(ranges);
3776                    }
3777                }
3778
3779                current_offset = anim_data_offset;
3780            } else {
3781                // No animation data collected - write static bones (zeroed tracks)
3782                for bone in &self.bones {
3783                    let mut static_bone = bone.clone();
3784                    static_bone.translation = M2TrackVec3::default();
3785                    static_bone.rotation = M2TrackQuat::default();
3786                    static_bone.scale = M2TrackVec3::default();
3787
3788                    let mut bone_data = Vec::new();
3789                    static_bone.write(&mut bone_data, header.version)?;
3790                    data_section.extend_from_slice(&bone_data);
3791                }
3792
3793                current_offset += bones_total_size as u32;
3794            }
3795        } else {
3796            header.bones = M2Array::new(0, 0);
3797        }
3798
3799        // Write key bone lookups
3800        if !self.key_bone_lookup.is_empty() {
3801            header.key_bone_lookup =
3802                M2Array::new(self.key_bone_lookup.len() as u32, current_offset);
3803
3804            for &lookup in &self.key_bone_lookup {
3805                data_section.extend_from_slice(&lookup.to_le_bytes());
3806            }
3807
3808            current_offset += (self.key_bone_lookup.len() * std::mem::size_of::<u16>()) as u32;
3809        } else {
3810            header.key_bone_lookup = M2Array::new(0, 0);
3811        }
3812
3813        // Write vertices
3814        if !self.vertices.is_empty() {
3815            header.vertices = M2Array::new(self.vertices.len() as u32, current_offset);
3816
3817            // Vertex size is always 48 bytes for all versions:
3818            // position (12) + bone_weights (4) + bone_indices (4) + normal (12) + tex_coords (8) + tex_coords2 (8)
3819            // Note: Secondary texture coordinates exist in ALL M2 versions (verified against vanilla files).
3820            let vertex_size = 48;
3821
3822            for vertex in &self.vertices {
3823                let mut vertex_data = Vec::new();
3824                vertex.write(&mut vertex_data, header.version)?;
3825                data_section.extend_from_slice(&vertex_data);
3826            }
3827
3828            current_offset += (self.vertices.len() * vertex_size) as u32;
3829        } else {
3830            header.vertices = M2Array::new(0, 0);
3831        }
3832
3833        // Write textures
3834        if !self.textures.is_empty() {
3835            header.textures = M2Array::new(self.textures.len() as u32, current_offset);
3836
3837            // First, we need to write the texture definitions
3838            let mut texture_name_offsets = Vec::new();
3839            let texture_def_size = 16; // Each texture definition is 16 bytes
3840
3841            for texture in &self.textures {
3842                // Save the current offset for this texture's filename
3843                texture_name_offsets
3844                    .push(current_offset + (self.textures.len() * texture_def_size) as u32);
3845
3846                // Write the texture definition (without the actual filename)
3847                let mut texture_def = Vec::new();
3848
3849                // Write texture type
3850                texture_def.extend_from_slice(&(texture.texture_type as u32).to_le_bytes());
3851
3852                // Write flags
3853                texture_def.extend_from_slice(&texture.flags.bits().to_le_bytes());
3854
3855                // Write filename offset and length (will be filled in later)
3856                texture_def.extend_from_slice(&0u32.to_le_bytes()); // Count
3857                texture_def.extend_from_slice(&0u32.to_le_bytes()); // Offset
3858
3859                data_section.extend_from_slice(&texture_def);
3860            }
3861
3862            // Now write the filenames
3863            current_offset += (self.textures.len() * texture_def_size) as u32;
3864
3865            // For each texture, update the offset in the definition and write the filename
3866            for (i, texture) in self.textures.iter().enumerate() {
3867                // Get the filename
3868                let filename_offset = texture.filename.array.offset as usize;
3869                let filename_len = texture.filename.array.count as usize;
3870                // Not every texture has a filename (some are hardcoded)
3871                if filename_offset == 0 || filename_len == 0 {
3872                    continue;
3873                }
3874
3875                // Calculate the offset in the data section where this texture's definition was written
3876                // The texture definitions start at (header.textures.offset - base_data_offset)
3877                let base_data_offset = std::mem::size_of::<M2Header>();
3878                let def_offset_in_data = (header.textures.offset as usize - base_data_offset)
3879                    + (i * texture_def_size)
3880                    + 8;
3881
3882                // Update the count and offset for the filename
3883                data_section[def_offset_in_data..def_offset_in_data + 4]
3884                    .copy_from_slice(&(filename_len as u32).to_le_bytes());
3885                data_section[def_offset_in_data + 4..def_offset_in_data + 8]
3886                    .copy_from_slice(&current_offset.to_le_bytes());
3887
3888                // Write the filename
3889                data_section.extend_from_slice(&texture.filename.string.data);
3890                data_section.push(0); // Null terminator
3891
3892                current_offset += filename_len as u32;
3893            }
3894        } else {
3895            header.textures = M2Array::new(0, 0);
3896        }
3897
3898        // Write materials (render flags)
3899        if !self.materials.is_empty() {
3900            header.render_flags = M2Array::new(self.materials.len() as u32, current_offset);
3901
3902            for material in &self.materials {
3903                let mut material_data = Vec::new();
3904                material.write(&mut material_data, header.version)?;
3905                data_section.extend_from_slice(&material_data);
3906            }
3907
3908            // Material is always 4 bytes: flags (u16) + blending_mode (u16)
3909            current_offset += (self.materials.len() * 4) as u32;
3910        } else {
3911            header.render_flags = M2Array::new(0, 0);
3912        }
3913
3914        // Write bone lookup table
3915        if !self.raw_data.bone_lookup_table.is_empty() {
3916            header.bone_lookup_table =
3917                M2Array::new(self.raw_data.bone_lookup_table.len() as u32, current_offset);
3918
3919            for &lookup in &self.raw_data.bone_lookup_table {
3920                data_section.extend_from_slice(&lookup.to_le_bytes());
3921            }
3922
3923            current_offset +=
3924                (self.raw_data.bone_lookup_table.len() * std::mem::size_of::<u16>()) as u32;
3925        } else {
3926            header.bone_lookup_table = M2Array::new(0, 0);
3927        }
3928
3929        // Write texture lookup table
3930        if !self.raw_data.texture_lookup_table.is_empty() {
3931            header.texture_lookup_table = M2Array::new(
3932                self.raw_data.texture_lookup_table.len() as u32,
3933                current_offset,
3934            );
3935
3936            for &lookup in &self.raw_data.texture_lookup_table {
3937                data_section.extend_from_slice(&lookup.to_le_bytes());
3938            }
3939
3940            current_offset +=
3941                (self.raw_data.texture_lookup_table.len() * std::mem::size_of::<u16>()) as u32;
3942        } else {
3943            header.texture_lookup_table = M2Array::new(0, 0);
3944        }
3945
3946        // Write texture units
3947        if !self.raw_data.texture_units.is_empty() {
3948            header.texture_units =
3949                M2Array::new(self.raw_data.texture_units.len() as u32, current_offset);
3950
3951            for &unit in &self.raw_data.texture_units {
3952                data_section.extend_from_slice(&unit.to_le_bytes());
3953            }
3954
3955            current_offset +=
3956                (self.raw_data.texture_units.len() * std::mem::size_of::<u16>()) as u32;
3957        } else {
3958            header.texture_units = M2Array::new(0, 0);
3959        }
3960
3961        // Write transparency lookup table
3962        if !self.raw_data.transparency_lookup_table.is_empty() {
3963            header.transparency_lookup_table = M2Array::new(
3964                self.raw_data.transparency_lookup_table.len() as u32,
3965                current_offset,
3966            );
3967
3968            for &lookup in &self.raw_data.transparency_lookup_table {
3969                data_section.extend_from_slice(&lookup.to_le_bytes());
3970            }
3971
3972            current_offset +=
3973                (self.raw_data.transparency_lookup_table.len() * std::mem::size_of::<u16>()) as u32;
3974        } else {
3975            header.transparency_lookup_table = M2Array::new(0, 0);
3976        }
3977
3978        // Write texture animation lookup
3979        if !self.raw_data.texture_animation_lookup.is_empty() {
3980            header.texture_animation_lookup = M2Array::new(
3981                self.raw_data.texture_animation_lookup.len() as u32,
3982                current_offset,
3983            );
3984
3985            for &lookup in &self.raw_data.texture_animation_lookup {
3986                data_section.extend_from_slice(&lookup.to_le_bytes());
3987            }
3988
3989            current_offset +=
3990                (self.raw_data.texture_animation_lookup.len() * std::mem::size_of::<u16>()) as u32;
3991        } else {
3992            header.texture_animation_lookup = M2Array::new(0, 0);
3993        }
3994
3995        // Write bounding triangles
3996        if !self.raw_data.bounding_triangles.is_empty() {
3997            // bounding_triangles count is number of u16 values (3 per triangle)
3998            let count = self.raw_data.bounding_triangles.len() / 2;
3999            header.bounding_triangles = M2Array::new(count as u32, current_offset);
4000            data_section.extend_from_slice(&self.raw_data.bounding_triangles);
4001            current_offset += self.raw_data.bounding_triangles.len() as u32;
4002        } else {
4003            header.bounding_triangles = M2Array::new(0, 0);
4004        }
4005
4006        // Write bounding vertices
4007        if !self.raw_data.bounding_vertices.is_empty() {
4008            // bounding_vertices are C3Vector (12 bytes each)
4009            let count = self.raw_data.bounding_vertices.len() / 12;
4010            header.bounding_vertices = M2Array::new(count as u32, current_offset);
4011            data_section.extend_from_slice(&self.raw_data.bounding_vertices);
4012            current_offset += self.raw_data.bounding_vertices.len() as u32;
4013        } else {
4014            header.bounding_vertices = M2Array::new(0, 0);
4015        }
4016
4017        // Write bounding normals
4018        if !self.raw_data.bounding_normals.is_empty() {
4019            // bounding_normals are C3Vector (12 bytes each)
4020            let count = self.raw_data.bounding_normals.len() / 12;
4021            header.bounding_normals = M2Array::new(count as u32, current_offset);
4022            data_section.extend_from_slice(&self.raw_data.bounding_normals);
4023            current_offset += self.raw_data.bounding_normals.len() as u32;
4024        } else {
4025            header.bounding_normals = M2Array::new(0, 0);
4026        }
4027
4028        // Write attachment lookup table
4029        if !self.raw_data.attachment_lookup_table.is_empty() {
4030            header.attachment_lookup_table = M2Array::new(
4031                self.raw_data.attachment_lookup_table.len() as u32,
4032                current_offset,
4033            );
4034            for &lookup in &self.raw_data.attachment_lookup_table {
4035                data_section.extend_from_slice(&lookup.to_le_bytes());
4036            }
4037            current_offset +=
4038                (self.raw_data.attachment_lookup_table.len() * std::mem::size_of::<u16>()) as u32;
4039        } else {
4040            header.attachment_lookup_table = M2Array::new(0, 0);
4041        }
4042
4043        // Write camera lookup table
4044        if !self.raw_data.camera_lookup_table.is_empty() {
4045            header.camera_lookup_table = M2Array::new(
4046                self.raw_data.camera_lookup_table.len() as u32,
4047                current_offset,
4048            );
4049            for &lookup in &self.raw_data.camera_lookup_table {
4050                data_section.extend_from_slice(&lookup.to_le_bytes());
4051            }
4052            current_offset +=
4053                (self.raw_data.camera_lookup_table.len() * std::mem::size_of::<u16>()) as u32;
4054        } else {
4055            header.camera_lookup_table = M2Array::new(0, 0);
4056        }
4057
4058        // Write embedded skin data for pre-WotLK versions (version <= 263)
4059        if header.version <= 263 && !self.raw_data.embedded_skins.is_empty() {
4060            // Calculate total size needed for all embedded skin data:
4061            // 1. ModelView structures (44 bytes each)
4062            // 2. All referenced data arrays (indices, triangles, properties, submeshes, batches)
4063
4064            const MODEL_VIEW_SIZE: u32 = 44;
4065            let model_view_count = self.raw_data.embedded_skins.len() as u32;
4066
4067            // Header.views points to the ModelView structures
4068            let model_views_offset = current_offset;
4069            header.views = M2Array::new(model_view_count, model_views_offset);
4070
4071            // Phase 1: Calculate all new offsets for all skins before writing anything
4072            let mut data_offset = model_views_offset + (model_view_count * MODEL_VIEW_SIZE);
4073            let submesh_size = if header.version < 260 { 32 } else { 48 };
4074
4075            // Store calculated offsets for each skin
4076            struct SkinOffsets {
4077                indices_offset: u32,
4078                triangles_offset: u32,
4079                properties_offset: u32,
4080                submeshes_offset: u32,
4081                batches_offset: u32,
4082            }
4083
4084            let mut all_offsets = Vec::with_capacity(self.raw_data.embedded_skins.len());
4085
4086            for skin in &self.raw_data.embedded_skins {
4087                let indices_offset = if skin.indices.is_empty() {
4088                    0
4089                } else {
4090                    let offset = data_offset;
4091                    data_offset += skin.indices.len() as u32;
4092                    offset
4093                };
4094
4095                let triangles_offset = if skin.triangles.is_empty() {
4096                    0
4097                } else {
4098                    let offset = data_offset;
4099                    data_offset += skin.triangles.len() as u32;
4100                    offset
4101                };
4102
4103                let properties_offset = if skin.properties.is_empty() {
4104                    0
4105                } else {
4106                    let offset = data_offset;
4107                    data_offset += skin.properties.len() as u32;
4108                    offset
4109                };
4110
4111                let submeshes_offset = if skin.submeshes.is_empty() {
4112                    0
4113                } else {
4114                    let offset = data_offset;
4115                    data_offset += skin.submeshes.len() as u32;
4116                    offset
4117                };
4118
4119                let batches_offset = if skin.batches.is_empty() {
4120                    0
4121                } else {
4122                    let offset = data_offset;
4123                    data_offset += skin.batches.len() as u32;
4124                    offset
4125                };
4126
4127                all_offsets.push(SkinOffsets {
4128                    indices_offset,
4129                    triangles_offset,
4130                    properties_offset,
4131                    submeshes_offset,
4132                    batches_offset,
4133                });
4134            }
4135
4136            // Phase 2: Write all ModelView structures with calculated offsets
4137            for (skin, offsets) in self.raw_data.embedded_skins.iter().zip(all_offsets.iter()) {
4138                // Calculate counts from data sizes
4139                let n_indices = (skin.indices.len() / 2) as u32;
4140                let n_triangles = (skin.triangles.len() / 2) as u32;
4141                let n_properties = (skin.properties.len() / 4) as u32;
4142                let n_submeshes = if skin.submeshes.is_empty() {
4143                    0
4144                } else {
4145                    (skin.submeshes.len() / submesh_size) as u32
4146                };
4147                let n_batches = (skin.batches.len() / 96) as u32;
4148
4149                // Extract bone_count_max from original ModelView (last 4 bytes)
4150                let bone_count_max = if skin.model_view.len() >= 44 {
4151                    u32::from_le_bytes([
4152                        skin.model_view[40],
4153                        skin.model_view[41],
4154                        skin.model_view[42],
4155                        skin.model_view[43],
4156                    ])
4157                } else {
4158                    0
4159                };
4160
4161                // Write ModelView structure (44 bytes: 5 M2Arrays + bone_count_max)
4162                data_section.extend_from_slice(&n_indices.to_le_bytes());
4163                data_section.extend_from_slice(&offsets.indices_offset.to_le_bytes());
4164                data_section.extend_from_slice(&n_triangles.to_le_bytes());
4165                data_section.extend_from_slice(&offsets.triangles_offset.to_le_bytes());
4166                data_section.extend_from_slice(&n_properties.to_le_bytes());
4167                data_section.extend_from_slice(&offsets.properties_offset.to_le_bytes());
4168                data_section.extend_from_slice(&n_submeshes.to_le_bytes());
4169                data_section.extend_from_slice(&offsets.submeshes_offset.to_le_bytes());
4170                data_section.extend_from_slice(&n_batches.to_le_bytes());
4171                data_section.extend_from_slice(&offsets.batches_offset.to_le_bytes());
4172                data_section.extend_from_slice(&bone_count_max.to_le_bytes());
4173            }
4174
4175            // Phase 3: Write all data arrays in the same order as offsets were calculated
4176            for skin in &self.raw_data.embedded_skins {
4177                if !skin.indices.is_empty() {
4178                    data_section.extend_from_slice(&skin.indices);
4179                }
4180                if !skin.triangles.is_empty() {
4181                    data_section.extend_from_slice(&skin.triangles);
4182                }
4183                if !skin.properties.is_empty() {
4184                    data_section.extend_from_slice(&skin.properties);
4185                }
4186                if !skin.submeshes.is_empty() {
4187                    data_section.extend_from_slice(&skin.submeshes);
4188                }
4189                if !skin.batches.is_empty() {
4190                    data_section.extend_from_slice(&skin.batches);
4191                }
4192            }
4193
4194            current_offset = data_offset;
4195        }
4196
4197        // Write particle emitters with animation data preservation
4198        // Similar pattern to bones - write structures with relocated offsets, then animation data
4199        if !self.particle_emitters.is_empty() {
4200            header.particle_emitters =
4201                M2Array::new(self.particle_emitters.len() as u32, current_offset);
4202
4203            // First, write all emitter structures to a temporary buffer to calculate their total size
4204            let mut temp_emitter_data = Vec::new();
4205            for emitter in &self.particle_emitters {
4206                let mut emitter_data = Vec::new();
4207                emitter.write(&mut emitter_data, header.version)?;
4208                temp_emitter_data.push(emitter_data);
4209            }
4210            let emitters_total_size: usize = temp_emitter_data.iter().map(|v| v.len()).sum();
4211
4212            // Calculate where animation data will be written (after all emitter structures)
4213            let anim_data_start = current_offset + emitters_total_size as u32;
4214
4215            // Check if we have animation data to preserve
4216            if !self.raw_data.particle_animation_data.is_empty() {
4217                // Build offset relocation map: old_offset -> new_offset
4218                let mut offset_map: HashMap<u32, u32> = HashMap::new();
4219                let mut anim_data_offset = anim_data_start;
4220
4221                use std::collections::hash_map::Entry;
4222
4223                for anim in &self.raw_data.particle_animation_data {
4224                    // Map interpolation_ranges offset (skip if already mapped - shared data)
4225                    if !anim.interpolation_ranges.is_empty()
4226                        && let Entry::Vacant(e) = offset_map.entry(anim.original_ranges_offset)
4227                    {
4228                        e.insert(anim_data_offset);
4229                        anim_data_offset += anim.interpolation_ranges.len() as u32;
4230                    }
4231
4232                    // Map timestamps offset (skip if already mapped - shared data)
4233                    if !anim.timestamps.is_empty()
4234                        && let Entry::Vacant(e) = offset_map.entry(anim.original_timestamps_offset)
4235                    {
4236                        e.insert(anim_data_offset);
4237                        anim_data_offset += anim.timestamps.len() as u32;
4238                    }
4239
4240                    // Map values offset (skip if already mapped - shared data)
4241                    if !anim.values.is_empty()
4242                        && let Entry::Vacant(e) = offset_map.entry(anim.original_values_offset)
4243                    {
4244                        e.insert(anim_data_offset);
4245                        anim_data_offset += anim.values.len() as u32;
4246                    }
4247                }
4248
4249                // Write emitters with relocated offsets
4250                for emitter in &self.particle_emitters {
4251                    let mut relocated_emitter = emitter.clone();
4252                    relocate_particle_animation_offsets(&mut relocated_emitter, &offset_map);
4253
4254                    let mut emitter_data = Vec::new();
4255                    relocated_emitter.write(&mut emitter_data, header.version)?;
4256                    data_section.extend_from_slice(&emitter_data);
4257                }
4258
4259                // Write animation keyframe data (only write each unique offset once)
4260                let mut written_offsets: std::collections::HashSet<u32> =
4261                    std::collections::HashSet::new();
4262
4263                for anim in &self.raw_data.particle_animation_data {
4264                    // Write interpolation_ranges only if not already written
4265                    if !anim.interpolation_ranges.is_empty()
4266                        && written_offsets.insert(anim.original_ranges_offset)
4267                    {
4268                        data_section.extend_from_slice(&anim.interpolation_ranges);
4269                    }
4270
4271                    // Write timestamps only if not already written
4272                    if !anim.timestamps.is_empty()
4273                        && written_offsets.insert(anim.original_timestamps_offset)
4274                    {
4275                        data_section.extend_from_slice(&anim.timestamps);
4276                    }
4277
4278                    // Write values only if not already written
4279                    if !anim.values.is_empty()
4280                        && written_offsets.insert(anim.original_values_offset)
4281                    {
4282                        data_section.extend_from_slice(&anim.values);
4283                    }
4284                }
4285
4286                current_offset = anim_data_offset;
4287            } else {
4288                // No animation data collected - write emitters with zeroed animation tracks
4289                // This happens when we're creating new emitters or the source had no animations
4290                for emitter in &self.particle_emitters {
4291                    let mut static_emitter = emitter.clone();
4292                    // Zero out all animation blocks by setting them to default
4293                    static_emitter.emission_speed_animation = M2AnimationBlock::default();
4294                    static_emitter.emission_rate_animation = M2AnimationBlock::default();
4295                    static_emitter.emission_area_animation = M2AnimationBlock::default();
4296                    static_emitter.xy_scale_animation = M2AnimationBlock::default();
4297                    static_emitter.z_scale_animation = M2AnimationBlock::default();
4298                    static_emitter.color_animation = M2AnimationBlock::default();
4299                    static_emitter.transparency_animation = M2AnimationBlock::default();
4300                    static_emitter.size_animation = M2AnimationBlock::default();
4301                    static_emitter.intensity_animation = M2AnimationBlock::default();
4302                    static_emitter.z_source_animation = M2AnimationBlock::default();
4303
4304                    let mut emitter_data = Vec::new();
4305                    static_emitter.write(&mut emitter_data, header.version)?;
4306                    data_section.extend_from_slice(&emitter_data);
4307                }
4308
4309                current_offset += emitters_total_size as u32;
4310            }
4311        } else {
4312            header.particle_emitters = M2Array::new(0, 0);
4313        }
4314
4315        // Write ribbon emitters with animation data preservation
4316        // Similar pattern to particle emitters - write structures with relocated offsets, then animation data
4317        if !self.ribbon_emitters.is_empty() {
4318            header.ribbon_emitters =
4319                M2Array::new(self.ribbon_emitters.len() as u32, current_offset);
4320
4321            // First, write all emitter structures to a temporary buffer to calculate their total size
4322            let mut temp_emitter_data = Vec::new();
4323            for emitter in &self.ribbon_emitters {
4324                let mut emitter_data = Vec::new();
4325                emitter.write(&mut emitter_data, header.version)?;
4326                temp_emitter_data.push(emitter_data);
4327            }
4328            let emitters_total_size: usize = temp_emitter_data.iter().map(|v| v.len()).sum();
4329
4330            // Calculate where animation data will be written (after all emitter structures)
4331            let anim_data_start = current_offset + emitters_total_size as u32;
4332
4333            // Check if we have animation data to preserve
4334            if !self.raw_data.ribbon_animation_data.is_empty() {
4335                // Build offset relocation map: old_offset -> new_offset
4336                let mut offset_map: HashMap<u32, u32> = HashMap::new();
4337                let mut anim_data_offset = anim_data_start;
4338
4339                use std::collections::hash_map::Entry;
4340
4341                for anim in &self.raw_data.ribbon_animation_data {
4342                    // Map interpolation_ranges offset (skip if already mapped - shared data)
4343                    if !anim.interpolation_ranges.is_empty()
4344                        && let Entry::Vacant(e) = offset_map.entry(anim.original_ranges_offset)
4345                    {
4346                        e.insert(anim_data_offset);
4347                        anim_data_offset += anim.interpolation_ranges.len() as u32;
4348                    }
4349
4350                    // Map timestamps offset (skip if already mapped - shared data)
4351                    if !anim.timestamps.is_empty()
4352                        && let Entry::Vacant(e) = offset_map.entry(anim.original_timestamps_offset)
4353                    {
4354                        e.insert(anim_data_offset);
4355                        anim_data_offset += anim.timestamps.len() as u32;
4356                    }
4357
4358                    // Map values offset (skip if already mapped - shared data)
4359                    if !anim.values.is_empty()
4360                        && let Entry::Vacant(e) = offset_map.entry(anim.original_values_offset)
4361                    {
4362                        e.insert(anim_data_offset);
4363                        anim_data_offset += anim.values.len() as u32;
4364                    }
4365                }
4366
4367                // Write emitters with relocated offsets
4368                for emitter in &self.ribbon_emitters {
4369                    let mut relocated_emitter = emitter.clone();
4370                    relocate_ribbon_animation_offsets(&mut relocated_emitter, &offset_map);
4371
4372                    let mut emitter_data = Vec::new();
4373                    relocated_emitter.write(&mut emitter_data, header.version)?;
4374                    data_section.extend_from_slice(&emitter_data);
4375                }
4376
4377                // Write animation keyframe data (only write each unique offset once)
4378                let mut written_offsets: std::collections::HashSet<u32> =
4379                    std::collections::HashSet::new();
4380
4381                for anim in &self.raw_data.ribbon_animation_data {
4382                    // Write interpolation_ranges only if not already written
4383                    if !anim.interpolation_ranges.is_empty()
4384                        && written_offsets.insert(anim.original_ranges_offset)
4385                    {
4386                        data_section.extend_from_slice(&anim.interpolation_ranges);
4387                    }
4388
4389                    // Write timestamps only if not already written
4390                    if !anim.timestamps.is_empty()
4391                        && written_offsets.insert(anim.original_timestamps_offset)
4392                    {
4393                        data_section.extend_from_slice(&anim.timestamps);
4394                    }
4395
4396                    // Write values only if not already written
4397                    if !anim.values.is_empty()
4398                        && written_offsets.insert(anim.original_values_offset)
4399                    {
4400                        data_section.extend_from_slice(&anim.values);
4401                    }
4402                }
4403
4404                current_offset = anim_data_offset;
4405            } else {
4406                // No animation data collected - write emitters with zeroed animation tracks
4407                // This happens when we're creating new emitters or the source had no animations
4408                for emitter in &self.ribbon_emitters {
4409                    let mut static_emitter = emitter.clone();
4410                    // Zero out all animation blocks by setting them to default
4411                    static_emitter.color_animation = M2AnimationBlock::default();
4412                    static_emitter.alpha_animation = M2AnimationBlock::default();
4413                    static_emitter.height_above_animation = M2AnimationBlock::default();
4414                    static_emitter.height_below_animation = M2AnimationBlock::default();
4415
4416                    let mut emitter_data = Vec::new();
4417                    static_emitter.write(&mut emitter_data, header.version)?;
4418                    data_section.extend_from_slice(&emitter_data);
4419                }
4420
4421                current_offset += emitters_total_size as u32;
4422            }
4423        } else {
4424            header.ribbon_emitters = M2Array::new(0, 0);
4425        }
4426
4427        // Write texture animations with animation data preservation
4428        // Similar pattern to particle/ribbon emitters - write structures with relocated offsets, then animation data
4429        if !self.texture_animations.is_empty() {
4430            header.texture_animations =
4431                M2Array::new(self.texture_animations.len() as u32, current_offset);
4432
4433            // First, write all animation structures to a temporary buffer to calculate their total size
4434            let mut temp_anim_data = Vec::new();
4435            for anim in &self.texture_animations {
4436                let mut anim_data = Vec::new();
4437                anim.write(&mut anim_data)?;
4438                temp_anim_data.push(anim_data);
4439            }
4440            let anims_total_size: usize = temp_anim_data.iter().map(|v| v.len()).sum();
4441
4442            // Calculate where animation data will be written (after all animation structures)
4443            let anim_data_start = current_offset + anims_total_size as u32;
4444
4445            // Check if we have animation data to preserve
4446            if !self.raw_data.texture_animation_data.is_empty() {
4447                // Build offset relocation map: old_offset -> new_offset
4448                let mut offset_map: HashMap<u32, u32> = HashMap::new();
4449                let mut anim_data_offset = anim_data_start;
4450
4451                use std::collections::hash_map::Entry;
4452
4453                for anim in &self.raw_data.texture_animation_data {
4454                    // Map interpolation_ranges offset (skip if already mapped - shared data)
4455                    if !anim.interpolation_ranges.is_empty()
4456                        && let Entry::Vacant(e) = offset_map.entry(anim.original_ranges_offset)
4457                    {
4458                        e.insert(anim_data_offset);
4459                        anim_data_offset += anim.interpolation_ranges.len() as u32;
4460                    }
4461
4462                    // Map timestamps offset (skip if already mapped - shared data)
4463                    if !anim.timestamps.is_empty()
4464                        && let Entry::Vacant(e) = offset_map.entry(anim.original_timestamps_offset)
4465                    {
4466                        e.insert(anim_data_offset);
4467                        anim_data_offset += anim.timestamps.len() as u32;
4468                    }
4469
4470                    // Map values offset (skip if already mapped - shared data)
4471                    if !anim.values.is_empty()
4472                        && let Entry::Vacant(e) = offset_map.entry(anim.original_values_offset)
4473                    {
4474                        e.insert(anim_data_offset);
4475                        anim_data_offset += anim.values.len() as u32;
4476                    }
4477                }
4478
4479                // Write animations with relocated offsets
4480                for anim in &self.texture_animations {
4481                    let mut relocated_anim = anim.clone();
4482                    relocate_texture_animation_offsets(&mut relocated_anim, &offset_map);
4483
4484                    let mut anim_data = Vec::new();
4485                    relocated_anim.write(&mut anim_data)?;
4486                    data_section.extend_from_slice(&anim_data);
4487                }
4488
4489                // Write animation keyframe data (only write each unique offset once)
4490                let mut written_offsets: std::collections::HashSet<u32> =
4491                    std::collections::HashSet::new();
4492
4493                for anim in &self.raw_data.texture_animation_data {
4494                    // Write interpolation_ranges only if not already written
4495                    if !anim.interpolation_ranges.is_empty()
4496                        && written_offsets.insert(anim.original_ranges_offset)
4497                    {
4498                        data_section.extend_from_slice(&anim.interpolation_ranges);
4499                    }
4500
4501                    // Write timestamps only if not already written
4502                    if !anim.timestamps.is_empty()
4503                        && written_offsets.insert(anim.original_timestamps_offset)
4504                    {
4505                        data_section.extend_from_slice(&anim.timestamps);
4506                    }
4507
4508                    // Write values only if not already written
4509                    if !anim.values.is_empty()
4510                        && written_offsets.insert(anim.original_values_offset)
4511                    {
4512                        data_section.extend_from_slice(&anim.values);
4513                    }
4514                }
4515
4516                current_offset = anim_data_offset;
4517            } else {
4518                // No animation data collected - write animations with zeroed animation tracks
4519                // This happens when we're creating new animations or the source had no keyframes
4520                for anim in &self.texture_animations {
4521                    let mut static_anim = anim.clone();
4522                    // Zero out all animation blocks by setting them to default
4523                    static_anim.translation_u = M2AnimationBlock::default();
4524                    static_anim.translation_v = M2AnimationBlock::default();
4525                    static_anim.rotation = M2AnimationBlock::default();
4526                    static_anim.scale_u = M2AnimationBlock::default();
4527                    static_anim.scale_v = M2AnimationBlock::default();
4528
4529                    let mut anim_data = Vec::new();
4530                    static_anim.write(&mut anim_data)?;
4531                    data_section.extend_from_slice(&anim_data);
4532                }
4533
4534                current_offset += anims_total_size as u32;
4535            }
4536        } else {
4537            header.texture_animations = M2Array::new(0, 0);
4538        }
4539
4540        // Write color animations with animation data preservation
4541        // Similar pattern to texture animations - write structures with relocated offsets, then animation data
4542        if !self.color_animations.is_empty() {
4543            header.color_animations =
4544                M2Array::new(self.color_animations.len() as u32, current_offset);
4545
4546            // First, write all animation structures to a temporary buffer to calculate their total size
4547            let mut temp_anim_data = Vec::new();
4548            for anim in &self.color_animations {
4549                let mut anim_data = Vec::new();
4550                anim.write(&mut anim_data)?;
4551                temp_anim_data.push(anim_data);
4552            }
4553            let anims_total_size: usize = temp_anim_data.iter().map(|v| v.len()).sum();
4554
4555            // Calculate where animation data will be written (after all animation structures)
4556            let anim_data_start = current_offset + anims_total_size as u32;
4557
4558            // Check if we have animation data to preserve
4559            if !self.raw_data.color_animation_data.is_empty() {
4560                // Build offset relocation map: old_offset -> new_offset
4561                let mut offset_map: HashMap<u32, u32> = HashMap::new();
4562                let mut anim_data_offset = anim_data_start;
4563
4564                use std::collections::hash_map::Entry;
4565
4566                for anim in &self.raw_data.color_animation_data {
4567                    // Map interpolation_ranges offset (skip if already mapped - shared data)
4568                    if !anim.interpolation_ranges.is_empty()
4569                        && let Entry::Vacant(e) = offset_map.entry(anim.original_ranges_offset)
4570                    {
4571                        e.insert(anim_data_offset);
4572                        anim_data_offset += anim.interpolation_ranges.len() as u32;
4573                    }
4574
4575                    // Map timestamps offset (skip if already mapped - shared data)
4576                    if !anim.timestamps.is_empty()
4577                        && let Entry::Vacant(e) = offset_map.entry(anim.original_timestamps_offset)
4578                    {
4579                        e.insert(anim_data_offset);
4580                        anim_data_offset += anim.timestamps.len() as u32;
4581                    }
4582
4583                    // Map values offset (skip if already mapped - shared data)
4584                    if !anim.values.is_empty()
4585                        && let Entry::Vacant(e) = offset_map.entry(anim.original_values_offset)
4586                    {
4587                        e.insert(anim_data_offset);
4588                        anim_data_offset += anim.values.len() as u32;
4589                    }
4590                }
4591
4592                // Write animations with relocated offsets
4593                for anim in &self.color_animations {
4594                    let mut relocated_anim = anim.clone();
4595                    relocate_color_animation_offsets(&mut relocated_anim, &offset_map);
4596
4597                    let mut anim_data = Vec::new();
4598                    relocated_anim.write(&mut anim_data)?;
4599                    data_section.extend_from_slice(&anim_data);
4600                }
4601
4602                // Write animation keyframe data (only write each unique offset once)
4603                let mut written_offsets: std::collections::HashSet<u32> =
4604                    std::collections::HashSet::new();
4605
4606                for anim in &self.raw_data.color_animation_data {
4607                    // Write interpolation_ranges only if not already written
4608                    if !anim.interpolation_ranges.is_empty()
4609                        && written_offsets.insert(anim.original_ranges_offset)
4610                    {
4611                        data_section.extend_from_slice(&anim.interpolation_ranges);
4612                    }
4613
4614                    // Write timestamps only if not already written
4615                    if !anim.timestamps.is_empty()
4616                        && written_offsets.insert(anim.original_timestamps_offset)
4617                    {
4618                        data_section.extend_from_slice(&anim.timestamps);
4619                    }
4620
4621                    // Write values only if not already written
4622                    if !anim.values.is_empty()
4623                        && written_offsets.insert(anim.original_values_offset)
4624                    {
4625                        data_section.extend_from_slice(&anim.values);
4626                    }
4627                }
4628
4629                current_offset = anim_data_offset;
4630            } else {
4631                // No animation data collected - write animations with zeroed animation tracks
4632                for anim in &self.color_animations {
4633                    let mut static_anim = anim.clone();
4634                    // Zero out all animation blocks by setting them to default
4635                    static_anim.color = M2AnimationBlock::default();
4636                    static_anim.alpha = M2AnimationBlock::default();
4637
4638                    let mut anim_data = Vec::new();
4639                    static_anim.write(&mut anim_data)?;
4640                    data_section.extend_from_slice(&anim_data);
4641                }
4642
4643                current_offset += anims_total_size as u32;
4644            }
4645        } else {
4646            header.color_animations = M2Array::new(0, 0);
4647        }
4648
4649        // Write transparency animations with animation data preservation
4650        // Note: header.transparency_lookup field contains M2TransparencyAnimation structures
4651        if !self.transparency_animations.is_empty() {
4652            header.transparency_lookup =
4653                M2Array::new(self.transparency_animations.len() as u32, current_offset);
4654
4655            // First, write all animation structures to a temporary buffer to calculate their total size
4656            let mut temp_anim_data = Vec::new();
4657            for anim in &self.transparency_animations {
4658                let mut anim_data = Vec::new();
4659                anim.write(&mut anim_data)?;
4660                temp_anim_data.push(anim_data);
4661            }
4662            let anims_total_size: usize = temp_anim_data.iter().map(|v| v.len()).sum();
4663
4664            // Calculate where animation data will be written (after all animation structures)
4665            let anim_data_start = current_offset + anims_total_size as u32;
4666
4667            // Check if we have animation data to preserve
4668            if !self.raw_data.transparency_animation_data.is_empty() {
4669                // Build offset relocation map: old_offset -> new_offset
4670                let mut offset_map: HashMap<u32, u32> = HashMap::new();
4671                let mut anim_data_offset = anim_data_start;
4672
4673                use std::collections::hash_map::Entry;
4674
4675                for anim in &self.raw_data.transparency_animation_data {
4676                    // Map interpolation_ranges offset (skip if already mapped - shared data)
4677                    if !anim.interpolation_ranges.is_empty()
4678                        && let Entry::Vacant(e) = offset_map.entry(anim.original_ranges_offset)
4679                    {
4680                        e.insert(anim_data_offset);
4681                        anim_data_offset += anim.interpolation_ranges.len() as u32;
4682                    }
4683
4684                    // Map timestamps offset (skip if already mapped - shared data)
4685                    if !anim.timestamps.is_empty()
4686                        && let Entry::Vacant(e) = offset_map.entry(anim.original_timestamps_offset)
4687                    {
4688                        e.insert(anim_data_offset);
4689                        anim_data_offset += anim.timestamps.len() as u32;
4690                    }
4691
4692                    // Map values offset (skip if already mapped - shared data)
4693                    if !anim.values.is_empty()
4694                        && let Entry::Vacant(e) = offset_map.entry(anim.original_values_offset)
4695                    {
4696                        e.insert(anim_data_offset);
4697                        anim_data_offset += anim.values.len() as u32;
4698                    }
4699                }
4700
4701                // Write animations with relocated offsets
4702                for anim in &self.transparency_animations {
4703                    let mut relocated_anim = anim.clone();
4704                    relocate_transparency_animation_offsets(&mut relocated_anim, &offset_map);
4705
4706                    let mut anim_data = Vec::new();
4707                    relocated_anim.write(&mut anim_data)?;
4708                    data_section.extend_from_slice(&anim_data);
4709                }
4710
4711                // Write animation keyframe data (only write each unique offset once)
4712                let mut written_offsets: std::collections::HashSet<u32> =
4713                    std::collections::HashSet::new();
4714
4715                for anim in &self.raw_data.transparency_animation_data {
4716                    // Write interpolation_ranges only if not already written
4717                    if !anim.interpolation_ranges.is_empty()
4718                        && written_offsets.insert(anim.original_ranges_offset)
4719                    {
4720                        data_section.extend_from_slice(&anim.interpolation_ranges);
4721                    }
4722
4723                    // Write timestamps only if not already written
4724                    if !anim.timestamps.is_empty()
4725                        && written_offsets.insert(anim.original_timestamps_offset)
4726                    {
4727                        data_section.extend_from_slice(&anim.timestamps);
4728                    }
4729
4730                    // Write values only if not already written
4731                    if !anim.values.is_empty()
4732                        && written_offsets.insert(anim.original_values_offset)
4733                    {
4734                        data_section.extend_from_slice(&anim.values);
4735                    }
4736                }
4737
4738                current_offset = anim_data_offset;
4739            } else {
4740                // No animation data collected - write animations with zeroed animation tracks
4741                for anim in &self.transparency_animations {
4742                    let mut static_anim = anim.clone();
4743                    // Zero out all animation blocks by setting them to default
4744                    static_anim.alpha = M2AnimationBlock::default();
4745
4746                    let mut anim_data = Vec::new();
4747                    static_anim.write(&mut anim_data)?;
4748                    data_section.extend_from_slice(&anim_data);
4749                }
4750
4751                current_offset += anims_total_size as u32;
4752            }
4753        } else {
4754            header.transparency_lookup = M2Array::new(0, 0);
4755        }
4756
4757        // ==============================
4758        // EVENTS SECTION
4759        // ==============================
4760        // Events are timeline triggers (sounds, effects) using simple M2Array<u32> for timestamps
4761        if !self.events.is_empty() {
4762            // Calculate offset from actual data_section length, not tracked current_offset
4763            let event_offset = self.calculate_header_size() + data_section.len();
4764            header.events = M2Array::new(self.events.len() as u32, event_offset as u32);
4765
4766            // First, write all event structures to a temporary buffer to calculate their total size
4767            let mut temp_event_data = Vec::new();
4768            for event in &self.events {
4769                let mut event_data = Vec::new();
4770                event.write(&mut event_data, header.version)?;
4771                temp_event_data.push(event_data);
4772            }
4773            let events_total_size: usize = temp_event_data.iter().map(|v| v.len()).sum();
4774
4775            // Calculate where event timestamp data will be written (after all event structures)
4776            let event_data_start = current_offset + events_total_size as u32;
4777
4778            // Check if we have event data to preserve
4779            if !self.raw_data.event_data.is_empty() {
4780                // Build offset relocation map: old_offset -> new_offset
4781                let mut offset_map: HashMap<u32, u32> = HashMap::new();
4782                let mut event_data_offset = event_data_start;
4783
4784                use std::collections::hash_map::Entry;
4785
4786                for event_raw in &self.raw_data.event_data {
4787                    // Map timestamps offset (skip if already mapped - shared data)
4788                    if !event_raw.timestamps.is_empty()
4789                        && let Entry::Vacant(e) =
4790                            offset_map.entry(event_raw.original_timestamps_offset)
4791                    {
4792                        e.insert(event_data_offset);
4793                        event_data_offset += event_raw.timestamps.len() as u32;
4794                    }
4795                }
4796
4797                // Write events with relocated offsets
4798                for event in &self.events {
4799                    let mut relocated_event = event.clone();
4800                    relocate_event_offset(&mut relocated_event, &offset_map);
4801
4802                    let mut event_data = Vec::new();
4803                    relocated_event.write(&mut event_data, header.version)?;
4804                    data_section.extend_from_slice(&event_data);
4805                }
4806
4807                // Write event data (ranges and timestamps, only write each unique offset once)
4808                let mut written_offsets: std::collections::HashSet<u32> =
4809                    std::collections::HashSet::new();
4810
4811                for event_raw in &self.raw_data.event_data {
4812                    // Write ranges if not already written
4813                    if !event_raw.ranges.is_empty()
4814                        && written_offsets.insert(event_raw.original_ranges_offset)
4815                    {
4816                        data_section.extend_from_slice(&event_raw.ranges);
4817                    }
4818                    // Write timestamps if not already written
4819                    if !event_raw.timestamps.is_empty()
4820                        && written_offsets.insert(event_raw.original_timestamps_offset)
4821                    {
4822                        data_section.extend_from_slice(&event_raw.timestamps);
4823                    }
4824                }
4825
4826                current_offset = event_data_offset;
4827            } else {
4828                // No event data collected - write events with zeroed tracks
4829                for event in &self.events {
4830                    let mut static_event = event.clone();
4831                    // Zero out the event tracks
4832                    static_event.ranges = M2Array::default();
4833                    static_event.times = M2Array::default();
4834
4835                    let mut event_data = Vec::new();
4836                    static_event.write(&mut event_data, header.version)?;
4837                    data_section.extend_from_slice(&event_data);
4838                }
4839
4840                current_offset += events_total_size as u32;
4841            }
4842        } else {
4843            header.events = M2Array::new(0, 0);
4844        }
4845
4846        // ==============================
4847        // ATTACHMENTS SECTION
4848        // ==============================
4849        // Attachments are attach points (weapons, effects) with scale animation
4850        if !self.attachments.is_empty() {
4851            // Calculate offset from actual data_section length, not tracked current_offset
4852            let attach_offset = self.calculate_header_size() + data_section.len();
4853            header.attachments = M2Array::new(self.attachments.len() as u32, attach_offset as u32);
4854
4855            // First, write all attachment structures to a temporary buffer to calculate their total size
4856            let mut temp_attach_data = Vec::new();
4857            for attach in &self.attachments {
4858                let mut attach_data = Vec::new();
4859                attach.write(&mut attach_data, header.version)?;
4860                temp_attach_data.push(attach_data);
4861            }
4862            let attachs_total_size: usize = temp_attach_data.iter().map(|v| v.len()).sum();
4863
4864            // Calculate where attachment animation data will be written (after all attachment structures)
4865            let attach_data_start = current_offset + attachs_total_size as u32;
4866
4867            // Check if we have attachment animation data to preserve
4868            if !self.raw_data.attachment_animation_data.is_empty() {
4869                // Build offset relocation map: old_offset -> new_offset
4870                let mut offset_map: HashMap<u32, u32> = HashMap::new();
4871                let mut attach_data_offset = attach_data_start;
4872
4873                use std::collections::hash_map::Entry;
4874
4875                for anim in &self.raw_data.attachment_animation_data {
4876                    // Map interpolation_ranges offset (skip if already mapped - shared data)
4877                    if !anim.interpolation_ranges.is_empty()
4878                        && let Entry::Vacant(e) = offset_map.entry(anim.original_ranges_offset)
4879                    {
4880                        e.insert(attach_data_offset);
4881                        attach_data_offset += anim.interpolation_ranges.len() as u32;
4882                    }
4883
4884                    // Map timestamps offset (skip if already mapped - shared data)
4885                    if !anim.timestamps.is_empty()
4886                        && let Entry::Vacant(e) = offset_map.entry(anim.original_timestamps_offset)
4887                    {
4888                        e.insert(attach_data_offset);
4889                        attach_data_offset += anim.timestamps.len() as u32;
4890                    }
4891
4892                    // Map values offset (skip if already mapped - shared data)
4893                    if !anim.values.is_empty()
4894                        && let Entry::Vacant(e) = offset_map.entry(anim.original_values_offset)
4895                    {
4896                        e.insert(attach_data_offset);
4897                        attach_data_offset += anim.values.len() as u32;
4898                    }
4899                }
4900
4901                // Write attachments with relocated offsets
4902                for attach in &self.attachments {
4903                    let mut relocated_attach = attach.clone();
4904                    relocate_attachment_animation_offsets(&mut relocated_attach, &offset_map);
4905
4906                    let mut attach_data = Vec::new();
4907                    relocated_attach.write(&mut attach_data, header.version)?;
4908                    data_section.extend_from_slice(&attach_data);
4909                }
4910
4911                // Write animation keyframe data (only write each unique offset once)
4912                let mut written_offsets: std::collections::HashSet<u32> =
4913                    std::collections::HashSet::new();
4914
4915                for anim in &self.raw_data.attachment_animation_data {
4916                    // Write interpolation_ranges only if not already written
4917                    if !anim.interpolation_ranges.is_empty()
4918                        && written_offsets.insert(anim.original_ranges_offset)
4919                    {
4920                        data_section.extend_from_slice(&anim.interpolation_ranges);
4921                    }
4922
4923                    // Write timestamps only if not already written
4924                    if !anim.timestamps.is_empty()
4925                        && written_offsets.insert(anim.original_timestamps_offset)
4926                    {
4927                        data_section.extend_from_slice(&anim.timestamps);
4928                    }
4929
4930                    // Write values only if not already written
4931                    if !anim.values.is_empty()
4932                        && written_offsets.insert(anim.original_values_offset)
4933                    {
4934                        data_section.extend_from_slice(&anim.values);
4935                    }
4936                }
4937
4938                current_offset = attach_data_offset;
4939            } else {
4940                // No attachment animation data collected - write attachments with zeroed animation tracks
4941                for attach in &self.attachments {
4942                    let mut static_attach = attach.clone();
4943                    // Zero out the scale animation block
4944                    static_attach.scale_animation = M2AnimationBlock::default();
4945
4946                    let mut attach_data = Vec::new();
4947                    static_attach.write(&mut attach_data, header.version)?;
4948                    data_section.extend_from_slice(&attach_data);
4949                }
4950
4951                current_offset += attachs_total_size as u32;
4952            }
4953        } else {
4954            header.attachments = M2Array::new(0, 0);
4955        }
4956
4957        // ==============================
4958        // CAMERAS SECTION
4959        // ==============================
4960        // Cameras have position, target_position, and roll animation tracks
4961        if !self.cameras.is_empty() {
4962            // Calculate offset from actual data_section length, not tracked current_offset
4963            let camera_offset = self.calculate_header_size() + data_section.len();
4964            header.cameras = M2Array::new(self.cameras.len() as u32, camera_offset as u32);
4965
4966            // First, write all camera structures to a temporary buffer to calculate their total size
4967            let mut temp_camera_data = Vec::new();
4968            for camera in &self.cameras {
4969                let mut camera_data = Vec::new();
4970                camera.write(&mut camera_data, header.version)?;
4971                temp_camera_data.push(camera_data);
4972            }
4973            let cameras_total_size: usize = temp_camera_data.iter().map(|v| v.len()).sum();
4974
4975            // Calculate where camera animation data will be written (after all camera structures)
4976            let camera_data_start = current_offset + cameras_total_size as u32;
4977
4978            // Check if we have camera animation data to preserve
4979            if !self.raw_data.camera_animation_data.is_empty() {
4980                // Build offset relocation map: old_offset -> new_offset
4981                let mut offset_map: HashMap<u32, u32> = HashMap::new();
4982                let mut camera_data_offset = camera_data_start;
4983
4984                use std::collections::hash_map::Entry;
4985
4986                for anim in &self.raw_data.camera_animation_data {
4987                    // Map interpolation_ranges offset (skip if already mapped - shared data)
4988                    if !anim.interpolation_ranges.is_empty()
4989                        && let Entry::Vacant(e) = offset_map.entry(anim.original_ranges_offset)
4990                    {
4991                        e.insert(camera_data_offset);
4992                        camera_data_offset += anim.interpolation_ranges.len() as u32;
4993                    }
4994
4995                    // Map timestamps offset (skip if already mapped - shared data)
4996                    if !anim.timestamps.is_empty()
4997                        && let Entry::Vacant(e) = offset_map.entry(anim.original_timestamps_offset)
4998                    {
4999                        e.insert(camera_data_offset);
5000                        camera_data_offset += anim.timestamps.len() as u32;
5001                    }
5002
5003                    // Map values offset (skip if already mapped - shared data)
5004                    if !anim.values.is_empty()
5005                        && let Entry::Vacant(e) = offset_map.entry(anim.original_values_offset)
5006                    {
5007                        e.insert(camera_data_offset);
5008                        camera_data_offset += anim.values.len() as u32;
5009                    }
5010                }
5011
5012                // Write cameras with relocated offsets
5013                for camera in &self.cameras {
5014                    let mut relocated_camera = camera.clone();
5015                    relocate_camera_animation_offsets(&mut relocated_camera, &offset_map);
5016
5017                    let mut camera_data = Vec::new();
5018                    relocated_camera.write(&mut camera_data, header.version)?;
5019                    data_section.extend_from_slice(&camera_data);
5020                }
5021
5022                // Write animation keyframe data (only write each unique offset once)
5023                let mut written_offsets: std::collections::HashSet<u32> =
5024                    std::collections::HashSet::new();
5025
5026                for anim in &self.raw_data.camera_animation_data {
5027                    // Write interpolation_ranges only if not already written
5028                    if !anim.interpolation_ranges.is_empty()
5029                        && written_offsets.insert(anim.original_ranges_offset)
5030                    {
5031                        data_section.extend_from_slice(&anim.interpolation_ranges);
5032                    }
5033
5034                    // Write timestamps only if not already written
5035                    if !anim.timestamps.is_empty()
5036                        && written_offsets.insert(anim.original_timestamps_offset)
5037                    {
5038                        data_section.extend_from_slice(&anim.timestamps);
5039                    }
5040
5041                    // Write values only if not already written
5042                    if !anim.values.is_empty()
5043                        && written_offsets.insert(anim.original_values_offset)
5044                    {
5045                        data_section.extend_from_slice(&anim.values);
5046                    }
5047                }
5048
5049                current_offset = camera_data_offset;
5050            } else {
5051                // No camera animation data collected - write cameras with zeroed animation tracks
5052                for camera in &self.cameras {
5053                    let mut static_camera = camera.clone();
5054                    // Zero out all animation blocks
5055                    static_camera.position_animation = M2AnimationBlock::default();
5056                    static_camera.target_position_animation = M2AnimationBlock::default();
5057                    static_camera.roll_animation = M2AnimationBlock::default();
5058
5059                    let mut camera_data = Vec::new();
5060                    static_camera.write(&mut camera_data, header.version)?;
5061                    data_section.extend_from_slice(&camera_data);
5062                }
5063
5064                current_offset += cameras_total_size as u32;
5065            }
5066        } else {
5067            header.cameras = M2Array::new(0, 0);
5068        }
5069
5070        // ==============================
5071        // LIGHTS SECTION
5072        // ==============================
5073        // Lights have ambient_color, diffuse_color, attenuation_start, attenuation_end, and visibility animation tracks
5074        if !self.lights.is_empty() {
5075            // Calculate offset from actual data_section length, not tracked current_offset
5076            let light_offset = self.calculate_header_size() + data_section.len();
5077            header.lights = M2Array::new(self.lights.len() as u32, light_offset as u32);
5078
5079            // First, write all light structures to a temporary buffer to calculate their total size
5080            let mut temp_light_data = Vec::new();
5081            for light in &self.lights {
5082                let mut light_data = Vec::new();
5083                light.write(&mut light_data, header.version)?;
5084                temp_light_data.push(light_data);
5085            }
5086            let lights_total_size: usize = temp_light_data.iter().map(|v| v.len()).sum();
5087
5088            // Calculate where light animation data will be written (after all light structures)
5089            let light_data_start = current_offset + lights_total_size as u32;
5090
5091            // Check if we have light animation data to preserve
5092            if !self.raw_data.light_animation_data.is_empty() {
5093                // Build offset relocation map: old_offset -> new_offset
5094                let mut offset_map: HashMap<u32, u32> = HashMap::new();
5095                let mut light_data_offset = light_data_start;
5096
5097                use std::collections::hash_map::Entry;
5098
5099                for anim in &self.raw_data.light_animation_data {
5100                    // Map interpolation_ranges offset (skip if already mapped - shared data)
5101                    if !anim.interpolation_ranges.is_empty()
5102                        && let Entry::Vacant(e) = offset_map.entry(anim.original_ranges_offset)
5103                    {
5104                        e.insert(light_data_offset);
5105                        light_data_offset += anim.interpolation_ranges.len() as u32;
5106                    }
5107
5108                    // Map timestamps offset (skip if already mapped - shared data)
5109                    if !anim.timestamps.is_empty()
5110                        && let Entry::Vacant(e) = offset_map.entry(anim.original_timestamps_offset)
5111                    {
5112                        e.insert(light_data_offset);
5113                        light_data_offset += anim.timestamps.len() as u32;
5114                    }
5115
5116                    // Map values offset (skip if already mapped - shared data)
5117                    if !anim.values.is_empty()
5118                        && let Entry::Vacant(e) = offset_map.entry(anim.original_values_offset)
5119                    {
5120                        e.insert(light_data_offset);
5121                        light_data_offset += anim.values.len() as u32;
5122                    }
5123                }
5124
5125                // Write lights with relocated offsets
5126                for light in &self.lights {
5127                    let mut relocated_light = light.clone();
5128                    relocate_light_animation_offsets(&mut relocated_light, &offset_map);
5129
5130                    let mut light_data = Vec::new();
5131                    relocated_light.write(&mut light_data, header.version)?;
5132                    data_section.extend_from_slice(&light_data);
5133                }
5134
5135                // Write animation keyframe data (only write each unique offset once)
5136                let mut written_offsets: std::collections::HashSet<u32> =
5137                    std::collections::HashSet::new();
5138
5139                for anim in &self.raw_data.light_animation_data {
5140                    // Write interpolation_ranges only if not already written
5141                    if !anim.interpolation_ranges.is_empty()
5142                        && written_offsets.insert(anim.original_ranges_offset)
5143                    {
5144                        data_section.extend_from_slice(&anim.interpolation_ranges);
5145                    }
5146
5147                    // Write timestamps only if not already written
5148                    if !anim.timestamps.is_empty()
5149                        && written_offsets.insert(anim.original_timestamps_offset)
5150                    {
5151                        data_section.extend_from_slice(&anim.timestamps);
5152                    }
5153
5154                    // Write values only if not already written
5155                    if !anim.values.is_empty()
5156                        && written_offsets.insert(anim.original_values_offset)
5157                    {
5158                        data_section.extend_from_slice(&anim.values);
5159                    }
5160                }
5161
5162                current_offset = light_data_offset;
5163            } else {
5164                // No light animation data collected - write lights with zeroed animation tracks
5165                for light in &self.lights {
5166                    let mut static_light = light.clone();
5167                    // Zero out all animation blocks
5168                    static_light.ambient_color_animation = M2AnimationBlock::default();
5169                    static_light.diffuse_color_animation = M2AnimationBlock::default();
5170                    static_light.attenuation_start_animation = M2AnimationBlock::default();
5171                    static_light.attenuation_end_animation = M2AnimationBlock::default();
5172                    static_light.visibility_animation = M2AnimationBlock::default();
5173
5174                    let mut light_data = Vec::new();
5175                    static_light.write(&mut light_data, header.version)?;
5176                    data_section.extend_from_slice(&light_data);
5177                }
5178
5179                current_offset += lights_total_size as u32;
5180            }
5181        } else {
5182            header.lights = M2Array::new(0, 0);
5183        }
5184
5185        // Zero out sections we don't write yet (so header references are valid)
5186        // These sections have complex structures with embedded offsets that need proper serialization
5187        header.color_replacements = M2Array::new(0, 0);
5188        // Note: ribbon_emitters is now handled in the serialization section above
5189        // Note: particle_emitters is now handled in the serialization section above
5190
5191        // Version-specific fields: set up correctly based on target version
5192        // TBC and earlier (version <= 263) require texture_flipbooks
5193        if header.version <= 263 {
5194            header.texture_flipbooks = Some(M2Array::new(0, 0));
5195            // Only zero views if we didn't write embedded skin data
5196            if self.raw_data.embedded_skins.is_empty() {
5197                header.views = M2Array::new(0, 0);
5198            }
5199        } else {
5200            header.texture_flipbooks = None;
5201        }
5202
5203        // Vanilla and TBC (256-263) have playable_animation_lookup
5204        // This field was removed in WotLK (264+)
5205        if (256..=263).contains(&header.version) {
5206            header.playable_animation_lookup = Some(M2Array::new(0, 0));
5207        } else {
5208            header.playable_animation_lookup = None;
5209        }
5210
5211        // Clear post-BC optional fields we don't serialize
5212        header.blend_map_overrides = None;
5213        header.texture_combiner_combos = None;
5214        header.texture_transforms = None;
5215
5216        // Suppress unused variable warning
5217        let _ = current_offset;
5218
5219        // Finally, write the header followed by the data section
5220        header.write(writer)?;
5221        writer.write_all(&data_section)?;
5222
5223        Ok(())
5224    }
5225
5226    /// Convert this model to a different version
5227    pub fn convert(&self, target_version: M2Version) -> Result<Self> {
5228        let source_version = self.header.version().ok_or(M2Error::ConversionError {
5229            from: self.header.version,
5230            to: target_version.to_header_version(),
5231            reason: "Unknown source version".to_string(),
5232        })?;
5233
5234        if source_version == target_version {
5235            return Ok(self.clone());
5236        }
5237
5238        // Convert the header
5239        let header = self.header.convert(target_version)?;
5240
5241        // Convert vertices
5242        let vertices = self
5243            .vertices
5244            .iter()
5245            .map(|v| v.convert(target_version))
5246            .collect();
5247
5248        // Convert textures
5249        let textures = self
5250            .textures
5251            .iter()
5252            .map(|t| t.convert(target_version))
5253            .collect();
5254
5255        // Convert bones
5256        let bones = self
5257            .bones
5258            .iter()
5259            .map(|b| b.convert(target_version))
5260            .collect();
5261
5262        // Convert materials
5263        let materials = self
5264            .materials
5265            .iter()
5266            .map(|m| m.convert(target_version))
5267            .collect();
5268
5269        // Create the new model
5270        let mut new_model = self.clone();
5271        new_model.header = header;
5272        new_model.vertices = vertices;
5273        new_model.textures = textures;
5274        new_model.bones = bones;
5275        new_model.materials = materials;
5276
5277        // Chunked format fields are preserved for compatibility
5278        // They will be None for legacy format conversions
5279        new_model.physics_file_id = self.physics_file_id.clone();
5280        new_model.skeleton_file_id = self.skeleton_file_id.clone();
5281        new_model.bone_file_ids = self.bone_file_ids.clone();
5282        new_model.lod_data = self.lod_data.clone();
5283
5284        Ok(new_model)
5285    }
5286
5287    /// Calculate the size of the header for this model version
5288    ///
5289    /// This must match exactly what M2Header::write() produces. The write() method
5290    /// clears optional fields (blend_map_overrides, texture_combiner_combos, texture_transforms)
5291    /// so we don't include them in the size calculation.
5292    fn calculate_header_size(&self) -> usize {
5293        let version = self.header.version().unwrap_or(M2Version::Vanilla);
5294
5295        let mut size = 4 + 4; // Magic + version
5296
5297        // Common fields
5298        size += 2 * 4; // name
5299        size += 4; // flags
5300
5301        size += 2 * 4; // global_sequences
5302        size += 2 * 4; // animations
5303        size += 2 * 4; // animation_lookup
5304
5305        // Vanilla and TBC (256-263) have playable_animation_lookup
5306        // This field was removed in WotLK (264+)
5307        let version_num = version.to_header_version();
5308        if (256..=263).contains(&version_num) {
5309            size += 2 * 4; // playable_animation_lookup
5310        }
5311
5312        size += 2 * 4; // bones
5313        size += 2 * 4; // key_bone_lookup
5314
5315        size += 2 * 4; // vertices
5316
5317        // Views field changes between versions
5318        if version <= M2Version::TBC {
5319            size += 2 * 4; // views as M2Array (8 bytes)
5320        } else {
5321            size += 4; // num_skin_profiles as u32 (4 bytes)
5322        }
5323
5324        size += 2 * 4; // color_animations
5325
5326        size += 2 * 4; // textures
5327        size += 2 * 4; // transparency_lookup
5328
5329        // Texture flipbooks only exist in BC and earlier
5330        if version <= M2Version::TBC {
5331            size += 2 * 4; // texture_flipbooks
5332        }
5333
5334        size += 2 * 4; // texture_animations
5335
5336        size += 2 * 4; // color_replacements
5337        size += 2 * 4; // render_flags
5338        size += 2 * 4; // bone_lookup_table
5339        size += 2 * 4; // texture_lookup_table
5340        size += 2 * 4; // texture_units
5341        size += 2 * 4; // transparency_lookup_table
5342        size += 2 * 4; // texture_animation_lookup
5343
5344        size += 3 * 4; // bounding_box_min
5345        size += 3 * 4; // bounding_box_max
5346        size += 4; // bounding_sphere_radius
5347
5348        size += 3 * 4; // collision_box_min
5349        size += 3 * 4; // collision_box_max
5350        size += 4; // collision_sphere_radius
5351
5352        size += 2 * 4; // bounding_triangles
5353        size += 2 * 4; // bounding_vertices
5354        size += 2 * 4; // bounding_normals
5355
5356        size += 2 * 4; // attachments
5357        size += 2 * 4; // attachment_lookup_table
5358        size += 2 * 4; // events
5359        size += 2 * 4; // lights
5360        size += 2 * 4; // cameras
5361        size += 2 * 4; // camera_lookup_table
5362
5363        size += 2 * 4; // ribbon_emitters
5364        size += 2 * 4; // particle_emitters
5365
5366        // Note: Optional fields (blend_map_overrides, texture_combiner_combos, texture_transforms)
5367        // are NOT included because write() always clears them to None before writing the header.
5368
5369        size
5370    }
5371
5372    /// Validate the model structure
5373    pub fn validate(&self) -> Result<()> {
5374        // Check if the version is supported
5375        if self.header.version().is_none() {
5376            return Err(M2Error::UnsupportedVersion(self.header.version.to_string()));
5377        }
5378
5379        // Validate vertices
5380        if self.vertices.is_empty() {
5381            return Err(M2Error::ValidationError(
5382                "Model has no vertices".to_string(),
5383            ));
5384        }
5385
5386        // Validate bones
5387        for (i, bone) in self.bones.iter().enumerate() {
5388            // Check if parent bone is valid
5389            if bone.parent_bone >= 0 && bone.parent_bone as usize >= self.bones.len() {
5390                return Err(M2Error::ValidationError(format!(
5391                    "Bone {} has invalid parent bone {}",
5392                    i, bone.parent_bone
5393                )));
5394            }
5395        }
5396
5397        // Validate textures
5398        for (i, texture) in self.textures.iter().enumerate() {
5399            // Check if the texture has a valid filename
5400            if texture.filename.array.count > 0 && texture.filename.array.offset == 0 {
5401                return Err(M2Error::ValidationError(format!(
5402                    "Texture {i} has invalid filename offset"
5403                )));
5404            }
5405        }
5406
5407        // Validate materials (simplified - just check basic structure)
5408        for (i, _material) in self.materials.iter().enumerate() {
5409            // Materials now only contain render flags and blend modes
5410            // No direct texture references to validate here
5411            let _material_index = i; // Just to acknowledge we're iterating
5412        }
5413
5414        Ok(())
5415    }
5416
5417    /// Check if this model has external file references (Legion+ chunked format)
5418    pub fn has_external_files(&self) -> bool {
5419        self.skin_file_ids.is_some()
5420            || self.animation_file_ids.is_some()
5421            || self.texture_file_ids.is_some()
5422            || self.physics_file_id.is_some()
5423            || self.skeleton_file_id.is_some()
5424            || self.bone_file_ids.is_some()
5425            || self.lod_data.is_some()
5426            || self.has_advanced_features()
5427    }
5428
5429    /// Get the number of skin files referenced
5430    pub fn skin_file_count(&self) -> usize {
5431        self.skin_file_ids.as_ref().map_or(0, |ids| ids.len())
5432    }
5433
5434    /// Get the number of animation files referenced
5435    pub fn animation_file_count(&self) -> usize {
5436        self.animation_file_ids.as_ref().map_or(0, |ids| ids.len())
5437    }
5438
5439    /// Get the number of texture files referenced
5440    pub fn texture_file_count(&self) -> usize {
5441        self.texture_file_ids.as_ref().map_or(0, |ids| ids.len())
5442    }
5443
5444    /// Resolve a skin file path by index using a FileResolver
5445    pub fn resolve_skin_path(&self, index: usize, resolver: &dyn FileResolver) -> Result<String> {
5446        let skin_ids = self.skin_file_ids.as_ref().ok_or_else(|| {
5447            M2Error::ExternalFileError("Model has no external skin files".to_string())
5448        })?;
5449
5450        let id = skin_ids.get(index).ok_or_else(|| {
5451            M2Error::ExternalFileError(format!("Skin index {} out of range", index))
5452        })?;
5453
5454        resolver.resolve_file_data_id(id)
5455    }
5456
5457    /// Load a skin file by index using a FileResolver
5458    pub fn load_skin_file(&self, index: usize, resolver: &dyn FileResolver) -> Result<Vec<u8>> {
5459        let skin_ids = self.skin_file_ids.as_ref().ok_or_else(|| {
5460            M2Error::ExternalFileError("Model has no external skin files".to_string())
5461        })?;
5462
5463        let id = skin_ids.get(index).ok_or_else(|| {
5464            M2Error::ExternalFileError(format!("Skin index {} out of range", index))
5465        })?;
5466
5467        resolver.load_skin_by_id(id)
5468    }
5469
5470    /// Resolve an animation file path by index using a FileResolver
5471    pub fn resolve_animation_path(
5472        &self,
5473        index: usize,
5474        resolver: &dyn FileResolver,
5475    ) -> Result<String> {
5476        let anim_ids = self.animation_file_ids.as_ref().ok_or_else(|| {
5477            M2Error::ExternalFileError("Model has no external animation files".to_string())
5478        })?;
5479
5480        let id = anim_ids.get(index).ok_or_else(|| {
5481            M2Error::ExternalFileError(format!("Animation index {} out of range", index))
5482        })?;
5483
5484        resolver.resolve_file_data_id(id)
5485    }
5486
5487    /// Load an animation file by index using a FileResolver
5488    pub fn load_animation_file(
5489        &self,
5490        index: usize,
5491        resolver: &dyn FileResolver,
5492    ) -> Result<Vec<u8>> {
5493        let anim_ids = self.animation_file_ids.as_ref().ok_or_else(|| {
5494            M2Error::ExternalFileError("Model has no external animation files".to_string())
5495        })?;
5496
5497        let id = anim_ids.get(index).ok_or_else(|| {
5498            M2Error::ExternalFileError(format!("Animation index {} out of range", index))
5499        })?;
5500
5501        resolver.load_animation_by_id(id)
5502    }
5503
5504    /// Resolve a texture file path by index using a FileResolver
5505    /// Falls back to embedded texture names for pre-Legion models
5506    pub fn resolve_texture_path(
5507        &self,
5508        index: usize,
5509        resolver: &dyn FileResolver,
5510    ) -> Result<String> {
5511        // Legion+ models use TXID chunk
5512        if let Some(texture_ids) = &self.texture_file_ids
5513            && let Some(id) = texture_ids.get(index)
5514        {
5515            return resolver.resolve_file_data_id(id);
5516        }
5517
5518        // Pre-Legion models use embedded texture names
5519        if let Some(texture) = self.textures.get(index)
5520            && !texture.filename.string.data.is_empty()
5521        {
5522            let filename = String::from_utf8_lossy(&texture.filename.string.data).to_string();
5523            return Ok(filename.trim_end_matches('\0').to_string());
5524        }
5525
5526        Err(M2Error::ExternalFileError(format!(
5527            "Texture index {} not found",
5528            index
5529        )))
5530    }
5531
5532    /// Load a texture file by index using a FileResolver
5533    /// Falls back to embedded texture names for pre-Legion models
5534    pub fn load_texture_file(&self, index: usize, resolver: &dyn FileResolver) -> Result<Vec<u8>> {
5535        // Legion+ models use TXID chunk
5536        if let Some(texture_ids) = &self.texture_file_ids
5537            && let Some(id) = texture_ids.get(index)
5538        {
5539            return resolver.load_texture_by_id(id);
5540        }
5541
5542        // Pre-Legion models use embedded texture names - we can't load them directly
5543        // since we don't have FileDataIDs, just return an error with the filename
5544        if let Some(texture) = self.textures.get(index)
5545            && !texture.filename.string.data.is_empty()
5546        {
5547            let filename = String::from_utf8_lossy(&texture.filename.string.data).to_string();
5548            let clean_filename = filename.trim_end_matches('\0').to_string();
5549            return Err(M2Error::ExternalFileError(format!(
5550                "Cannot load pre-Legion texture '{}' without FileDataID",
5551                clean_filename
5552            )));
5553        }
5554
5555        Err(M2Error::ExternalFileError(format!(
5556            "Texture index {} not found",
5557            index
5558        )))
5559    }
5560
5561    /// Get all skin file IDs
5562    pub fn get_skin_file_ids(&self) -> Option<&[u32]> {
5563        self.skin_file_ids.as_ref().map(|ids| ids.ids.as_slice())
5564    }
5565
5566    /// Get all animation file IDs
5567    pub fn get_animation_file_ids(&self) -> Option<&[u32]> {
5568        self.animation_file_ids
5569            .as_ref()
5570            .map(|ids| ids.ids.as_slice())
5571    }
5572
5573    /// Get all texture file IDs
5574    pub fn get_texture_file_ids(&self) -> Option<&[u32]> {
5575        self.texture_file_ids.as_ref().map(|ids| ids.ids.as_slice())
5576    }
5577
5578    /// Get the physics file ID
5579    pub fn get_physics_file_id(&self) -> Option<u32> {
5580        self.physics_file_id.as_ref().map(|id| id.id)
5581    }
5582
5583    /// Get the skeleton file ID
5584    pub fn get_skeleton_file_id(&self) -> Option<u32> {
5585        self.skeleton_file_id.as_ref().map(|id| id.id)
5586    }
5587
5588    /// Get all bone file IDs
5589    pub fn get_bone_file_ids(&self) -> Option<&[u32]> {
5590        self.bone_file_ids.as_ref().map(|ids| ids.ids.as_slice())
5591    }
5592
5593    /// Get the LOD data
5594    pub fn get_lod_data(&self) -> Option<&LodData> {
5595        self.lod_data.as_ref()
5596    }
5597
5598    /// Load physics data using a FileResolver
5599    pub fn load_physics(&self, resolver: &dyn FileResolver) -> Result<Option<PhysicsData>> {
5600        match &self.physics_file_id {
5601            Some(pfid) => {
5602                let data = resolver.load_physics_by_id(&pfid.id)?;
5603                Ok(Some(PhysicsData::parse(&data)?))
5604            }
5605            None => Ok(None),
5606        }
5607    }
5608
5609    /// Load skeleton data using a FileResolver
5610    pub fn load_skeleton(&self, resolver: &dyn FileResolver) -> Result<Option<SkeletonData>> {
5611        match &self.skeleton_file_id {
5612            Some(skid) => {
5613                let data = resolver.load_skeleton_by_id(&skid.id)?;
5614                Ok(Some(SkeletonData::parse(&data)?))
5615            }
5616            None => Ok(None),
5617        }
5618    }
5619
5620    /// Load bone data by index using a FileResolver
5621    pub fn load_bone_data(
5622        &self,
5623        index: usize,
5624        resolver: &dyn FileResolver,
5625    ) -> Result<Option<BoneData>> {
5626        match &self.bone_file_ids {
5627            Some(bfid) => {
5628                if let Some(id) = bfid.get(index) {
5629                    let data = resolver.load_bone_by_id(&id)?;
5630                    Ok(Some(BoneData::parse(&data)?))
5631                } else {
5632                    Err(M2Error::ExternalFileError(format!(
5633                        "Bone index {} out of range",
5634                        index
5635                    )))
5636                }
5637            }
5638            None => Ok(None),
5639        }
5640    }
5641
5642    /// Get the number of bone files referenced
5643    pub fn bone_file_count(&self) -> usize {
5644        self.bone_file_ids.as_ref().map_or(0, |ids| ids.len())
5645    }
5646
5647    /// Select the appropriate LOD level for a given distance
5648    pub fn select_lod(&self, distance: f32) -> Option<&crate::chunks::file_references::LodLevel> {
5649        self.lod_data.as_ref()?.select_lod(distance)
5650    }
5651
5652    /// Check if the model has LOD data
5653    pub fn has_lod_data(&self) -> bool {
5654        self.lod_data.is_some()
5655    }
5656
5657    /// Check if an animation sequence is blacklisted
5658    pub fn is_animation_blacklisted(&self, sequence_id: u16) -> bool {
5659        self.parent_animation_blacklist
5660            .as_ref()
5661            .is_some_and(|pabc| pabc.blacklisted_sequences.contains(&sequence_id))
5662    }
5663
5664    /// Get extended particle data
5665    pub fn get_extended_particle_data(&self) -> Option<&ExtendedParticleData> {
5666        self.extended_particle_data.as_ref()
5667    }
5668
5669    /// Get parent animation blacklist
5670    pub fn get_parent_animation_blacklist(&self) -> Option<&ParentAnimationBlacklist> {
5671        self.parent_animation_blacklist.as_ref()
5672    }
5673
5674    /// Get parent animation data
5675    pub fn get_parent_animation_data(&self) -> Option<&ParentAnimationData> {
5676        self.parent_animation_data.as_ref()
5677    }
5678
5679    /// Get waterfall effect data
5680    pub fn get_waterfall_effect(&self) -> Option<&WaterfallEffect> {
5681        self.waterfall_effect.as_ref()
5682    }
5683
5684    /// Get edge fade data
5685    pub fn get_edge_fade_data(&self) -> Option<&EdgeFadeData> {
5686        self.edge_fade_data.as_ref()
5687    }
5688
5689    /// Get model alpha data
5690    pub fn get_model_alpha_data(&self) -> Option<&ModelAlphaData> {
5691        self.model_alpha_data.as_ref()
5692    }
5693
5694    /// Get lighting details
5695    pub fn get_lighting_details(&self) -> Option<&LightingDetails> {
5696        self.lighting_details.as_ref()
5697    }
5698
5699    /// Get recursive particle model IDs
5700    pub fn get_recursive_particle_ids(&self) -> Option<&[u32]> {
5701        self.recursive_particle_ids
5702            .as_ref()
5703            .map(|ids| ids.model_ids.as_slice())
5704    }
5705
5706    /// Get geometry particle model IDs
5707    pub fn get_geometry_particle_ids(&self) -> Option<&[u32]> {
5708        self.geometry_particle_ids
5709            .as_ref()
5710            .map(|ids| ids.model_ids.as_slice())
5711    }
5712
5713    /// Load particle models using a FileResolver
5714    /// This method implements recursion protection to avoid infinite loops
5715    pub fn load_particle_models(
5716        &self,
5717        file_resolver: &dyn crate::file_resolver::FileResolver,
5718    ) -> crate::error::Result<Vec<M2Model>> {
5719        let mut models = Vec::new();
5720        let mut loaded_ids = std::collections::HashSet::new();
5721
5722        // Load recursive particle models with protection against infinite recursion
5723        if let Some(rpid) = &self.recursive_particle_ids {
5724            for &id in &rpid.model_ids {
5725                if !loaded_ids.contains(&id) {
5726                    loaded_ids.insert(id);
5727
5728                    match file_resolver.load_animation_by_id(id) {
5729                        Ok(data) => {
5730                            let mut cursor = std::io::Cursor::new(data);
5731                            match parse_m2(&mut cursor) {
5732                                Ok(format) => models.push(format.model().clone()),
5733                                Err(e) => {
5734                                    // Log warning but continue loading other models
5735                                    eprintln!(
5736                                        "Warning: Failed to load recursive particle model {}: {:?}",
5737                                        id, e
5738                                    );
5739                                }
5740                            }
5741                        }
5742                        Err(e) => {
5743                            eprintln!(
5744                                "Warning: Failed to load recursive particle model data {}: {:?}",
5745                                id, e
5746                            );
5747                        }
5748                    }
5749                }
5750            }
5751        }
5752
5753        // Load geometry particle models
5754        if let Some(gpid) = &self.geometry_particle_ids {
5755            for &id in &gpid.model_ids {
5756                if !loaded_ids.contains(&id) {
5757                    loaded_ids.insert(id);
5758
5759                    match file_resolver.load_animation_by_id(id) {
5760                        Ok(data) => {
5761                            let mut cursor = std::io::Cursor::new(data);
5762                            match parse_m2(&mut cursor) {
5763                                Ok(format) => models.push(format.model().clone()),
5764                                Err(e) => {
5765                                    eprintln!(
5766                                        "Warning: Failed to load geometry particle model {}: {:?}",
5767                                        id, e
5768                                    );
5769                                }
5770                            }
5771                        }
5772                        Err(e) => {
5773                            eprintln!(
5774                                "Warning: Failed to load geometry particle model data {}: {:?}",
5775                                id, e
5776                            );
5777                        }
5778                    }
5779                }
5780            }
5781        }
5782
5783        Ok(models)
5784    }
5785
5786    /// Check if the model has any advanced rendering features
5787    /// Get parent sequence bounds data (PSBC chunk)
5788    pub fn get_parent_sequence_bounds(&self) -> Option<&ParentSequenceBounds> {
5789        self.parent_sequence_bounds.as_ref()
5790    }
5791
5792    /// Get parent event data (PEDC chunk)
5793    pub fn get_parent_event_data(&self) -> Option<&ParentEventData> {
5794        self.parent_event_data.as_ref()
5795    }
5796
5797    /// Get collision mesh data (PCOL chunk)
5798    pub fn get_collision_mesh_data(&self) -> Option<&CollisionMeshData> {
5799        self.collision_mesh_data.as_ref()
5800    }
5801
5802    /// Get physics file data (PFDC chunk)
5803    pub fn get_physics_file_data(&self) -> Option<&PhysicsFileDataChunk> {
5804        self.physics_file_data.as_ref()
5805    }
5806
5807    /// Check if model has advanced features (Legion+)
5808    pub fn has_advanced_features(&self) -> bool {
5809        self.extended_particle_data.is_some()
5810            || self.parent_animation_blacklist.is_some()
5811            || self.parent_animation_data.is_some()
5812            || self.parent_sequence_bounds.is_some()
5813            || self.parent_event_data.is_some()
5814            || self.waterfall_effect.is_some()
5815            || self.edge_fade_data.is_some()
5816            || self.model_alpha_data.is_some()
5817            || self.lighting_details.is_some()
5818            || self.recursive_particle_ids.is_some()
5819            || self.geometry_particle_ids.is_some()
5820            || self.collision_mesh_data.is_some()
5821            || self.physics_file_data.is_some()
5822    }
5823}
5824
5825#[cfg(test)]
5826mod tests {
5827    use super::*;
5828    use crate::chunks::{AnimationFileIds, SkinFileIds, TextureFileIds};
5829    use crate::header::{M2_MAGIC_CHUNKED, M2_MAGIC_LEGACY};
5830    use std::io::Cursor;
5831
5832    #[test]
5833    fn test_m2_format_detection_legacy() {
5834        // Create minimal MD20 format data
5835        let mut data = Vec::new();
5836        data.extend_from_slice(&M2_MAGIC_LEGACY); // MD20 magic
5837        data.extend_from_slice(&256u32.to_le_bytes()); // Version
5838        // Add minimal header data to prevent parse errors
5839        for _ in 0..100 {
5840            data.extend_from_slice(&0u32.to_le_bytes());
5841        }
5842
5843        let mut cursor = Cursor::new(data);
5844        let result = parse_m2(&mut cursor);
5845
5846        assert!(result.is_ok());
5847        let format = result.unwrap();
5848        assert!(format.is_legacy());
5849        assert!(!format.is_chunked());
5850    }
5851
5852    #[test]
5853    fn test_m2_format_detection_chunked() {
5854        // Create minimal MD21 format data with MD21 chunk
5855        let mut data = Vec::new();
5856        data.extend_from_slice(&M2_MAGIC_CHUNKED); // MD21 magic
5857        data.extend_from_slice(&8u32.to_le_bytes()); // Chunk size
5858
5859        // MD21 chunk containing MD20 data
5860        data.extend_from_slice(b"MD21"); // Chunk magic
5861        data.extend_from_slice(&400u32.to_le_bytes()); // Large chunk size for MD20 data
5862
5863        // MD20 data within the chunk
5864        data.extend_from_slice(&M2_MAGIC_LEGACY); // MD20 magic
5865        data.extend_from_slice(&276u32.to_le_bytes()); // Legion version
5866        // Add minimal header data
5867        for _ in 0..100 {
5868            data.extend_from_slice(&0u32.to_le_bytes());
5869        }
5870
5871        let mut cursor = Cursor::new(data);
5872        let result = parse_m2(&mut cursor);
5873
5874        // This will currently fail because our chunked parser is incomplete
5875        // but we can test the format detection part
5876        match result {
5877            Ok(format) => {
5878                assert!(format.is_chunked());
5879                assert!(!format.is_legacy());
5880            }
5881            Err(M2Error::ParseError(msg)) => {
5882                // Expected for incomplete implementation
5883                assert!(
5884                    msg.contains("TODO") || msg.contains("not yet") || msg.contains("incomplete")
5885                );
5886            }
5887            Err(other) => panic!("Unexpected error: {:?}", other),
5888        }
5889    }
5890
5891    #[test]
5892    fn test_invalid_magic_detection() {
5893        let data = b"FAIL\x00\x00\x00\x00"; // Invalid magic
5894        let mut cursor = Cursor::new(data);
5895        let result = parse_m2(&mut cursor);
5896
5897        assert!(result.is_err());
5898        match result.unwrap_err() {
5899            M2Error::InvalidMagicBytes(magic) => {
5900                assert_eq!(&magic, b"FAIL");
5901            }
5902            other => panic!("Expected InvalidMagicBytes error, got: {:?}", other),
5903        }
5904    }
5905
5906    #[test]
5907    fn test_m2_format_model_access() {
5908        // Test that we can access the underlying model from both formats
5909        use crate::version::M2Version;
5910
5911        // Create a test model
5912        let mut test_model = M2Model {
5913            header: M2Header::new(M2Version::Vanilla),
5914            name: Some("test".to_string()),
5915            global_sequences: Vec::new(),
5916            animations: Vec::new(),
5917            animation_lookup: Vec::new(),
5918            bones: Vec::new(),
5919            key_bone_lookup: Vec::new(),
5920            vertices: Vec::new(),
5921            textures: Vec::new(),
5922            materials: Vec::new(),
5923            particle_emitters: Vec::new(),
5924            ribbon_emitters: Vec::new(),
5925            texture_animations: Vec::new(),
5926            color_animations: Vec::new(),
5927            transparency_animations: Vec::new(),
5928            events: Vec::new(),
5929            attachments: Vec::new(),
5930            cameras: Vec::new(),
5931            lights: Vec::new(),
5932            raw_data: M2RawData::default(),
5933            skin_file_ids: None,
5934            animation_file_ids: None,
5935            texture_file_ids: None,
5936            physics_file_id: None,
5937            skeleton_file_id: None,
5938            bone_file_ids: None,
5939            lod_data: None,
5940            extended_particle_data: None,
5941            parent_animation_blacklist: None,
5942            parent_animation_data: None,
5943            waterfall_effect: None,
5944            edge_fade_data: None,
5945            model_alpha_data: None,
5946            lighting_details: None,
5947            recursive_particle_ids: None,
5948            geometry_particle_ids: None,
5949            texture_animation_chunk: None,
5950            particle_geoset_data: None,
5951            dboc_chunk: None,
5952            afra_chunk: None,
5953            dpiv_chunk: None,
5954            parent_sequence_bounds: None,
5955            parent_event_data: None,
5956            collision_mesh_data: None,
5957            physics_file_data: None,
5958        };
5959
5960        // Test legacy format access
5961        let legacy_format = M2Format::Legacy(test_model.clone());
5962        assert_eq!(legacy_format.model().name.as_ref().unwrap(), "test");
5963        assert!(legacy_format.is_legacy());
5964
5965        // Test chunked format access
5966        test_model.skin_file_ids = Some(SkinFileIds { ids: vec![1, 2, 3] });
5967        let chunked_format = M2Format::Chunked(test_model.clone());
5968        assert_eq!(chunked_format.model().name.as_ref().unwrap(), "test");
5969        assert!(chunked_format.is_chunked());
5970        assert_eq!(
5971            chunked_format.model().skin_file_ids.as_ref().unwrap().len(),
5972            3
5973        );
5974    }
5975
5976    #[test]
5977    fn test_file_reference_methods() {
5978        use crate::file_resolver::ListfileResolver;
5979        use crate::version::M2Version;
5980
5981        // Create a chunked format model with file references
5982        let mut model = M2Model {
5983            header: M2Header::new(M2Version::Legion),
5984            name: Some("test_model".to_string()),
5985            global_sequences: Vec::new(),
5986            animations: Vec::new(),
5987            animation_lookup: Vec::new(),
5988            bones: Vec::new(),
5989            key_bone_lookup: Vec::new(),
5990            vertices: Vec::new(),
5991            textures: Vec::new(),
5992            materials: Vec::new(),
5993            particle_emitters: Vec::new(),
5994            ribbon_emitters: Vec::new(),
5995            texture_animations: Vec::new(),
5996            color_animations: Vec::new(),
5997            transparency_animations: Vec::new(),
5998            events: Vec::new(),
5999            attachments: Vec::new(),
6000            cameras: Vec::new(),
6001            lights: Vec::new(),
6002            raw_data: M2RawData::default(),
6003            skin_file_ids: Some(SkinFileIds {
6004                ids: vec![123456, 789012],
6005            }),
6006            animation_file_ids: Some(AnimationFileIds {
6007                ids: vec![111111, 222222],
6008            }),
6009            texture_file_ids: Some(TextureFileIds {
6010                ids: vec![333333, 444444],
6011            }),
6012            physics_file_id: None,
6013            skeleton_file_id: None,
6014            bone_file_ids: None,
6015            lod_data: None,
6016            extended_particle_data: None,
6017            parent_animation_blacklist: None,
6018            parent_animation_data: None,
6019            waterfall_effect: None,
6020            edge_fade_data: None,
6021            model_alpha_data: None,
6022            lighting_details: None,
6023            recursive_particle_ids: None,
6024            geometry_particle_ids: None,
6025            texture_animation_chunk: None,
6026            particle_geoset_data: None,
6027            dboc_chunk: None,
6028            afra_chunk: None,
6029            dpiv_chunk: None,
6030            parent_sequence_bounds: None,
6031            parent_event_data: None,
6032            collision_mesh_data: None,
6033            physics_file_data: None,
6034        };
6035
6036        // Test file count methods
6037        assert!(model.has_external_files());
6038        assert_eq!(model.skin_file_count(), 2);
6039        assert_eq!(model.animation_file_count(), 2);
6040        assert_eq!(model.texture_file_count(), 2);
6041
6042        // Test getter methods
6043        assert_eq!(
6044            model.get_skin_file_ids(),
6045            Some([123456u32, 789012u32].as_slice())
6046        );
6047        assert_eq!(
6048            model.get_animation_file_ids(),
6049            Some([111111u32, 222222u32].as_slice())
6050        );
6051        assert_eq!(
6052            model.get_texture_file_ids(),
6053            Some([333333u32, 444444u32].as_slice())
6054        );
6055
6056        // Create a mock resolver
6057        let mut resolver = ListfileResolver::new();
6058        resolver.add_mapping(123456, "character/human/male/humanmale00.skin");
6059        resolver.add_mapping(789012, "character/human/male/humanmale01.skin");
6060        resolver.add_mapping(111111, "character/human/male/humanmale_walk.anim");
6061        resolver.add_mapping(222222, "character/human/male/humanmale_run.anim");
6062        resolver.add_mapping(333333, "character/textures/skin_human_male.blp");
6063        resolver.add_mapping(444444, "character/textures/hair_human_male.blp");
6064
6065        // Test path resolution
6066        assert_eq!(
6067            model.resolve_skin_path(0, &resolver).unwrap(),
6068            "character/human/male/humanmale00.skin"
6069        );
6070        assert_eq!(
6071            model.resolve_skin_path(1, &resolver).unwrap(),
6072            "character/human/male/humanmale01.skin"
6073        );
6074        assert!(model.resolve_skin_path(2, &resolver).is_err()); // Out of range
6075
6076        assert_eq!(
6077            model.resolve_animation_path(0, &resolver).unwrap(),
6078            "character/human/male/humanmale_walk.anim"
6079        );
6080        assert_eq!(
6081            model.resolve_animation_path(1, &resolver).unwrap(),
6082            "character/human/male/humanmale_run.anim"
6083        );
6084        assert!(model.resolve_animation_path(2, &resolver).is_err()); // Out of range
6085
6086        assert_eq!(
6087            model.resolve_texture_path(0, &resolver).unwrap(),
6088            "character/textures/skin_human_male.blp"
6089        );
6090        assert_eq!(
6091            model.resolve_texture_path(1, &resolver).unwrap(),
6092            "character/textures/hair_human_male.blp"
6093        );
6094        assert!(model.resolve_texture_path(2, &resolver).is_err()); // Out of range
6095
6096        // Test loading methods (they should return errors since we don't have actual files)
6097        assert!(model.load_skin_file(0, &resolver).is_err());
6098        assert!(model.load_animation_file(0, &resolver).is_err());
6099        assert!(model.load_texture_file(0, &resolver).is_err());
6100
6101        // Test model without external files
6102        model.skin_file_ids = None;
6103        model.animation_file_ids = None;
6104        model.texture_file_ids = None;
6105
6106        // This model doesn't have advanced features initially, so it should not have external files
6107        assert!(!model.has_external_files());
6108
6109        // Just ensure the test runs properly by clearing any existing advanced features
6110        model.extended_particle_data = None;
6111        model.parent_animation_blacklist = None;
6112        model.parent_animation_data = None;
6113        model.waterfall_effect = None;
6114        model.edge_fade_data = None;
6115        model.model_alpha_data = None;
6116        model.lighting_details = None;
6117        model.recursive_particle_ids = None;
6118        model.geometry_particle_ids = None;
6119
6120        assert!(!model.has_external_files());
6121        assert_eq!(model.skin_file_count(), 0);
6122        assert_eq!(model.animation_file_count(), 0);
6123        assert_eq!(model.texture_file_count(), 0);
6124
6125        assert!(model.resolve_skin_path(0, &resolver).is_err());
6126        assert!(model.resolve_animation_path(0, &resolver).is_err());
6127        assert!(model.resolve_texture_path(0, &resolver).is_err());
6128    }
6129
6130    #[test]
6131    fn test_legacy_model_texture_handling() {
6132        use crate::chunks::texture::{M2Texture, M2TextureFlags, M2TextureType};
6133        use crate::common::{FixedString, M2Array, M2ArrayString};
6134        use crate::file_resolver::ListfileResolver;
6135        use crate::version::M2Version;
6136
6137        // Create a legacy model with embedded texture names
6138        let texture_filename = "character/textures/skin_human_male.blp";
6139        let mut filename_data = texture_filename.as_bytes().to_vec();
6140        filename_data.push(0); // Null terminator
6141
6142        let texture = M2Texture {
6143            texture_type: M2TextureType::Body,
6144            flags: M2TextureFlags::empty(),
6145            filename: M2ArrayString {
6146                array: M2Array::new(filename_data.len() as u32, 0),
6147                string: FixedString {
6148                    data: filename_data,
6149                },
6150            },
6151        };
6152
6153        let model = M2Model {
6154            header: M2Header::new(M2Version::Vanilla),
6155            name: Some("legacy_model".to_string()),
6156            global_sequences: Vec::new(),
6157            animations: Vec::new(),
6158            animation_lookup: Vec::new(),
6159            bones: Vec::new(),
6160            key_bone_lookup: Vec::new(),
6161            vertices: Vec::new(),
6162            textures: vec![texture],
6163            materials: Vec::new(),
6164            particle_emitters: Vec::new(),
6165            ribbon_emitters: Vec::new(),
6166            texture_animations: Vec::new(),
6167            color_animations: Vec::new(),
6168            transparency_animations: Vec::new(),
6169            events: Vec::new(),
6170            attachments: Vec::new(),
6171            cameras: Vec::new(),
6172            lights: Vec::new(),
6173            raw_data: M2RawData::default(),
6174            skin_file_ids: None,
6175            animation_file_ids: None,
6176            texture_file_ids: None,
6177            physics_file_id: None,
6178            skeleton_file_id: None,
6179            bone_file_ids: None,
6180            lod_data: None,
6181            extended_particle_data: None,
6182            parent_animation_blacklist: None,
6183            parent_animation_data: None,
6184            waterfall_effect: None,
6185            edge_fade_data: None,
6186            model_alpha_data: None,
6187            lighting_details: None,
6188            recursive_particle_ids: None,
6189            geometry_particle_ids: None,
6190            texture_animation_chunk: None,
6191            particle_geoset_data: None,
6192            dboc_chunk: None,
6193            afra_chunk: None,
6194            dpiv_chunk: None,
6195            parent_sequence_bounds: None,
6196            parent_event_data: None,
6197            collision_mesh_data: None,
6198            physics_file_data: None,
6199        };
6200
6201        let resolver = ListfileResolver::new();
6202
6203        // Test texture path resolution for legacy model
6204        assert_eq!(
6205            model.resolve_texture_path(0, &resolver).unwrap(),
6206            texture_filename
6207        );
6208
6209        // Test texture loading for legacy model (should fail with descriptive error)
6210        match model.load_texture_file(0, &resolver) {
6211            Err(M2Error::ExternalFileError(msg)) => {
6212                assert!(msg.contains("Cannot load pre-Legion texture"));
6213                assert!(msg.contains(texture_filename));
6214            }
6215            _ => panic!("Expected external file error for legacy texture loading"),
6216        }
6217    }
6218
6219    #[test]
6220    fn test_advanced_features() {
6221        use crate::chunks::rendering_enhancements::*;
6222        use crate::file_resolver::ListfileResolver;
6223        use crate::version::M2Version;
6224
6225        // Create a model with advanced features
6226        let mut model = M2Model {
6227            header: M2Header::new(M2Version::Legion),
6228            name: Some("advanced_model".to_string()),
6229            global_sequences: Vec::new(),
6230            animations: Vec::new(),
6231            animation_lookup: Vec::new(),
6232            bones: Vec::new(),
6233            key_bone_lookup: Vec::new(),
6234            vertices: Vec::new(),
6235            textures: Vec::new(),
6236            materials: Vec::new(),
6237            particle_emitters: Vec::new(),
6238            ribbon_emitters: Vec::new(),
6239            texture_animations: Vec::new(),
6240            color_animations: Vec::new(),
6241            transparency_animations: Vec::new(),
6242            events: Vec::new(),
6243            attachments: Vec::new(),
6244            cameras: Vec::new(),
6245            lights: Vec::new(),
6246            raw_data: M2RawData::default(),
6247            skin_file_ids: None,
6248            animation_file_ids: None,
6249            texture_file_ids: None,
6250            physics_file_id: None,
6251            skeleton_file_id: None,
6252            bone_file_ids: None,
6253            lod_data: None,
6254            extended_particle_data: Some(ExtendedParticleData {
6255                version: 1,
6256                enhanced_emitters: Vec::new(),
6257                particle_systems: Vec::new(),
6258            }),
6259            parent_animation_blacklist: Some(ParentAnimationBlacklist {
6260                blacklisted_sequences: vec![1, 5, 10],
6261            }),
6262            parent_animation_data: Some(ParentAnimationData {
6263                texture_weights: Vec::new(),
6264                blending_modes: Vec::new(),
6265            }),
6266            waterfall_effect: Some(WaterfallEffect {
6267                version: 1,
6268                parameters: WaterfallParameters {
6269                    flow_velocity: 1.0,
6270                    turbulence: 0.5,
6271                    foam_intensity: 0.75,
6272                    additional_params: Vec::new(),
6273                },
6274            }),
6275            edge_fade_data: Some(EdgeFadeData {
6276                fade_distances: vec![10.0, 20.0],
6277                fade_factors: vec![0.5, 0.8],
6278            }),
6279            model_alpha_data: Some(ModelAlphaData {
6280                alpha_test_threshold: 0.5,
6281                blend_mode: AlphaBlendMode::Normal,
6282            }),
6283            lighting_details: Some(LightingDetails {
6284                ambient_factor: 0.2,
6285                diffuse_factor: 0.8,
6286                specular_factor: 0.3,
6287            }),
6288            recursive_particle_ids: Some(RecursiveParticleIds {
6289                model_ids: vec![123456, 789012],
6290            }),
6291            geometry_particle_ids: Some(GeometryParticleIds {
6292                model_ids: vec![345678, 901234],
6293            }),
6294            texture_animation_chunk: None,
6295            particle_geoset_data: None,
6296            dboc_chunk: None,
6297            afra_chunk: None,
6298            dpiv_chunk: None,
6299            parent_sequence_bounds: None,
6300            parent_event_data: None,
6301            collision_mesh_data: None,
6302            physics_file_data: None,
6303        };
6304
6305        // Test advanced features detection
6306        assert!(model.has_advanced_features());
6307        assert!(model.has_external_files());
6308
6309        // Test animation blacklisting
6310        assert!(model.is_animation_blacklisted(1));
6311        assert!(model.is_animation_blacklisted(5));
6312        assert!(model.is_animation_blacklisted(10));
6313        assert!(!model.is_animation_blacklisted(2));
6314
6315        // Test getters
6316        assert!(model.get_extended_particle_data().is_some());
6317        assert!(model.get_parent_animation_blacklist().is_some());
6318        assert!(model.get_parent_animation_data().is_some());
6319        assert!(model.get_waterfall_effect().is_some());
6320        assert!(model.get_edge_fade_data().is_some());
6321        assert!(model.get_model_alpha_data().is_some());
6322        assert!(model.get_lighting_details().is_some());
6323
6324        assert_eq!(
6325            model.get_recursive_particle_ids(),
6326            Some([123456u32, 789012u32].as_slice())
6327        );
6328        assert_eq!(
6329            model.get_geometry_particle_ids(),
6330            Some([345678u32, 901234u32].as_slice())
6331        );
6332
6333        // Test waterfall effect version
6334        let waterfall = model.get_waterfall_effect().unwrap();
6335        assert_eq!(waterfall.version, 1);
6336        assert_eq!(waterfall.parameters.flow_velocity, 1.0);
6337
6338        // Test edge fade data
6339        let edge_fade = model.get_edge_fade_data().unwrap();
6340        assert_eq!(edge_fade.fade_distances, vec![10.0, 20.0]);
6341        assert_eq!(edge_fade.fade_factors, vec![0.5, 0.8]);
6342
6343        // Test model alpha data
6344        let alpha_data = model.get_model_alpha_data().unwrap();
6345        assert_eq!(alpha_data.alpha_test_threshold, 0.5);
6346        assert_eq!(alpha_data.blend_mode, AlphaBlendMode::Normal);
6347
6348        // Test lighting details
6349        let lighting = model.get_lighting_details().unwrap();
6350        assert_eq!(lighting.ambient_factor, 0.2);
6351        assert_eq!(lighting.diffuse_factor, 0.8);
6352        assert_eq!(lighting.specular_factor, 0.3);
6353
6354        // Test particle model loading (will fail since we don't have real resolver)
6355        let resolver = ListfileResolver::new();
6356        let result = model.load_particle_models(&resolver);
6357        assert!(result.is_ok()); // Should succeed but return empty list due to resolver not having data
6358
6359        // Clear all advanced features
6360        model.extended_particle_data = None;
6361        model.parent_animation_blacklist = None;
6362        model.parent_animation_data = None;
6363        model.waterfall_effect = None;
6364        model.edge_fade_data = None;
6365        model.model_alpha_data = None;
6366        model.lighting_details = None;
6367        model.recursive_particle_ids = None;
6368        model.geometry_particle_ids = None;
6369
6370        assert!(!model.has_advanced_features());
6371        assert!(!model.has_external_files());
6372        assert!(!model.is_animation_blacklisted(1));
6373        assert!(model.get_extended_particle_data().is_none());
6374    }
6375
6376    #[test]
6377    #[allow(clippy::field_reassign_with_default)]
6378    fn test_m2_write_read_roundtrip() {
6379        use crate::chunks::vertex::M2Vertex;
6380        use crate::common::{C2Vector, C3Vector};
6381        use crate::version::M2Version;
6382
6383        // Create a minimal but complete M2 model
6384        let mut model = M2Model::default();
6385        model.header = M2Header::new(M2Version::WotLK);
6386        model.name = Some("TestModel".to_string());
6387
6388        // Add some test vertices
6389        for i in 0..4 {
6390            let vertex = M2Vertex {
6391                position: C3Vector {
6392                    x: i as f32,
6393                    y: 0.0,
6394                    z: 0.0,
6395                },
6396                bone_weights: [255, 0, 0, 0],
6397                bone_indices: [0, 0, 0, 0],
6398                normal: C3Vector {
6399                    x: 0.0,
6400                    y: 1.0,
6401                    z: 0.0,
6402                },
6403                tex_coords: C2Vector { x: 0.0, y: 0.0 },
6404                tex_coords2: None,
6405            };
6406            model.vertices.push(vertex);
6407        }
6408
6409        // Write to bytes
6410        let mut buffer = Cursor::new(Vec::new());
6411        let write_result = model.write(&mut buffer);
6412        assert!(write_result.is_ok(), "Write should succeed");
6413
6414        // Read back
6415        buffer.set_position(0);
6416        let read_result = M2Model::parse(&mut buffer);
6417        assert!(read_result.is_ok(), "Read should succeed");
6418
6419        let read_model = read_result.unwrap();
6420
6421        // Verify key fields match
6422        assert_eq!(read_model.name, model.name, "Name should match");
6423        assert_eq!(
6424            read_model.vertices.len(),
6425            model.vertices.len(),
6426            "Vertex count should match"
6427        );
6428        assert_eq!(
6429            read_model.header.version, model.header.version,
6430            "Version should match"
6431        );
6432
6433        // Verify vertex data
6434        for (i, (orig, read)) in model
6435            .vertices
6436            .iter()
6437            .zip(read_model.vertices.iter())
6438            .enumerate()
6439        {
6440            assert_eq!(
6441                orig.position.x, read.position.x,
6442                "Vertex {} X position should match",
6443                i
6444            );
6445            assert_eq!(
6446                orig.position.y, read.position.y,
6447                "Vertex {} Y position should match",
6448                i
6449            );
6450            assert_eq!(
6451                orig.position.z, read.position.z,
6452                "Vertex {} Z position should match",
6453                i
6454            );
6455        }
6456    }
6457
6458    #[test]
6459    #[allow(clippy::field_reassign_with_default)]
6460    fn test_m2_version_conversion_roundtrip() {
6461        use crate::chunks::vertex::M2Vertex;
6462        use crate::common::{C2Vector, C3Vector};
6463        use crate::version::M2Version;
6464
6465        // Create a WotLK model
6466        let mut model = M2Model::default();
6467        model.header = M2Header::new(M2Version::WotLK);
6468        model.name = Some("ConversionTest".to_string());
6469
6470        // Add a test vertex
6471        let vertex = M2Vertex {
6472            position: C3Vector {
6473                x: 1.0,
6474                y: 2.0,
6475                z: 3.0,
6476            },
6477            bone_weights: [255, 0, 0, 0],
6478            bone_indices: [0, 0, 0, 0],
6479            normal: C3Vector {
6480                x: 0.0,
6481                y: 1.0,
6482                z: 0.0,
6483            },
6484            tex_coords: C2Vector { x: 0.5, y: 0.5 },
6485            tex_coords2: None,
6486        };
6487        model.vertices.push(vertex);
6488
6489        // Convert to TBC
6490        let convert_result = model.convert(M2Version::TBC);
6491        assert!(convert_result.is_ok(), "Conversion to TBC should succeed");
6492        let tbc_model = convert_result.unwrap();
6493
6494        // Verify conversion changed version
6495        assert_eq!(
6496            tbc_model.header.version,
6497            M2Version::TBC.to_header_version(),
6498            "Version should be TBC"
6499        );
6500
6501        // Write TBC model to bytes
6502        let mut buffer = Cursor::new(Vec::new());
6503        let write_result = tbc_model.write(&mut buffer);
6504        assert!(
6505            write_result.is_ok(),
6506            "Write of converted model should succeed"
6507        );
6508
6509        // Read back and verify
6510        buffer.set_position(0);
6511        let read_result = M2Model::parse(&mut buffer);
6512        assert!(
6513            read_result.is_ok(),
6514            "Read of converted model should succeed: {:?}",
6515            read_result.err()
6516        );
6517
6518        let read_model = read_result.unwrap();
6519        assert_eq!(
6520            read_model.header.version,
6521            M2Version::TBC.to_header_version(),
6522            "Re-read version should be TBC"
6523        );
6524        assert_eq!(read_model.vertices.len(), 1, "Should have 1 vertex");
6525    }
6526
6527    #[test]
6528    #[allow(clippy::field_reassign_with_default)]
6529    fn test_m2_cataclysm_roundtrip() {
6530        use crate::chunks::vertex::M2Vertex;
6531        use crate::common::{C2Vector, C3Vector};
6532        use crate::version::M2Version;
6533
6534        // Create a Cataclysm model (includes secondary tex coords)
6535        let mut model = M2Model::default();
6536        model.header = M2Header::new(M2Version::Cataclysm);
6537        model.name = Some("CataModel".to_string());
6538
6539        // Add a test vertex with secondary texture coordinates
6540        let vertex = M2Vertex {
6541            position: C3Vector {
6542                x: 1.0,
6543                y: 2.0,
6544                z: 3.0,
6545            },
6546            bone_weights: [255, 0, 0, 0],
6547            bone_indices: [0, 0, 0, 0],
6548            normal: C3Vector {
6549                x: 0.0,
6550                y: 1.0,
6551                z: 0.0,
6552            },
6553            tex_coords: C2Vector { x: 0.5, y: 0.5 },
6554            tex_coords2: Some(C2Vector { x: 0.25, y: 0.75 }),
6555        };
6556        model.vertices.push(vertex);
6557
6558        // Write to bytes
6559        let mut buffer = Cursor::new(Vec::new());
6560        let write_result = model.write(&mut buffer);
6561        assert!(
6562            write_result.is_ok(),
6563            "Write of Cataclysm model should succeed: {:?}",
6564            write_result.err()
6565        );
6566
6567        // Read back and verify
6568        buffer.set_position(0);
6569        let read_result = M2Model::parse(&mut buffer);
6570        assert!(
6571            read_result.is_ok(),
6572            "Read of Cataclysm model should succeed: {:?}",
6573            read_result.err()
6574        );
6575
6576        let read_model = read_result.unwrap();
6577        assert_eq!(
6578            read_model.header.version,
6579            M2Version::Cataclysm.to_header_version(),
6580            "Re-read version should be Cataclysm (272)"
6581        );
6582        assert_eq!(read_model.vertices.len(), 1, "Should have 1 vertex");
6583
6584        // Cataclysm vertices have secondary tex coords
6585        assert!(
6586            read_model.vertices[0].tex_coords2.is_some(),
6587            "Cataclysm should have secondary tex coords"
6588        );
6589    }
6590
6591    #[test]
6592    #[allow(clippy::field_reassign_with_default)]
6593    fn test_m2_mop_roundtrip() {
6594        use crate::chunks::vertex::M2Vertex;
6595        use crate::common::{C2Vector, C3Vector};
6596        use crate::version::M2Version;
6597
6598        // Create a MoP model
6599        let mut model = M2Model::default();
6600        model.header = M2Header::new(M2Version::MoP);
6601        model.name = Some("MoPModel".to_string());
6602
6603        // Add test vertices
6604        for i in 0..3 {
6605            let vertex = M2Vertex {
6606                position: C3Vector {
6607                    x: i as f32,
6608                    y: 0.0,
6609                    z: 0.0,
6610                },
6611                bone_weights: [255, 0, 0, 0],
6612                bone_indices: [0, 0, 0, 0],
6613                normal: C3Vector {
6614                    x: 0.0,
6615                    y: 1.0,
6616                    z: 0.0,
6617                },
6618                tex_coords: C2Vector { x: 0.0, y: 0.0 },
6619                tex_coords2: Some(C2Vector { x: 1.0, y: 1.0 }),
6620            };
6621            model.vertices.push(vertex);
6622        }
6623
6624        // Write to bytes
6625        let mut buffer = Cursor::new(Vec::new());
6626        let write_result = model.write(&mut buffer);
6627        assert!(
6628            write_result.is_ok(),
6629            "Write of MoP model should succeed: {:?}",
6630            write_result.err()
6631        );
6632
6633        // Read back and verify
6634        buffer.set_position(0);
6635        let read_result = M2Model::parse(&mut buffer);
6636        assert!(
6637            read_result.is_ok(),
6638            "Read of MoP model should succeed: {:?}",
6639            read_result.err()
6640        );
6641
6642        let read_model = read_result.unwrap();
6643        assert_eq!(
6644            read_model.header.version,
6645            M2Version::MoP.to_header_version(),
6646            "Re-read version should be MoP (272)"
6647        );
6648        assert_eq!(read_model.vertices.len(), 3, "Should have 3 vertices");
6649    }
6650
6651    #[test]
6652    #[allow(clippy::field_reassign_with_default)]
6653    fn test_m2_wotlk_to_cataclysm_conversion() {
6654        use crate::chunks::vertex::M2Vertex;
6655        use crate::common::{C2Vector, C3Vector};
6656        use crate::version::M2Version;
6657
6658        // Create a WotLK model
6659        let mut model = M2Model::default();
6660        model.header = M2Header::new(M2Version::WotLK);
6661        model.name = Some("WotLKToCata".to_string());
6662
6663        let vertex = M2Vertex {
6664            position: C3Vector {
6665                x: 1.0,
6666                y: 2.0,
6667                z: 3.0,
6668            },
6669            bone_weights: [255, 0, 0, 0],
6670            bone_indices: [0, 0, 0, 0],
6671            normal: C3Vector {
6672                x: 0.0,
6673                y: 1.0,
6674                z: 0.0,
6675            },
6676            tex_coords: C2Vector { x: 0.5, y: 0.5 },
6677            tex_coords2: None,
6678        };
6679        model.vertices.push(vertex);
6680
6681        // Convert to Cataclysm
6682        let converted = model
6683            .convert(M2Version::Cataclysm)
6684            .expect("WotLK -> Cataclysm conversion failed");
6685
6686        assert_eq!(
6687            converted.header.version,
6688            M2Version::Cataclysm.to_header_version(),
6689            "Version should be Cataclysm (272)"
6690        );
6691
6692        // Write and re-read
6693        let mut buffer = Cursor::new(Vec::new());
6694        converted.write(&mut buffer).expect("Write failed");
6695
6696        buffer.set_position(0);
6697        let read_model = M2Model::parse(&mut buffer).expect("Read failed");
6698
6699        assert_eq!(
6700            read_model.header.version,
6701            M2Version::Cataclysm.to_header_version()
6702        );
6703        assert_eq!(read_model.vertices.len(), 1);
6704    }
6705
6706    #[test]
6707    #[allow(clippy::field_reassign_with_default)]
6708    fn test_m2_wotlk_to_mop_conversion() {
6709        use crate::chunks::vertex::M2Vertex;
6710        use crate::common::{C2Vector, C3Vector};
6711        use crate::version::M2Version;
6712
6713        let mut model = M2Model::default();
6714        model.header = M2Header::new(M2Version::WotLK);
6715        model.name = Some("WotLKToMoP".to_string());
6716
6717        let vertex = M2Vertex {
6718            position: C3Vector {
6719                x: 1.0,
6720                y: 2.0,
6721                z: 3.0,
6722            },
6723            bone_weights: [255, 0, 0, 0],
6724            bone_indices: [0, 0, 0, 0],
6725            normal: C3Vector {
6726                x: 0.0,
6727                y: 1.0,
6728                z: 0.0,
6729            },
6730            tex_coords: C2Vector { x: 0.5, y: 0.5 },
6731            tex_coords2: None,
6732        };
6733        model.vertices.push(vertex);
6734
6735        // Convert to MoP
6736        let converted = model
6737            .convert(M2Version::MoP)
6738            .expect("WotLK -> MoP conversion failed");
6739
6740        assert_eq!(converted.header.version, M2Version::MoP.to_header_version());
6741
6742        // Write and re-read
6743        let mut buffer = Cursor::new(Vec::new());
6744        converted.write(&mut buffer).expect("Write failed");
6745
6746        buffer.set_position(0);
6747        let read_model = M2Model::parse(&mut buffer).expect("Read failed");
6748
6749        assert_eq!(
6750            read_model.header.version,
6751            M2Version::MoP.to_header_version()
6752        );
6753        assert_eq!(read_model.vertices.len(), 1);
6754    }
6755
6756    #[test]
6757    #[allow(clippy::field_reassign_with_default)]
6758    fn test_m2_wotlk_to_vanilla_conversion() {
6759        use crate::chunks::vertex::M2Vertex;
6760        use crate::common::{C2Vector, C3Vector};
6761        use crate::version::M2Version;
6762
6763        let mut model = M2Model::default();
6764        model.header = M2Header::new(M2Version::WotLK);
6765        model.name = Some("WotLKToVanilla".to_string());
6766
6767        let vertex = M2Vertex {
6768            position: C3Vector {
6769                x: 1.0,
6770                y: 2.0,
6771                z: 3.0,
6772            },
6773            bone_weights: [255, 0, 0, 0],
6774            bone_indices: [0, 0, 0, 0],
6775            normal: C3Vector {
6776                x: 0.0,
6777                y: 1.0,
6778                z: 0.0,
6779            },
6780            tex_coords: C2Vector { x: 0.5, y: 0.5 },
6781            tex_coords2: None,
6782        };
6783        model.vertices.push(vertex);
6784
6785        // Convert to Vanilla
6786        let converted = model
6787            .convert(M2Version::Vanilla)
6788            .expect("WotLK -> Vanilla conversion failed");
6789
6790        assert_eq!(
6791            converted.header.version,
6792            M2Version::Vanilla.to_header_version()
6793        );
6794
6795        // Write and re-read
6796        let mut buffer = Cursor::new(Vec::new());
6797        converted.write(&mut buffer).expect("Write failed");
6798
6799        buffer.set_position(0);
6800        let read_model = M2Model::parse(&mut buffer).expect("Read failed");
6801
6802        assert_eq!(
6803            read_model.header.version,
6804            M2Version::Vanilla.to_header_version()
6805        );
6806        assert_eq!(read_model.vertices.len(), 1);
6807    }
6808
6809    #[test]
6810    #[allow(clippy::field_reassign_with_default)]
6811    fn test_m2_cataclysm_to_wotlk_conversion() {
6812        use crate::chunks::vertex::M2Vertex;
6813        use crate::common::{C2Vector, C3Vector};
6814        use crate::version::M2Version;
6815
6816        let mut model = M2Model::default();
6817        model.header = M2Header::new(M2Version::Cataclysm);
6818        model.name = Some("CataToWotLK".to_string());
6819
6820        let vertex = M2Vertex {
6821            position: C3Vector {
6822                x: 1.0,
6823                y: 2.0,
6824                z: 3.0,
6825            },
6826            bone_weights: [255, 0, 0, 0],
6827            bone_indices: [0, 0, 0, 0],
6828            normal: C3Vector {
6829                x: 0.0,
6830                y: 1.0,
6831                z: 0.0,
6832            },
6833            tex_coords: C2Vector { x: 0.5, y: 0.5 },
6834            tex_coords2: Some(C2Vector { x: 0.25, y: 0.75 }),
6835        };
6836        model.vertices.push(vertex);
6837
6838        // Convert to WotLK
6839        let converted = model
6840            .convert(M2Version::WotLK)
6841            .expect("Cataclysm -> WotLK conversion failed");
6842
6843        assert_eq!(
6844            converted.header.version,
6845            M2Version::WotLK.to_header_version()
6846        );
6847
6848        // Write and re-read
6849        let mut buffer = Cursor::new(Vec::new());
6850        converted.write(&mut buffer).expect("Write failed");
6851
6852        buffer.set_position(0);
6853        let read_model = M2Model::parse(&mut buffer).expect("Read failed");
6854
6855        assert_eq!(
6856            read_model.header.version,
6857            M2Version::WotLK.to_header_version()
6858        );
6859        assert_eq!(read_model.vertices.len(), 1);
6860
6861        // Note: tex_coords2 is present in ALL versions (48-byte vertex format)
6862        // The secondary texture coords are preserved during conversion
6863        assert!(read_model.vertices[0].tex_coords2.is_some());
6864    }
6865}