Skip to main content

rpfm_lib/files/anim_fragment_battle/
mod.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Animation fragment battle file format support.
12//!
13//! This module handles animation fragment files (`.bin`/`.frg`) which define battle animations
14//! for units in Total War games. These files replaced the older text-based animation tables
15//! with a more efficient binary format.
16//!
17//! # File Format
18//!
19//! Animation fragments use game-specific binary formats with different versions:
20//! - **Version 2 (Warhammer 2)**: Basic animation metadata with file references
21//! - **Version 2 (Three Kingdoms)**: Enhanced with mount/unmount tables and locomotion graphs
22//! - **Version 4 (Warhammer 3)**: Advanced format with animation references and cavalry tech flags
23//!
24//! # File Extensions
25//!
26//! - `.bin` - Modern binary animation fragment files (Warhammer 2+)
27//! - `.frg` - Legacy fragment file extension (older games)
28//!
29//! # File Organization
30//!
31//! Animation fragments are stored in:
32//! ```text
33//! animations/{skeleton_name}/battle/{animation_type}.bin
34//! ```
35//!
36//! # Supported Games
37//!
38//! Full support for:
39//! - Total War: Warhammer II (version 2)
40//! - Total War: Three Kingdoms (version 2, enhanced)
41//! - A Total War Saga: Troy (version 2)
42//! - Total War: Warhammer III (version 4)
43//! - Total War: Pharaoh / Pharaoh Dynasties (version 2)
44//!
45//! Older games (pre-Warhammer 2) are not supported.
46//!
47//! # Animation Entry Structure
48//!
49//! Each entry contains:
50//! - Animation ID and metadata (blend time, selection weight)
51//! - Weapon bone flags (for weapon attachment points)
52//! - File references to animation data and metadata
53//! - Optional single-frame variant flag
54//!
55//! # Table Conversion
56//!
57//! Animation fragments can be converted to/from [`TableInMemory`]
58//! for editing as TSV files.
59//!
60//! [`TableInMemory`]: crate::files::table::local::TableInMemory
61//!
62//! # Usage
63//!
64//! ```ignore
65//! use rpfm_lib::files::anim_fragment_battle::AnimFragmentBattle;
66//! use rpfm_lib::files::Decodeable;
67//!
68//! // Decode an animation fragment
69//! let fragment = AnimFragmentBattle::decode(&mut data, &Some(extra_data))?;
70//!
71//! // Access entries
72//! for entry in fragment.entries() {
73//!     println!("Animation {}: blend={}, weight={}",
74//!         entry.animation_id(),
75//!         entry.blend_in_time(),
76//!         entry.selection_weight()
77//!     );
78//! }
79//!
80//! // Convert to table for TSV export
81//! let table = fragment.to_table()?;
82//! ```
83
84use bitflags::bitflags;
85use getset::*;
86use serde_derive::{Serialize, Deserialize};
87
88use std::collections::{BTreeMap, HashMap};
89use std::io::Cursor;
90
91use crate::binary::{ReadBytes, WriteBytes};
92use crate::error::{RLibError, Result};
93use crate::files::{DecodeableExtraData, Decodeable, EncodeableExtraData, Encodeable, table::{DecodedData, local::TableInMemory, Table}};
94use crate::games::supported_games::{KEY_PHARAOH, KEY_PHARAOH_DYNASTIES, KEY_THREE_KINGDOMS, KEY_TROY, KEY_WARHAMMER_2};
95use crate::schema::*;
96use crate::utils::check_size_mismatch;
97
98/// Base directory path for animation files.
99pub const BASE_PATH: &str = "animations/";
100
101/// Middle path component for battle animations.
102pub const MID_PATH: &str = "/battle/";
103
104/// Modern file extension for animation fragment files.
105pub const EXTENSION_NEW: &str = ".bin";
106
107/// Legacy file extension for animation fragment files.
108pub const EXTENSION_OLD: &str = ".frg";
109
110mod versions;
111
112#[cfg(test)] mod anim_fragment_battle_test;
113
114//---------------------------------------------------------------------------//
115//                              Enum & Structs
116//---------------------------------------------------------------------------//
117
118/// Represents a battle animation fragment file.
119///
120/// Contains animation entries and metadata for a specific skeleton type.
121/// The structure varies by game version, with newer games supporting more features.
122///
123/// # Version Differences
124///
125/// - **Version 2 (Warhammer 2/Troy/Pharaoh)**: Basic structure with min/max IDs
126/// - **Version 2 (Three Kingdoms)**: Adds mount tables and locomotion graph support
127/// - **Version 4 (Warhammer 3)**: Enhanced with subversion and cavalry tech flags
128#[derive(PartialEq, Clone, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize)]
129#[getset(get = "pub", get_mut = "pub", set = "pub")]
130pub struct AnimFragmentBattle {
131    // Common fields across all versions
132
133    /// File format version (2 or 4).
134    version: u32,
135
136    /// List of animation entries in this fragment.
137    entries: Vec<Entry>,
138
139    /// Name of the skeleton this animation fragment applies to.
140    skeleton_name: String,
141
142    // Warhammer 3 specific (version 4)
143
144    /// Format subversion (version 4 only).
145    subversion: u32,
146
147    // Warhammer 3 / Three Kingdoms specific
148
149    /// Name of the animation table.
150    table_name: String,
151
152    /// Name of the mount animation table.
153    mount_table_name: String,
154
155    /// Name of the unmount animation table.
156    unmount_table_name: String,
157
158    /// Locomotion graph identifier.
159    locomotion_graph: String,
160
161    /// Whether this uses simple flight mechanics.
162    is_simple_flight: bool,
163
164    /// Whether this uses new cavalry technology.
165    is_new_cavalry_tech: bool,
166
167    // Warhammer 2 specific (version 2)
168
169    /// Minimum animation ID in this fragment (version 2 only).
170    min_id: u32,
171
172    /// Maximum animation ID in this fragment (version 2 only).
173    max_id: u32,
174}
175
176/// Represents a single animation entry within a fragment.
177///
178/// Contains all metadata and file references for one animation. The structure
179/// varies between version 2 and version 4 formats.
180///
181/// # Version Differences
182///
183/// - **Version 2**: Uses direct filename and metadata strings
184/// - **Version 4**: Uses `anim_refs` for multiple animation file references
185#[derive(PartialEq, Clone, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize)]
186#[getset(get = "pub", get_mut = "pub", set = "pub")]
187pub struct Entry {
188    // Common fields across all versions
189
190    /// Unique animation identifier.
191    animation_id: u32,
192
193    /// Blend-in time in seconds for smooth transitions.
194    blend_in_time: f32,
195
196    /// Selection weight for animation variation selection (higher = more likely).
197    selection_weight: f32,
198
199    /// Weapon attachment bone flags.
200    weapon_bone: WeaponBone,
201
202    // Warhammer 3 specific (version 4)
203
204    /// Animation file references (version 4 only).
205    ///
206    /// Contains paths to animation data, metadata, and sound files.
207    anim_refs: Vec<AnimRef>,
208
209    // Warhammer 2 specific (version 2)
210
211    /// Slot identifier (version 2 only).
212    slot_id: u32,
213
214    /// Animation filename (version 2 only).
215    filename: String,
216
217    /// Metadata file path (version 2 only).
218    metadata: String,
219
220    /// Sound metadata file path (version 2 only).
221    metadata_sound: String,
222
223    /// Skeleton type identifier (version 2 only).
224    skeleton_type: String,
225
226    /// Unknown field (purpose not identified, version 2 only).
227    uk_3: u32,
228
229    /// Unknown field (purpose not identified, version 2 only).
230    uk_4: String,
231
232    // Common to version 2 and 4
233
234    /// Whether this is a single-frame animation variant.
235    single_frame_variant: bool,
236}
237
238/// Animation file reference (version 4 only).
239///
240/// Contains paths to the three files that make up an animation:
241/// - Animation data file (skeletal animation)
242/// - Metadata file (timing, events, etc.)
243/// - Sound file (audio cues and effects)
244#[derive(PartialEq, Clone, Debug, Default, Getters, MutGetters, Setters, Serialize, Deserialize)]
245#[getset(get = "pub", get_mut = "pub", set = "pub")]
246pub struct AnimRef {
247    /// Path to the animation data file.
248    file_path: String,
249
250    /// Path to the animation metadata file.
251    meta_file_path: String,
252
253    /// Path to the sound file.
254    snd_file_path: String,
255}
256
257bitflags! {
258    /// Weapon attachment bone flags.
259    ///
260    /// Defines which bones (attachment points) on the skeleton are used for
261    /// weapon positioning during this animation. Multiple bones can be active
262    /// simultaneously (e.g., for dual-wielding).
263    ///
264    /// # Bone Mapping
265    ///
266    /// Each bit corresponds to a specific weapon attachment point:
267    /// - `WEAPON_BONE_1`: Primary weapon hand (typically right hand)
268    /// - `WEAPON_BONE_2`: Secondary weapon hand (typically left hand)
269    /// - `WEAPON_BONE_3`: Back-mounted weapon (holstered)
270    /// - `WEAPON_BONE_4`: Additional attachment point
271    /// - `WEAPON_BONE_5`: Additional attachment point
272    /// - `WEAPON_BONE_6`: Additional attachment point
273    ///
274    /// The exact bone mapping depends on the skeleton definition.
275    #[derive(PartialEq, Clone, Copy, Debug, Default, Serialize, Deserialize)]
276    pub struct WeaponBone: u32 {
277        /// Primary weapon bone (bit 0).
278        const WEAPON_BONE_1 = 0b0000_0000_0000_0001;
279        
280        /// Secondary weapon bone (bit 1).
281        const WEAPON_BONE_2 = 0b0000_0000_0000_0010;
282        
283        /// Tertiary weapon bone (bit 2).
284        const WEAPON_BONE_3 = 0b0000_0000_0000_0100;
285        
286        /// Fourth weapon bone (bit 3).
287        const WEAPON_BONE_4 = 0b0000_0000_0000_1000;
288        
289        /// Fifth weapon bone (bit 4).
290        const WEAPON_BONE_5 = 0b0000_0000_0001_0000;
291        
292        /// Sixth weapon bone (bit 5).
293        const WEAPON_BONE_6 = 0b0000_0000_0010_0000;
294    }
295}
296
297//---------------------------------------------------------------------------//
298//                      Implementation of AnimFragment
299//---------------------------------------------------------------------------//
300
301impl AnimFragmentBattle {
302
303    /// Returns table schema definitions for animation fragments.
304    ///
305    /// Provides two [`Definition`]s:
306    /// 1. Main entry definition with all animation entry fields
307    /// 2. Animation reference sub-definition (for version 4 `anim_refs` field)
308    ///
309    /// These definitions are used when converting animation fragments to/from
310    /// [`TableInMemory`] for TSV export/import.
311    ///
312    /// [`TableInMemory`]: crate::files::table::local::TableInMemory
313    ///
314    /// # Returns
315    ///
316    /// A tuple of `(entry_definition, anim_ref_definition)`.
317    pub fn definitions() -> (Definition, Definition) {
318        let mut anim_refs_definition = Definition::default();
319        anim_refs_definition.fields_mut().push(Field::new("file_path".to_string(), FieldType::StringU8, false, None, false, None, None, None, String::new(), -1, 0, BTreeMap::new(), None));
320        anim_refs_definition.fields_mut().push(Field::new("meta_file_path".to_string(), FieldType::StringU8, false, None, false, None, None, None, String::new(), -1, 0, BTreeMap::new(), None));
321        anim_refs_definition.fields_mut().push(Field::new("snd_file_path".to_string(), FieldType::StringU8, false, None, false, None, None, None, String::new(), -1, 0, BTreeMap::new(), None));
322
323        let mut definition = Definition::default();
324        definition.fields_mut().push(Field::new("animation_id".to_string(), FieldType::I32, true, None, false, None, None, None, String::new(), -1, 0, BTreeMap::new(), None));
325        definition.fields_mut().push(Field::new("blend_in_time".to_string(), FieldType::F32, false, None, false, None, None, None, String::new(), -1, 0, BTreeMap::new(), None));
326        definition.fields_mut().push(Field::new("selection_weight".to_string(), FieldType::F32, false, None, false, None, None, None, String::new(), -1, 0, BTreeMap::new(), None));
327        definition.fields_mut().push(Field::new("weapon_bone".to_string(), FieldType::I32, false, None, false, None, None, None, String::new(), -1, 6, BTreeMap::new(), None));
328        definition.fields_mut().push(Field::new("anim_refs".to_string(), FieldType::SequenceU32(Box::new(anim_refs_definition.clone())), false, None, false, None, None, None, String::new(), -1, 0, BTreeMap::new(), None));
329        definition.fields_mut().push(Field::new("slot_id".to_string(), FieldType::I32, true, None, false, None, None, None, String::new(), -1, 0, BTreeMap::new(), None));
330        definition.fields_mut().push(Field::new("filename".to_string(), FieldType::StringU8, false, None, false, None, None, None, String::new(), -1, 0, BTreeMap::new(), None));
331        definition.fields_mut().push(Field::new("metadata".to_string(), FieldType::StringU8, false, None, false, None, None, None, String::new(), -1, 0, BTreeMap::new(), None));
332        definition.fields_mut().push(Field::new("metadata_sound".to_string(), FieldType::StringU8, false, None, false, None, None, None, String::new(), -1, 0, BTreeMap::new(), None));
333        definition.fields_mut().push(Field::new("skeleton_type".to_string(), FieldType::StringU8, false, None, false, None, None, None, String::new(), -1, 0, BTreeMap::new(), None));
334        definition.fields_mut().push(Field::new("uk_3".to_string(), FieldType::I32, false, None, false, None, None, None, String::new(), -1, 0, BTreeMap::new(), None));
335        definition.fields_mut().push(Field::new("uk_4".to_string(), FieldType::StringU8, false, None, false, None, None, None, String::new(), -1, 0, BTreeMap::new(), None));
336        definition.fields_mut().push(Field::new("single_frame_variant".to_string(), FieldType::Boolean, false, None, false, None, None, None, String::new(), -1, 0, BTreeMap::new(), None));
337
338        (definition, anim_refs_definition)
339    }
340
341    /// Converts a table to a list of animation entries.
342    ///
343    /// Parses a [`TableInMemory`] (typically loaded from TSV)
344    /// and extracts animation entry data.
345    ///
346    /// [`TableInMemory`]: crate::files::table::local::TableInMemory
347    ///
348    /// # Parameters
349    ///
350    /// - `table`: The table containing animation entry data
351    ///
352    /// # Returns
353    ///
354    /// A vector of [`Entry`] structs parsed from the table rows.
355    ///
356    /// # Errors
357    ///
358    /// Returns an error if the table structure doesn't match the expected schema
359    /// or if data conversion fails.
360    pub fn from_table(table: &TableInMemory) -> Result<Vec<Entry>> {
361        let mut entries = vec![];
362
363        let definition = table.definition();
364        let fields_processed = definition.fields_processed();
365
366        for row in table.data().iter() {
367            let mut entry = Entry::default();
368
369            if let DecodedData::I32(data) = row[0] {
370                entry.set_animation_id(data as u32);
371            }
372
373            if let DecodedData::F32(data) = row[1] {
374                entry.set_blend_in_time(data);
375            }
376
377            if let DecodedData::F32(data) = row[2] {
378                entry.set_selection_weight(data);
379            }
380
381            if let DecodedData::Boolean(data_1) = row[3] {
382                if let DecodedData::Boolean(data_2) = row[4] {
383                    if let DecodedData::Boolean(data_3) = row[5] {
384                        if let DecodedData::Boolean(data_4) = row[6] {
385                            if let DecodedData::Boolean(data_5) = row[7] {
386                                if let DecodedData::Boolean(data_6) = row[8] {
387                                    let mut bits = WeaponBone::empty();
388
389                                    if data_1 { bits |= WeaponBone::WEAPON_BONE_1; }
390                                    if data_2 { bits |= WeaponBone::WEAPON_BONE_2; }
391                                    if data_3 { bits |= WeaponBone::WEAPON_BONE_3; }
392                                    if data_4 { bits |= WeaponBone::WEAPON_BONE_4; }
393                                    if data_5 { bits |= WeaponBone::WEAPON_BONE_5; }
394                                    if data_6 { bits |= WeaponBone::WEAPON_BONE_6; }
395
396                                    entry.set_weapon_bone(bits);
397                                }
398                            }
399                        }
400                    }
401                }
402            }
403
404            if let DecodedData::SequenceU32(ref data) = row[9] {
405                if let FieldType::SequenceU32(ref definition) = fields_processed[9].field_type() {
406                    let mut data = Cursor::new(data);
407                    let data = TableInMemory::decode(&mut data, definition, &HashMap::new(), None, false, fields_processed[9].name())?;
408                    let mut entries = vec![];
409
410                    for row in data.data().iter() {
411                        let mut entry = AnimRef::default();
412
413                        if let DecodedData::StringU8(ref data) = row[0] {
414                            entry.set_file_path(data.to_string());
415                        }
416
417                        if let DecodedData::StringU8(ref data) = row[1] {
418                            entry.set_meta_file_path(data.to_string());
419                        }
420
421                        if let DecodedData::StringU8(ref data) = row[2] {
422                            entry.set_snd_file_path(data.to_string());
423                        }
424
425                        entries.push(entry);
426                    }
427
428                    entry.set_anim_refs(entries);
429                }
430            }
431
432            if let DecodedData::I32(data) = row[10] {
433                entry.set_slot_id(data as u32);
434            }
435
436            if let DecodedData::StringU8(ref data) = row[11] {
437                entry.set_filename(data.to_string());
438            }
439
440            if let DecodedData::StringU8(ref data) = row[12] {
441                entry.set_metadata(data.to_string());
442            }
443
444            if let DecodedData::StringU8(ref data) = row[13] {
445                entry.set_metadata_sound(data.to_string());
446            }
447
448            if let DecodedData::StringU8(ref data) = row[14] {
449                entry.set_skeleton_type(data.to_string());
450            }
451
452            if let DecodedData::I32(data) = row[15] {
453                entry.set_uk_3(data as u32);
454            }
455
456            if let DecodedData::StringU8(ref data) = row[16] {
457                entry.set_uk_4(data.to_string());
458            }
459
460            if let DecodedData::Boolean(data) = row[17] {
461                entry.set_single_frame_variant(data);
462            }
463
464            entries.push(entry);
465        }
466
467        Ok(entries)
468    }
469
470    /// Converts this animation fragment to a table.
471    ///
472    /// Creates a [`TableInMemory`] from the animation entries, which can then be
473    /// exported as TSV for editing.
474    ///
475    /// [`TableInMemory`]: crate::files::table::local::TableInMemory
476    ///
477    /// # Returns
478    ///
479    /// A table containing all animation entries with the appropriate schema.
480    ///
481    /// # Errors
482    ///
483    /// Returns an error if table construction or data conversion fails.
484    pub fn to_table(&self) -> Result<TableInMemory> {
485        let (definition, anim_refs_definition) = Self::definitions();
486        let mut table = TableInMemory::new(&definition, None, "");
487
488        let data = self.entries()
489            .iter()
490            .map(|entry| {
491            let mut row = Vec::with_capacity(19);
492            row.push(DecodedData::I32(*entry.animation_id() as i32));
493            row.push(DecodedData::F32(*entry.blend_in_time()));
494            row.push(DecodedData::F32(*entry.selection_weight()));
495            row.push(DecodedData::Boolean(entry.weapon_bone().contains(WeaponBone::WEAPON_BONE_1)));
496            row.push(DecodedData::Boolean(entry.weapon_bone().contains(WeaponBone::WEAPON_BONE_2)));
497            row.push(DecodedData::Boolean(entry.weapon_bone().contains(WeaponBone::WEAPON_BONE_3)));
498            row.push(DecodedData::Boolean(entry.weapon_bone().contains(WeaponBone::WEAPON_BONE_4)));
499            row.push(DecodedData::Boolean(entry.weapon_bone().contains(WeaponBone::WEAPON_BONE_5)));
500            row.push(DecodedData::Boolean(entry.weapon_bone().contains(WeaponBone::WEAPON_BONE_6)));
501
502            let mut anim_refs_subtable = TableInMemory::new(&anim_refs_definition, None, "anim_refs");
503            let mut anim_ref_rows = Vec::with_capacity(entry.anim_refs().len());
504            for anim_ref in entry.anim_refs() {
505                let anim_ref_row = vec![
506                    DecodedData::StringU8(anim_ref.file_path().to_string()),
507                    DecodedData::StringU8(anim_ref.meta_file_path().to_string()),
508                    DecodedData::StringU8(anim_ref.snd_file_path().to_string()),
509                ];
510                anim_ref_rows.push(anim_ref_row)
511            }
512            anim_refs_subtable.set_data(&anim_ref_rows).unwrap();
513            let mut writer = vec![];
514            writer.write_u32(anim_ref_rows.len() as u32).unwrap();
515            let _ = anim_refs_subtable.encode(&mut writer);
516
517            row.push(DecodedData::SequenceU32(writer));
518
519            row.push(DecodedData::I32(*entry.slot_id() as i32));
520            row.push(DecodedData::StringU8(entry.filename().to_string()));
521            row.push(DecodedData::StringU8(entry.metadata().to_string()));
522            row.push(DecodedData::StringU8(entry.metadata_sound().to_string()));
523            row.push(DecodedData::StringU8(entry.skeleton_type().to_string()));
524            row.push(DecodedData::I32(*entry.uk_3() as i32));
525            row.push(DecodedData::StringU8(entry.uk_4().to_string()));
526            row.push(DecodedData::Boolean(*entry.single_frame_variant()));
527
528            row
529        }).collect::<Vec<_>>();
530
531        table.set_data(&data)?;
532        Ok(table)
533    }
534}
535
536impl Decodeable for AnimFragmentBattle {
537
538    fn decode<R: ReadBytes>(data: &mut R, extra_data: &Option<DecodeableExtraData>) -> Result<Self> {
539        let extra_data = extra_data.as_ref().ok_or(RLibError::DecodingMissingExtraData)?;
540        let game_info = extra_data.game_info.ok_or_else(|| RLibError::DecodingMissingExtraDataField("game_info".to_owned()))?;
541
542        let version = data.read_u32()?;
543
544        let mut fragment = Self::default();
545        fragment.version = version;
546
547        match version {
548            2 => match game_info.key() {
549                KEY_WARHAMMER_2 | KEY_TROY | KEY_PHARAOH | KEY_PHARAOH_DYNASTIES => fragment.read_v2_wh2(data)?,
550                KEY_THREE_KINGDOMS => fragment.read_v2_3k(data)?,
551                _ => Err(RLibError::DecodingMatchedCombatUnsupportedVersion(fragment.version as usize))?,
552            },
553            4 => fragment.read_v4(data)?,
554            _ => Err(RLibError::DecodingAnimFragmentUnsupportedVersion(version as usize))?,
555        }
556
557        // If we are not in the last byte, it means we didn't parse the entire file, which means this file is corrupt.
558        check_size_mismatch(data.stream_position()? as usize, data.len()? as usize)?;
559
560        Ok(fragment)
561    }
562}
563
564impl Encodeable for AnimFragmentBattle {
565
566    fn encode<W: WriteBytes>(&mut self, buffer: &mut W, extra_data: &Option<EncodeableExtraData>) -> Result<()> {
567        let extra_data = extra_data.as_ref().ok_or(RLibError::DecodingMissingExtraData)?;
568        let game_info = extra_data.game_info.ok_or_else(|| RLibError::DecodingMissingExtraDataField("game_info".to_owned()))?;
569
570        buffer.write_u32(self.version)?;
571
572        match self.version {
573            2 => match game_info.key() {
574                KEY_WARHAMMER_2 | KEY_TROY | KEY_PHARAOH | KEY_PHARAOH_DYNASTIES => self.write_v2_wh2(buffer)?,
575                KEY_THREE_KINGDOMS => self.write_v2_3k(buffer)?,
576                _ => Err(RLibError::DecodingMatchedCombatUnsupportedVersion(self.version as usize))?,
577            },
578            4 => self.write_v4(buffer)?,
579            _ => Err(RLibError::DecodingAnimFragmentUnsupportedVersion(self.version as usize))?,
580        };
581
582        Ok(())
583    }
584}