1use crate::io_ext::{ReadExt, WriteExt};
2use std::fs::File;
3use std::io::{Read, Seek, SeekFrom, Write};
4use std::path::Path;
5
6use crate::common::{C3Vector, Quaternion};
7use crate::error::{M2Error, Result};
8use crate::version::M2Version;
9
10pub const ANIM_MAGIC: [u8; 4] = *b"MAOF";
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum AnimFormat {
20 Legacy,
27 Modern,
34}
35
36pub struct AnimFormatDetector;
38
39impl AnimFormatDetector {
40 pub fn detect_format<R: Read + Seek>(reader: &mut R) -> Result<AnimFormat> {
54 let initial_pos = reader.stream_position().map_err(M2Error::Io)?;
55
56 let mut magic = [0u8; 4];
58 match reader.read_exact(&mut magic) {
59 Ok(()) => {
60 reader
62 .seek(SeekFrom::Start(initial_pos))
63 .map_err(M2Error::Io)?;
64
65 if magic == ANIM_MAGIC {
66 Ok(AnimFormat::Modern)
67 } else {
68 Ok(AnimFormat::Legacy)
71 }
72 }
73 Err(e) => {
74 reader
76 .seek(SeekFrom::Start(initial_pos))
77 .map_err(|_| M2Error::Io(e))?;
78 Err(M2Error::AnimFormatError(
79 "File too small to determine ANIM format - need at least 4 bytes".to_string(),
80 ))
81 }
82 }
83 }
84
85 pub fn detect_format_by_version(version: M2Version) -> AnimFormat {
87 match version {
88 M2Version::Vanilla
90 | M2Version::TBC
91 | M2Version::WotLK
92 | M2Version::Cataclysm
93 | M2Version::MoP
94 | M2Version::WoD => AnimFormat::Legacy,
95 _ => AnimFormat::Modern,
97 }
98 }
99}
100
101#[derive(Debug, Clone)]
103pub struct AnimHeader {
104 pub magic: [u8; 4],
106 pub version: u32,
108 pub id_count: u32,
110 pub unknown: u32,
112 pub anim_entry_offset: u32,
114}
115
116impl AnimHeader {
117 pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
119 let mut magic = [0u8; 4];
121 reader.read_exact(&mut magic)?;
122
123 if magic != ANIM_MAGIC {
124 return Err(M2Error::InvalidMagic {
125 expected: String::from_utf8_lossy(&ANIM_MAGIC).to_string(),
126 actual: String::from_utf8_lossy(&magic).to_string(),
127 });
128 }
129
130 let version = reader.read_u32_le()?;
132 let id_count = reader.read_u32_le()?;
133 let unknown = reader.read_u32_le()?;
134 let anim_entry_offset = reader.read_u32_le()?;
135
136 Ok(Self {
137 magic,
138 version,
139 id_count,
140 unknown,
141 anim_entry_offset,
142 })
143 }
144
145 pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
147 writer.write_all(&self.magic)?;
148 writer.write_u32_le(self.version)?;
149 writer.write_u32_le(self.id_count)?;
150 writer.write_u32_le(self.unknown)?;
151 writer.write_u32_le(self.anim_entry_offset)?;
152
153 Ok(())
154 }
155}
156
157#[derive(Debug, Clone)]
159pub struct AnimEntry {
160 pub id: u32,
162 pub offset: u32,
164 pub size: u32,
166}
167
168impl AnimEntry {
169 pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
171 let id = reader.read_u32_le()?;
172 let offset = reader.read_u32_le()?;
173 let size = reader.read_u32_le()?;
174
175 Ok(Self { id, offset, size })
176 }
177
178 pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
180 writer.write_u32_le(self.id)?;
181 writer.write_u32_le(self.offset)?;
182 writer.write_u32_le(self.size)?;
183
184 Ok(())
185 }
186}
187
188#[derive(Debug, Clone)]
190pub struct AnimSectionHeader {
191 pub magic: [u8; 4],
193 pub id: u32,
195 pub start: u32,
197 pub end: u32,
199}
200
201impl AnimSectionHeader {
202 pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
204 let mut magic = [0u8; 4];
205 reader.read_exact(&mut magic)?;
206
207 if magic != *b"AFID" {
208 return Err(M2Error::InvalidMagic {
209 expected: "AFID".to_string(),
210 actual: String::from_utf8_lossy(&magic).to_string(),
211 });
212 }
213
214 let id = reader.read_u32_le()?;
215 let start = reader.read_u32_le()?;
216 let end = reader.read_u32_le()?;
217
218 Ok(Self {
219 magic,
220 id,
221 start,
222 end,
223 })
224 }
225
226 pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
228 writer.write_all(&self.magic)?;
229 writer.write_u32_le(self.id)?;
230 writer.write_u32_le(self.start)?;
231 writer.write_u32_le(self.end)?;
232
233 Ok(())
234 }
235}
236
237#[derive(Debug, Clone)]
239pub struct AnimTranslation {
240 pub timestamps: Vec<u32>,
242 pub translations: Vec<C3Vector>,
244}
245
246#[derive(Debug, Clone)]
248pub struct AnimRotation {
249 pub timestamps: Vec<u32>,
251 pub rotations: Vec<Quaternion>,
253}
254
255#[derive(Debug, Clone)]
257pub struct AnimScaling {
258 pub timestamps: Vec<u32>,
260 pub scalings: Vec<C3Vector>,
262}
263
264#[derive(Debug, Clone)]
266pub struct AnimBoneAnimation {
267 pub bone_id: u32,
269 pub translation: Option<AnimTranslation>,
271 pub rotation: Option<AnimRotation>,
273 pub scaling: Option<AnimScaling>,
275}
276
277#[derive(Debug, Clone)]
279pub struct AnimSection {
280 pub header: AnimSectionHeader,
282 pub bone_animations: Vec<AnimBoneAnimation>,
284}
285
286impl AnimSection {
287 pub fn parse<R: Read>(reader: &mut R, size: u32) -> Result<Self> {
289 let header = AnimSectionHeader::parse(reader)?;
290
291 let header_size = 16; let remaining_size = size - header_size;
294 let bone_count = remaining_size / 4; let mut bone_offsets = Vec::with_capacity(bone_count as usize);
298 for _ in 0..bone_count {
299 bone_offsets.push(reader.read_u32_le()?);
300 }
301
302 let mut bone_animations = Vec::with_capacity(bone_count as usize);
304
305 for &offset in &bone_offsets {
306 if offset > 0 {
307 let bone_id = reader.read_u32_le()?;
309
310 let flags = reader.read_u32_le()?;
312
313 let translation = if (flags & 0x1) != 0 {
315 let timestamp_count = reader.read_u32_le()?;
316
317 let mut timestamps = Vec::with_capacity(timestamp_count as usize);
318 for _ in 0..timestamp_count {
319 timestamps.push(reader.read_u32_le()?);
320 }
321
322 let mut translations = Vec::with_capacity(timestamp_count as usize);
323 for _ in 0..timestamp_count {
324 translations.push(C3Vector::parse(reader)?);
325 }
326
327 Some(AnimTranslation {
328 timestamps,
329 translations,
330 })
331 } else {
332 None
333 };
334
335 let rotation = if (flags & 0x2) != 0 {
337 let timestamp_count = reader.read_u32_le()?;
338
339 let mut timestamps = Vec::with_capacity(timestamp_count as usize);
340 for _ in 0..timestamp_count {
341 timestamps.push(reader.read_u32_le()?);
342 }
343
344 let mut rotations = Vec::with_capacity(timestamp_count as usize);
345 for _ in 0..timestamp_count {
346 rotations.push(Quaternion::parse(reader)?);
347 }
348
349 Some(AnimRotation {
350 timestamps,
351 rotations,
352 })
353 } else {
354 None
355 };
356
357 let scaling = if (flags & 0x4) != 0 {
359 let timestamp_count = reader.read_u32_le()?;
360
361 let mut timestamps = Vec::with_capacity(timestamp_count as usize);
362 for _ in 0..timestamp_count {
363 timestamps.push(reader.read_u32_le()?);
364 }
365
366 let mut scalings = Vec::with_capacity(timestamp_count as usize);
367 for _ in 0..timestamp_count {
368 scalings.push(C3Vector::parse(reader)?);
369 }
370
371 Some(AnimScaling {
372 timestamps,
373 scalings,
374 })
375 } else {
376 None
377 };
378
379 bone_animations.push(AnimBoneAnimation {
380 bone_id,
381 translation,
382 rotation,
383 scaling,
384 });
385 } else {
386 bone_animations.push(AnimBoneAnimation {
388 bone_id: 0,
389 translation: None,
390 rotation: None,
391 scaling: None,
392 });
393 }
394 }
395
396 Ok(Self {
397 header,
398 bone_animations,
399 })
400 }
401
402 pub fn write<W: Write + Seek>(&self, writer: &mut W) -> Result<()> {
404 self.header.write(writer)?;
406
407 let bone_offsets_pos = writer.stream_position()?;
409
410 for _ in 0..self.bone_animations.len() {
411 writer.write_u32_le(0)?; }
413
414 let mut bone_offsets = Vec::with_capacity(self.bone_animations.len());
416
417 for bone_animation in &self.bone_animations {
418 if bone_animation.translation.is_some()
419 || bone_animation.rotation.is_some()
420 || bone_animation.scaling.is_some()
421 {
422 let offset = writer.stream_position()? as u32;
424 bone_offsets.push(offset);
425
426 writer.write_u32_le(bone_animation.bone_id)?;
428
429 let mut flags = 0u32;
431 if bone_animation.translation.is_some() {
432 flags |= 0x1;
433 }
434 if bone_animation.rotation.is_some() {
435 flags |= 0x2;
436 }
437 if bone_animation.scaling.is_some() {
438 flags |= 0x4;
439 }
440
441 writer.write_u32_le(flags)?;
443
444 if let Some(ref translation) = bone_animation.translation {
446 writer.write_u32_le(translation.timestamps.len() as u32)?;
447
448 for ×tamp in &translation.timestamps {
449 writer.write_u32_le(timestamp)?;
450 }
451
452 for translation in &translation.translations {
453 translation.write(writer)?;
454 }
455 }
456
457 if let Some(ref rotation) = bone_animation.rotation {
459 writer.write_u32_le(rotation.timestamps.len() as u32)?;
460
461 for ×tamp in &rotation.timestamps {
462 writer.write_u32_le(timestamp)?;
463 }
464
465 for rotation in &rotation.rotations {
466 rotation.write(writer)?;
467 }
468 }
469
470 if let Some(ref scaling) = bone_animation.scaling {
472 writer.write_u32_le(scaling.timestamps.len() as u32)?;
473
474 for ×tamp in &scaling.timestamps {
475 writer.write_u32_le(timestamp)?;
476 }
477
478 for scaling in &scaling.scalings {
479 scaling.write(writer)?;
480 }
481 }
482 } else {
483 bone_offsets.push(0);
485 }
486 }
487
488 let current_pos = writer.stream_position()?;
490 writer.seek(SeekFrom::Start(bone_offsets_pos))?;
491
492 for &offset in &bone_offsets {
493 writer.write_u32_le(offset)?;
494 }
495
496 writer.seek(SeekFrom::Start(current_pos))?;
498
499 Ok(())
500 }
501}
502
503#[derive(Debug, Clone)]
505pub enum AnimMetadata {
506 Legacy {
507 file_size: u32,
509 animation_count: u32,
511 structure_hints: LegacyStructureHints,
513 },
514 Modern {
515 header: AnimHeader,
517 entries: Vec<AnimEntry>,
519 },
520}
521
522#[derive(Debug, Clone)]
524pub struct LegacyStructureHints {
525 pub appears_valid: bool,
527 pub estimated_blocks: u32,
529 pub has_timestamps: bool,
531}
532
533#[derive(Debug, Clone, Default)]
535pub struct MemoryUsage {
536 pub sections: usize,
538 pub bone_animations: usize,
540 pub translation_keyframes: usize,
542 pub rotation_keyframes: usize,
544 pub scaling_keyframes: usize,
546 pub approximate_bytes: usize,
548}
549
550impl MemoryUsage {
551 pub fn new() -> Self {
553 Self::default()
554 }
555
556 pub fn calculate_approximate_bytes(&self) -> usize {
558 let mut bytes = 0;
559
560 bytes += self.sections * std::mem::size_of::<AnimSectionHeader>();
562
563 bytes += self.bone_animations * std::mem::size_of::<AnimBoneAnimation>();
565
566 bytes += self.translation_keyframes
568 * (std::mem::size_of::<u32>() + std::mem::size_of::<C3Vector>());
569 bytes += self.rotation_keyframes
570 * (std::mem::size_of::<u32>() + std::mem::size_of::<Quaternion>());
571 bytes +=
572 self.scaling_keyframes * (std::mem::size_of::<u32>() + std::mem::size_of::<C3Vector>());
573
574 bytes
575 }
576
577 pub fn total_keyframes(&self) -> usize {
579 self.translation_keyframes + self.rotation_keyframes + self.scaling_keyframes
580 }
581}
582
583#[derive(Debug, Clone)]
585pub struct AnimFile {
586 pub format: AnimFormat,
588 pub sections: Vec<AnimSection>,
590 pub metadata: AnimMetadata,
592}
593
594pub struct AnimParser;
596
597impl AnimParser {
598 pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<AnimFile> {
600 let format = AnimFormatDetector::detect_format(reader)?;
601
602 match format {
603 AnimFormat::Legacy => Self::parse_legacy(reader),
604 AnimFormat::Modern => Self::parse_modern(reader),
605 }
606 }
607
608 pub fn parse_with_format<R: Read + Seek>(
610 reader: &mut R,
611 format: AnimFormat,
612 ) -> Result<AnimFile> {
613 match format {
614 AnimFormat::Legacy => Self::parse_legacy(reader),
615 AnimFormat::Modern => Self::parse_modern(reader),
616 }
617 }
618
619 fn parse_legacy<R: Read + Seek>(reader: &mut R) -> Result<AnimFile> {
621 let file_size = reader.seek(SeekFrom::End(0))? as u32;
623 reader.seek(SeekFrom::Start(0))?;
624
625 let mut header_bytes = [0u8; 16];
636 reader.read_exact(&mut header_bytes)?;
637 reader.seek(SeekFrom::Start(0))?;
638
639 let _first_value = u32::from_le_bytes([
641 header_bytes[0],
642 header_bytes[1],
643 header_bytes[2],
644 header_bytes[3],
645 ]);
646
647 let animation_id = Self::extract_anim_id_from_legacy_data(&header_bytes);
650
651 let structure_hints = Self::analyze_legacy_structure(reader, file_size)?;
653
654 let sections = vec![Self::create_legacy_animation_section(
657 reader,
658 animation_id,
659 file_size,
660 )?];
661
662 Ok(AnimFile {
663 format: AnimFormat::Legacy,
664 sections,
665 metadata: AnimMetadata::Legacy {
666 file_size,
667 animation_count: 1, structure_hints,
669 },
670 })
671 }
672
673 fn extract_anim_id_from_legacy_data(_header_bytes: &[u8; 16]) -> u32 {
675 1
680 }
681
682 fn create_legacy_animation_section<R: Read + Seek>(
684 reader: &mut R,
685 animation_id: u32,
686 _file_size: u32,
687 ) -> Result<AnimSection> {
688 reader.seek(SeekFrom::Start(0))?;
690
691 let header = AnimSectionHeader {
694 magic: *b"AFID",
695 id: animation_id,
696 start: 0,
697 end: 0, };
699
700 let bone_animations = Vec::new();
704
705 Ok(AnimSection {
706 header,
707 bone_animations,
708 })
709 }
710
711 fn analyze_legacy_structure<R: Read + Seek>(
713 reader: &mut R,
714 file_size: u32,
715 ) -> Result<LegacyStructureHints> {
716 reader.seek(SeekFrom::Start(0))?;
717
718 let mut appears_valid = true;
719 let mut estimated_blocks = 0;
720 let mut has_timestamps = false;
721
722 let mut buffer = vec![0u8; (file_size as usize).min(1024)];
724 let bytes_read = reader.read(&mut buffer)?;
725
726 if bytes_read < 16 {
727 appears_valid = false;
728 } else {
729 let u32_values: Vec<u32> = buffer
732 .chunks_exact(4)
733 .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
734 .collect();
735
736 for window in u32_values.windows(3) {
737 if window.len() == 3 {
738 let (val1, val2, val3) = (window[0], window[1], window[2]);
739
740 if val1 < val2 && val2 < val3 && val1 < 100000 {
742 has_timestamps = true;
743 estimated_blocks += 1;
744 }
745 }
746 }
747
748 if estimated_blocks == 0 {
750 estimated_blocks = (file_size / 1000).max(1); }
752 }
753
754 reader.seek(SeekFrom::Start(0))?; Ok(LegacyStructureHints {
757 appears_valid,
758 estimated_blocks,
759 has_timestamps,
760 })
761 }
762
763 fn parse_modern<R: Read + Seek>(reader: &mut R) -> Result<AnimFile> {
765 let header = AnimHeader::parse(reader)?;
767
768 reader.seek(SeekFrom::Start(header.anim_entry_offset as u64))?;
770
771 let mut entries = Vec::with_capacity(header.id_count as usize);
772 for _ in 0..header.id_count {
773 entries.push(AnimEntry::parse(reader)?);
774 }
775
776 let mut sections = Vec::with_capacity(entries.len());
778
779 for entry in &entries {
780 reader.seek(SeekFrom::Start(entry.offset as u64))?;
781 sections.push(AnimSection::parse(reader, entry.size)?);
782 }
783
784 Ok(AnimFile {
785 format: AnimFormat::Modern,
786 sections,
787 metadata: AnimMetadata::Modern { header, entries },
788 })
789 }
790}
791
792impl AnimFile {
793 pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
795 AnimParser::parse(reader)
796 }
797
798 pub fn parse_validated<R: Read + Seek>(reader: &mut R) -> Result<Self> {
800 let anim_file = Self::parse(reader)?;
801 anim_file.validate()?;
802 Ok(anim_file)
803 }
804
805 pub fn validate(&self) -> Result<()> {
807 if self.sections.is_empty() {
809 return Err(M2Error::ValidationError(
810 "ANIM file must contain at least one section".to_string(),
811 ));
812 }
813
814 match (&self.format, &self.metadata) {
816 (
817 AnimFormat::Legacy,
818 AnimMetadata::Legacy {
819 structure_hints, ..
820 },
821 ) => {
822 if !structure_hints.appears_valid {
823 return Err(M2Error::ValidationError(
824 "Legacy ANIM file structure appears invalid".to_string(),
825 ));
826 }
827 }
828 (AnimFormat::Modern, AnimMetadata::Modern { header, entries }) => {
829 if header.id_count as usize != entries.len() {
830 return Err(M2Error::ValidationError(format!(
831 "Header ID count ({}) doesn't match entries count ({})",
832 header.id_count,
833 entries.len()
834 )));
835 }
836
837 if header.id_count as usize != self.sections.len() {
838 return Err(M2Error::ValidationError(format!(
839 "Header ID count ({}) doesn't match sections count ({})",
840 header.id_count,
841 self.sections.len()
842 )));
843 }
844 }
845 _ => {
846 return Err(M2Error::ValidationError(
847 "Format and metadata type mismatch".to_string(),
848 ));
849 }
850 }
851
852 Ok(())
853 }
854
855 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
857 let mut file = File::open(path)?;
858 Self::parse(&mut file)
859 }
860
861 pub fn load_with_version<P: AsRef<Path>>(path: P, version: M2Version) -> Result<Self> {
863 let mut file = File::open(path)?;
864 let format = AnimFormatDetector::detect_format_by_version(version);
865 AnimParser::parse_with_format(&mut file, format)
866 }
867
868 pub fn parse_with_format<R: Read + Seek>(reader: &mut R, format: AnimFormat) -> Result<Self> {
870 AnimParser::parse_with_format(reader, format)
871 }
872
873 pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
875 let mut file = File::create(path)?;
876 self.write(&mut file)
877 }
878
879 pub fn write<W: Write + Seek>(&self, writer: &mut W) -> Result<()> {
881 match self.format {
882 AnimFormat::Modern => self.write_modern(writer),
883 AnimFormat::Legacy => self.write_legacy(writer),
884 }
885 }
886
887 fn write_modern<W: Write + Seek>(&self, writer: &mut W) -> Result<()> {
889 let (header, entries) = match &self.metadata {
890 AnimMetadata::Modern { header, entries } => (header, entries),
891 _ => {
892 return Err(M2Error::InternalError(
893 "Attempting to write modern format with legacy metadata".to_string(),
894 ));
895 }
896 };
897
898 let header_size = 20; let entry_size = 12; let entry_offset = header_size;
903 let _section_offset = entry_offset + entries.len() as u32 * entry_size;
904
905 let mut header = header.clone();
907 header.anim_entry_offset = entry_offset;
908 header.write(writer)?;
909
910 let mut updated_entries = Vec::with_capacity(entries.len());
912
913 for entry in entries {
914 let entry = AnimEntry {
915 id: entry.id,
916 offset: 0, size: 0, };
919
920 entry.write(writer)?;
921 updated_entries.push(entry);
922 }
923
924 for (i, section) in self.sections.iter().enumerate() {
926 let section_start = writer.stream_position()? as u32;
927 section.write(writer)?;
928 let section_end = writer.stream_position()? as u32;
929
930 updated_entries[i].offset = section_start;
931 updated_entries[i].size = section_end - section_start;
932 }
933
934 writer.seek(SeekFrom::Start(entry_offset as u64))?;
936
937 for entry in &updated_entries {
938 entry.write(writer)?;
939 }
940
941 Ok(())
942 }
943
944 fn write_legacy<W: Write + Seek>(&self, writer: &mut W) -> Result<()> {
946 writer.write_u32_le(self.sections.len() as u32)?;
951
952 let offsets_pos = writer.stream_position()?;
954 for _ in 0..self.sections.len() {
955 writer.write_u32_le(0)?; }
957
958 let mut offsets = Vec::with_capacity(self.sections.len());
960
961 for section in &self.sections {
962 let offset = writer.stream_position()? as u32;
963 offsets.push(offset);
964
965 writer.write_u32_le(section.header.id)?;
967 writer.write_u32_le(section.header.start)?;
968 writer.write_u32_le(section.header.end)?;
969
970 writer.write_u32_le(section.bone_animations.len() as u32)?;
972
973 for bone_anim in §ion.bone_animations {
975 writer.write_u32_le(bone_anim.bone_id)?;
976
977 let mut flags = 0u32;
979 if bone_anim.translation.is_some() {
980 flags |= 0x1;
981 }
982 if bone_anim.rotation.is_some() {
983 flags |= 0x2;
984 }
985 if bone_anim.scaling.is_some() {
986 flags |= 0x4;
987 }
988 writer.write_u32_le(flags)?;
989
990 if let Some(ref translation) = bone_anim.translation {
992 writer.write_u32_le(translation.timestamps.len() as u32)?;
993 for ×tamp in &translation.timestamps {
994 writer.write_u32_le(timestamp)?;
995 }
996 for translation in &translation.translations {
997 translation.write(writer)?;
998 }
999 }
1000
1001 if let Some(ref rotation) = bone_anim.rotation {
1002 writer.write_u32_le(rotation.timestamps.len() as u32)?;
1003 for ×tamp in &rotation.timestamps {
1004 writer.write_u32_le(timestamp)?;
1005 }
1006 for rotation in &rotation.rotations {
1007 rotation.write(writer)?;
1008 }
1009 }
1010
1011 if let Some(ref scaling) = bone_anim.scaling {
1012 writer.write_u32_le(scaling.timestamps.len() as u32)?;
1013 for ×tamp in &scaling.timestamps {
1014 writer.write_u32_le(timestamp)?;
1015 }
1016 for scaling in &scaling.scalings {
1017 scaling.write(writer)?;
1018 }
1019 }
1020 }
1021 }
1022
1023 let current_pos = writer.stream_position()?;
1025 writer.seek(SeekFrom::Start(offsets_pos))?;
1026 for offset in offsets {
1027 writer.write_u32_le(offset)?;
1028 }
1029 writer.seek(SeekFrom::Start(current_pos))?;
1030
1031 Ok(())
1032 }
1033
1034 pub fn convert(&self, target_version: M2Version) -> Self {
1036 let target_format = AnimFormatDetector::detect_format_by_version(target_version);
1037
1038 if target_format == self.format {
1039 return self.clone();
1041 }
1042
1043 match (self.format, target_format) {
1045 (AnimFormat::Legacy, AnimFormat::Modern) => {
1046 let header = AnimHeader {
1048 magic: ANIM_MAGIC,
1049 version: 1,
1050 id_count: self.sections.len() as u32,
1051 unknown: 0,
1052 anim_entry_offset: 20,
1053 };
1054
1055 let entries: Vec<AnimEntry> = self
1056 .sections
1057 .iter()
1058 .map(|section| {
1059 AnimEntry {
1060 id: section.header.id,
1061 offset: 0, size: 0, }
1064 })
1065 .collect();
1066
1067 AnimFile {
1068 format: AnimFormat::Modern,
1069 sections: self.sections.clone(),
1070 metadata: AnimMetadata::Modern { header, entries },
1071 }
1072 }
1073 (AnimFormat::Modern, AnimFormat::Legacy) => {
1074 AnimFile {
1076 format: AnimFormat::Legacy,
1077 sections: self.sections.clone(),
1078 metadata: AnimMetadata::Legacy {
1079 file_size: 0, animation_count: self.sections.len() as u32,
1081 structure_hints: LegacyStructureHints {
1082 appears_valid: true,
1083 estimated_blocks: self.sections.len() as u32,
1084 has_timestamps: false, },
1086 },
1087 }
1088 }
1089 _ => self.clone(), }
1091 }
1092
1093 pub fn animation_count(&self) -> u32 {
1095 self.sections.len() as u32
1096 }
1097
1098 pub fn is_legacy_format(&self) -> bool {
1100 matches!(self.format, AnimFormat::Legacy)
1101 }
1102
1103 pub fn is_modern_format(&self) -> bool {
1105 matches!(self.format, AnimFormat::Modern)
1106 }
1107
1108 pub fn memory_usage(&self) -> MemoryUsage {
1110 let mut usage = MemoryUsage::new();
1111
1112 for section in &self.sections {
1114 usage.sections += 1;
1115 usage.bone_animations += section.bone_animations.len();
1116
1117 for bone_anim in §ion.bone_animations {
1118 if let Some(ref translation) = bone_anim.translation {
1119 usage.translation_keyframes += translation.timestamps.len();
1120 }
1121 if let Some(ref rotation) = bone_anim.rotation {
1122 usage.rotation_keyframes += rotation.timestamps.len();
1123 }
1124 if let Some(ref scaling) = bone_anim.scaling {
1125 usage.scaling_keyframes += scaling.timestamps.len();
1126 }
1127 }
1128 }
1129
1130 usage.approximate_bytes = usage.calculate_approximate_bytes();
1132
1133 usage
1134 }
1135
1136 pub fn optimize_memory(&mut self) {
1138 for section in &mut self.sections {
1143 section.bone_animations.retain(|bone_anim| {
1145 bone_anim.translation.is_some()
1146 || bone_anim.rotation.is_some()
1147 || bone_anim.scaling.is_some()
1148 });
1149
1150 section.bone_animations.shrink_to_fit();
1152
1153 for bone_anim in &mut section.bone_animations {
1154 if let Some(ref mut translation) = bone_anim.translation {
1155 translation.timestamps.shrink_to_fit();
1156 translation.translations.shrink_to_fit();
1157 }
1158 if let Some(ref mut rotation) = bone_anim.rotation {
1159 rotation.timestamps.shrink_to_fit();
1160 rotation.rotations.shrink_to_fit();
1161 }
1162 if let Some(ref mut scaling) = bone_anim.scaling {
1163 scaling.timestamps.shrink_to_fit();
1164 scaling.scalings.shrink_to_fit();
1165 }
1166 }
1167 }
1168 }
1169}
1170
1171mod legacy_utils {
1173 use super::*;
1174
1175 pub fn validate_legacy_structure<R: Read + Seek>(
1177 reader: &mut R,
1178 animation_count: u32,
1179 ) -> Result<bool> {
1180 if animation_count == 0 || animation_count > 10000 {
1181 return Ok(false);
1182 }
1183
1184 let file_size = reader.seek(SeekFrom::End(0))?;
1185 reader.seek(SeekFrom::Start(0))?;
1186
1187 let min_size = 4 + (animation_count * 4) + (animation_count * 16);
1189 if file_size < min_size as u64 {
1190 return Ok(false);
1191 }
1192
1193 reader.seek(SeekFrom::Start(4))?;
1195
1196 for _ in 0..animation_count {
1198 let offset = reader.read_u32_le()?;
1199 if offset > 0 && (offset as u64) >= file_size {
1200 return Ok(false);
1201 }
1202 }
1203
1204 Ok(true)
1205 }
1206
1207 #[allow(dead_code)]
1209 pub fn estimate_animation_count<R: Read + Seek>(reader: &mut R) -> Result<u32> {
1210 let file_size = reader.seek(SeekFrom::End(0))? as u32;
1211 reader.seek(SeekFrom::Start(0))?;
1212
1213 let potential_count = reader.read_u32_le()?;
1214
1215 if validate_legacy_structure(reader, potential_count)? {
1217 Ok(potential_count)
1218 } else {
1219 let estimated = file_size / 1000; Ok(estimated.clamp(1, 100))
1224 }
1225 }
1226}
1227
1228#[cfg(test)]
1229mod tests {
1230 use super::*;
1231 use std::io::Cursor;
1232
1233 #[test]
1234 fn test_anim_header_parse_write() {
1235 let header = AnimHeader {
1236 magic: ANIM_MAGIC,
1237 version: 1,
1238 id_count: 2,
1239 unknown: 0,
1240 anim_entry_offset: 20,
1241 };
1242
1243 let mut data = Vec::new();
1244 header.write(&mut data).unwrap();
1245
1246 let mut cursor = Cursor::new(data);
1247 let parsed_header = AnimHeader::parse(&mut cursor).unwrap();
1248
1249 assert_eq!(parsed_header.magic, ANIM_MAGIC);
1250 assert_eq!(parsed_header.version, 1);
1251 assert_eq!(parsed_header.id_count, 2);
1252 assert_eq!(parsed_header.unknown, 0);
1253 assert_eq!(parsed_header.anim_entry_offset, 20);
1254 }
1255
1256 #[test]
1257 fn test_format_detection_modern() {
1258 let mut data = Vec::new();
1260 data.extend_from_slice(&ANIM_MAGIC);
1261 data.extend_from_slice(&[1, 0, 0, 0]); let mut cursor = Cursor::new(data);
1264 let format = AnimFormatDetector::detect_format(&mut cursor).unwrap();
1265
1266 assert_eq!(format, AnimFormat::Modern);
1267 assert_eq!(cursor.position(), 0); }
1269
1270 #[test]
1271 fn test_format_detection_legacy() {
1272 let mut data = Vec::new();
1274 data.extend_from_slice(&[2, 0, 0, 0]); data.extend_from_slice(&[100, 0, 0, 0]); let mut cursor = Cursor::new(data);
1278 let format = AnimFormatDetector::detect_format(&mut cursor).unwrap();
1279
1280 assert_eq!(format, AnimFormat::Legacy);
1281 assert_eq!(cursor.position(), 0); }
1283
1284 #[test]
1285 fn test_format_detection_by_version() {
1286 assert_eq!(
1288 AnimFormatDetector::detect_format_by_version(M2Version::Vanilla),
1289 AnimFormat::Legacy
1290 );
1291 assert_eq!(
1292 AnimFormatDetector::detect_format_by_version(M2Version::Cataclysm),
1293 AnimFormat::Legacy
1294 );
1295 assert_eq!(
1296 AnimFormatDetector::detect_format_by_version(M2Version::Legion),
1297 AnimFormat::Modern
1298 );
1299 }
1300
1301 #[test]
1302 fn test_anim_file_format_properties() {
1303 let legacy_file = AnimFile {
1305 format: AnimFormat::Legacy,
1306 sections: Vec::new(),
1307 metadata: AnimMetadata::Legacy {
1308 file_size: 1000,
1309 animation_count: 5,
1310 structure_hints: LegacyStructureHints {
1311 appears_valid: true,
1312 estimated_blocks: 5,
1313 has_timestamps: false,
1314 },
1315 },
1316 };
1317
1318 assert!(legacy_file.is_legacy_format());
1319 assert!(!legacy_file.is_modern_format());
1320 assert_eq!(legacy_file.animation_count(), 0); let modern_file = AnimFile {
1323 format: AnimFormat::Modern,
1324 sections: Vec::new(),
1325 metadata: AnimMetadata::Modern {
1326 header: AnimHeader {
1327 magic: ANIM_MAGIC,
1328 version: 1,
1329 id_count: 3,
1330 unknown: 0,
1331 anim_entry_offset: 20,
1332 },
1333 entries: Vec::new(),
1334 },
1335 };
1336
1337 assert!(!modern_file.is_legacy_format());
1338 assert!(modern_file.is_modern_format());
1339 }
1340
1341 #[test]
1342 fn test_anim_entry_parse_write() {
1343 let entry = AnimEntry {
1344 id: 1,
1345 offset: 100,
1346 size: 200,
1347 };
1348
1349 let mut data = Vec::new();
1350 entry.write(&mut data).unwrap();
1351
1352 let mut cursor = Cursor::new(data);
1353 let parsed_entry = AnimEntry::parse(&mut cursor).unwrap();
1354
1355 assert_eq!(parsed_entry.id, 1);
1356 assert_eq!(parsed_entry.offset, 100);
1357 assert_eq!(parsed_entry.size, 200);
1358 }
1359
1360 #[test]
1361 fn test_format_conversion() {
1362 let legacy_file = AnimFile {
1364 format: AnimFormat::Legacy,
1365 sections: vec![AnimSection {
1366 header: AnimSectionHeader {
1367 magic: *b"AFID",
1368 id: 1,
1369 start: 0,
1370 end: 100,
1371 },
1372 bone_animations: Vec::new(),
1373 }],
1374 metadata: AnimMetadata::Legacy {
1375 file_size: 1000,
1376 animation_count: 1,
1377 structure_hints: LegacyStructureHints {
1378 appears_valid: true,
1379 estimated_blocks: 1,
1380 has_timestamps: false,
1381 },
1382 },
1383 };
1384
1385 let modern_file = legacy_file.convert(M2Version::Legion);
1387 assert_eq!(modern_file.format, AnimFormat::Modern);
1388 assert_eq!(modern_file.sections.len(), 1);
1389 assert_eq!(modern_file.sections[0].header.id, 1);
1390
1391 let legacy_again = modern_file.convert(M2Version::Cataclysm);
1393 assert_eq!(legacy_again.format, AnimFormat::Legacy);
1394 assert_eq!(legacy_again.sections.len(), 1);
1395 assert_eq!(legacy_again.sections[0].header.id, 1);
1396 }
1397}
1398
1399#[cfg(test)]
1400mod integration_tests {
1401 use super::*;
1402 use std::path::Path;
1403
1404 #[test]
1405 fn test_real_cataclysm_anim_file() {
1406 let anim_path =
1407 "/home/danielsreichenbach/analysis/anim_samples/cataclysm/OrcFemale0064-00.anim";
1408
1409 if Path::new(anim_path).exists() {
1410 let result = AnimFile::load(anim_path);
1411 match result {
1412 Ok(anim_file) => {
1413 println!(
1414 "Successfully parsed ANIM file: {} sections, format: {:?}",
1415 anim_file.sections.len(),
1416 anim_file.format
1417 );
1418 assert!(
1419 !anim_file.sections.is_empty(),
1420 "ANIM file should have at least one section"
1421 );
1422 }
1423 Err(e) => {
1424 println!("Failed to parse ANIM file: {:?}", e);
1425 }
1428 }
1429 } else {
1430 println!("Test ANIM file not found at: {}", anim_path);
1431 }
1432 }
1433
1434 #[test]
1435 fn test_all_cataclysm_anim_samples() {
1436 let samples_dir = "/home/danielsreichenbach/analysis/anim_samples/cataclysm/";
1437
1438 if Path::new(samples_dir).exists()
1439 && let Ok(entries) = std::fs::read_dir(samples_dir)
1440 {
1441 let mut success_count = 0;
1442 let mut total_count = 0;
1443 let mut memory_stats = Vec::new();
1444
1445 for entry in entries.flatten() {
1446 let path = entry.path();
1447 if path.extension().map(|s| s == "anim").unwrap_or(false) {
1448 total_count += 1;
1449 println!("Testing ANIM file: {:?}", path.file_name());
1450
1451 match AnimFile::load(&path) {
1452 Ok(mut anim_file) => {
1453 success_count += 1;
1454
1455 match anim_file.validate() {
1457 Ok(()) => println!(" ✓ Validation: Pass"),
1458 Err(e) => println!(" ⚠ Validation: {:?}", e),
1459 }
1460
1461 let usage = anim_file.memory_usage();
1463 memory_stats.push(usage.clone());
1464 println!(
1465 " ✓ Success: {} sections, format: {:?}, memory: ~{} bytes",
1466 anim_file.sections.len(),
1467 anim_file.format,
1468 usage.approximate_bytes
1469 );
1470
1471 let before_opt = anim_file.memory_usage();
1473 anim_file.optimize_memory();
1474 let after_opt = anim_file.memory_usage();
1475 if after_opt.approximate_bytes < before_opt.approximate_bytes {
1476 println!(
1477 " ✓ Optimization: {} -> {} bytes",
1478 before_opt.approximate_bytes, after_opt.approximate_bytes
1479 );
1480 }
1481
1482 if anim_file.is_legacy_format() {
1484 let converted =
1485 anim_file.convert(crate::version::M2Version::Legion);
1486 assert!(
1487 converted.is_modern_format(),
1488 "Conversion to modern format failed"
1489 );
1490 }
1491 }
1492 Err(e) => {
1493 println!(" ✗ Failed: {:?}", e);
1494 }
1495 }
1496 }
1497 }
1498
1499 println!(
1500 "Summary: {}/{} ANIM files parsed successfully",
1501 success_count, total_count
1502 );
1503
1504 if !memory_stats.is_empty() {
1505 let total_memory: usize = memory_stats.iter().map(|s| s.approximate_bytes).sum();
1506 let avg_memory = total_memory / memory_stats.len();
1507 let total_keyframes: usize = memory_stats.iter().map(|s| s.total_keyframes()).sum();
1508 println!(
1509 "Memory stats: total ~{} bytes, avg ~{} bytes per file, {} total keyframes",
1510 total_memory, avg_memory, total_keyframes
1511 );
1512 }
1513 }
1514 }
1515
1516 #[test]
1517 fn test_anim_format_detection_edge_cases() {
1518 use std::io::Cursor;
1519
1520 let empty_data = vec![];
1522 let mut cursor = Cursor::new(empty_data);
1523 let result = AnimFormatDetector::detect_format(&mut cursor);
1524 assert!(result.is_err(), "Empty file should return error");
1525
1526 let small_data = vec![0x4D, 0x41, 0x4F]; let mut cursor = Cursor::new(small_data);
1529 let result = AnimFormatDetector::detect_format(&mut cursor);
1530 assert!(result.is_err(), "File with < 4 bytes should return error");
1531
1532 let min_data = vec![0x4D, 0x41, 0x4F, 0x46]; let mut cursor = Cursor::new(min_data);
1535 let result = AnimFormatDetector::detect_format(&mut cursor);
1536 assert!(result.is_ok(), "File with exactly 4 bytes should work");
1537 assert_eq!(result.unwrap(), AnimFormat::Modern);
1538
1539 let data = vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05];
1541 let mut cursor = Cursor::new(data);
1542 cursor.set_position(2);
1543 let initial_pos = cursor.position();
1544 let _result = AnimFormatDetector::detect_format(&mut cursor);
1545 assert_eq!(
1546 cursor.position(),
1547 initial_pos,
1548 "Position should be restored after detection"
1549 );
1550 }
1551
1552 #[test]
1553 fn test_anim_validation_edge_cases() {
1554 let empty_anim = AnimFile {
1556 format: AnimFormat::Legacy,
1557 sections: Vec::new(),
1558 metadata: AnimMetadata::Legacy {
1559 file_size: 100,
1560 animation_count: 0,
1561 structure_hints: LegacyStructureHints {
1562 appears_valid: true,
1563 estimated_blocks: 0,
1564 has_timestamps: false,
1565 },
1566 },
1567 };
1568
1569 let result = empty_anim.validate();
1570 assert!(result.is_err(), "Empty sections should fail validation");
1571
1572 let mismatched_anim = AnimFile {
1574 format: AnimFormat::Modern,
1575 sections: vec![],
1576 metadata: AnimMetadata::Legacy {
1577 file_size: 100,
1578 animation_count: 1,
1579 structure_hints: LegacyStructureHints {
1580 appears_valid: true,
1581 estimated_blocks: 1,
1582 has_timestamps: false,
1583 },
1584 },
1585 };
1586
1587 let result = mismatched_anim.validate();
1588 assert!(
1589 result.is_err(),
1590 "Format/metadata mismatch should fail validation"
1591 );
1592 }
1593}
1594
1595#[cfg(test)]
1596mod legacy_tests {
1597 use super::*;
1598 use std::io::Cursor;
1599
1600 #[test]
1601 fn test_legacy_animation_count_validation() {
1602 let mut data = vec![2u8, 0, 0, 0]; data.extend_from_slice(&[100u8, 0, 0, 0]); data.extend_from_slice(&[200u8, 0, 0, 0]); data.resize(300, 0);
1608
1609 let mut cursor = Cursor::new(data);
1610 let result = legacy_utils::validate_legacy_structure(&mut cursor, 2);
1611 assert!(result.is_ok());
1612 }
1613
1614 #[test]
1615 fn test_legacy_invalid_animation_count() {
1616 let data = vec![0u8, 0, 0, 0]; let mut cursor = Cursor::new(data);
1618
1619 let result = legacy_utils::validate_legacy_structure(&mut cursor, 0);
1620 assert!(result.is_ok());
1621 assert!(!result.unwrap()); }
1623
1624 #[test]
1625 fn test_legacy_file_too_small() {
1626 let data = vec![10u8, 0, 0, 0]; let mut cursor = Cursor::new(data);
1628
1629 let result = legacy_utils::validate_legacy_structure(&mut cursor, 10);
1630 assert!(result.is_ok());
1631 assert!(!result.unwrap()); }
1633}