Skip to main content

rpfm_lib/files/matched_combat/
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//! This module contains the implementation of the Matched Combat file format for Total War games.
12//!
13//! Matched combat files define synchronized combat animations between units, specifying which
14//! animation files to use, what conditions must be met, and how units transition during the
15//! animation sequence. These files control cinematic melee combat interactions where units
16//! perform matched animations together (e.g., sword duels, executions, grapples).
17//!
18//! # File Format
19//!
20//! Matched combat files (`.bin`) contain entries that define combat animation sequences.
21//! Each entry specifies:
22//!
23//! - Animation participants (multiple units involved in the sequence)
24//! - Entity-specific animation files and metadata
25//! - Selection weights for animation variety
26//! - Filters to determine when animations can be used (unit types, equipment, etc.)
27//! - State transitions (alive to dead, alive to alive, etc.)
28//! - Team assignments for participants
29//!
30//! # Versions
31//!
32//! The format has multiple versions with game-specific variations:
33//!
34//! - Version 1 (Three Kingdoms): Basic matched combat system
35//! - Version 1 (Warhammer 3): Extended with mount animations
36//! - Version 3: Further expanded format
37//!
38//! # File Locations
39//!
40//! Matched combat files are typically found in:
41//! - `animations/matched_combat/*.bin`
42//! - `animations/database/matched/*.bin`
43//! - `animations/database/trigger/*.bin`
44//!
45//! # Usage Example
46//!
47//! ```rust,ignore
48//! use rpfm_lib::files::{Decodeable, matched_combat::*};
49//!
50//! // Decode a matched combat file
51//! let mut data = std::io::Cursor::new(file_data);
52//! let extra_data = Some(DecodeableExtraData {
53//!     game_info: Some(game_info),
54//!     ..Default::default()
55//! });
56//! let matched = MatchedCombat::decode(&mut data, &extra_data)?;
57//!
58//! // Access combat entries
59//! for entry in matched.entries() {
60//!     println!("Combat ID: {}", entry.id());
61//!     for participant in entry.participants() {
62//!         println!("Team: {}", participant.team());
63//!     }
64//! }
65//! ```
66
67use getset::{Getters, Setters};
68use serde_derive::{Serialize, Deserialize};
69
70use crate::binary::{ReadBytes, WriteBytes};
71use crate::error::{RLibError, Result};
72use crate::files::{DecodeableExtraData, Decodeable, EncodeableExtraData, Encodeable};
73use crate::games::supported_games::{KEY_THREE_KINGDOMS, KEY_WARHAMMER_3};
74use crate::utils::check_size_mismatch;
75
76/// Matched combat files go under these folders.
77pub const BASE_PATHS: [&str; 3] = ["animations/matched_combat", "animations/database/matched", "animations/database/trigger"];
78
79/// Extension of MatchedCombat files.
80pub const EXTENSION: &str = ".bin";
81
82mod versions;
83
84#[cfg(test)] mod matched_combat_test;
85
86//---------------------------------------------------------------------------//
87//                              Enum & Structs
88//---------------------------------------------------------------------------//
89
90/// Represents a matched combat file decoded in memory.
91///
92/// Contains the format version and a list of combat animation entries.
93#[derive(PartialEq, Clone, Debug, Default, Getters, Setters, Serialize, Deserialize)]
94#[getset(get = "pub", set = "pub")]
95pub struct MatchedCombat {
96    /// File format version (1 or 3).
97    version: u32,
98
99    /// List of matched combat animation entries.
100    entries: Vec<MatchedEntry>,
101}
102
103/// A single matched combat animation entry.
104///
105/// Defines a synchronized combat sequence involving one or more participants,
106/// each with their own animations and state transitions.
107#[derive(PartialEq, Clone, Debug, Default, Getters, Setters, Serialize, Deserialize)]
108#[getset(get = "pub", set = "pub")]
109pub struct MatchedEntry {
110    /// Unique identifier for this matched combat entry.
111    id: String,
112
113    /// List of participants involved in this combat sequence.
114    ///
115    /// Typically 2 participants for duels, but can be more for group animations.
116    participants: Vec<Participant>,
117}
118
119/// A participant in a matched combat animation sequence.
120///
121/// Represents one unit/entity involved in the combat, including which animations
122/// it should play, what conditions must be met, and how its state changes.
123#[derive(PartialEq, Clone, Debug, Default, Getters, Setters, Serialize, Deserialize)]
124#[getset(get = "pub", set = "pub")]
125pub struct Participant {
126    /// Team identifier for this participant (e.g., 0 for attacker, 1 for defender).
127    team: u32,
128
129    /// Bundles of animation entities with selection weights.
130    ///
131    /// Multiple bundles allow for animation variety - the game randomly selects
132    /// one bundle based on weights.
133    entity_info: Vec<EntityBundle>,
134
135    /// State transition for this participant (e.g., alive to dead).
136    state: State,
137
138    /// Unknown field from Three Kingdoms files.
139    uk1: u32,
140
141    /// Unknown field from Three Kingdoms files.
142    uk2: u32,
143
144    /// Unknown field from Warhammer 3 files.
145    uk3: u32,
146
147    /// Unknown field from Warhammer 3 files.
148    uk4: u32,
149}
150
151/// A bundle of animation entities that can be selected together.
152///
153/// Allows the game to randomly choose between different animation variations
154/// based on selection weights, providing visual variety in combat.
155#[derive(PartialEq, Clone, Debug, Default, Getters, Setters, Serialize, Deserialize)]
156#[getset(get = "pub", set = "pub")]
157pub struct EntityBundle {
158    /// Animation entities in this bundle.
159    entities: Vec<Entity>,
160
161    /// Weight for random selection of this bundle (higher = more likely to be chosen).
162    selection_weight: f32,
163}
164
165/// An entity's animation data for matched combat.
166///
167/// Specifies the animation files, metadata, timing, equipment visibility,
168/// and conditions (filters) for when this animation can be used.
169#[derive(PartialEq, Clone, Debug, Default, Getters, Setters, Serialize, Deserialize)]
170#[getset(get = "pub", set = "pub")]
171pub struct Entity {
172    /// Path to the animation file for this entity.
173    animation_filename: String,
174
175    /// Paths to metadata files containing additional animation data.
176    metadata_filenames: Vec<String>,
177
178    /// Time in seconds for blending into this animation from the previous state.
179    blend_in_time: f32,
180
181    /// Equipment display flags controlling weapon/shield visibility during animation.
182    equipment_display: u32,
183
184    /// Filters that must match for this animation to be eligible for selection.
185    ///
186    /// Filters check conditions like unit type, equipment, size, etc.
187    filters: Vec<Filter>,
188
189    /// Unknown field.
190    uk: u32,
191
192    /// Animation filename for the mount (only in Warhammer 3 files).
193    ///
194    /// Used when the participant is mounted on a creature or horse.
195    mount_filename: String,
196}
197
198/// A filter condition for determining when an animation can be used.
199///
200/// Filters check properties of the participating units (e.g., unit type, equipment,
201/// size class) to ensure animations only play when appropriate.
202#[derive(PartialEq, Clone, Debug, Default, Getters, Setters, Serialize, Deserialize)]
203#[getset(get = "pub", set = "pub")]
204pub struct Filter {
205    /// If true, check for equality; if false, check for inequality.
206    equals: bool,
207
208    /// If true, combine with previous filter using OR; if false, use AND.
209    or: bool,
210
211    /// Type of filter (e.g., unit type, equipment slot, size class).
212    filter_type: u32,
213
214    /// Value to compare against (interpretation depends on filter_type).
215    value: String,
216}
217
218/// State transition for a participant during the matched combat sequence.
219///
220/// Defines how the participant's state changes from the start to the end of the animation
221/// (e.g., alive to dead for an execution animation, alive to alive for a non-lethal clash).
222#[derive(PartialEq, Clone, Debug, Default, Getters, Setters, Serialize, Deserialize)]
223#[getset(get = "pub", set = "pub")]
224pub struct State {
225    /// State at the beginning of the animation.
226    start: StateParticipant,
227
228    /// State at the end of the animation.
229    end: StateParticipant,
230}
231
232/// Possible states for a participant in a matched combat animation.
233///
234/// Indicates whether the participant is alive, dead, or in some other state
235/// at the beginning or end of the animation sequence.
236#[derive(PartialEq, Clone, Copy, Debug, Default, Serialize, Deserialize)]
237#[repr(u32)]
238pub enum StateParticipant {
239    /// Participant is alive and active.
240    #[default] Alive,
241
242    /// Participant is dead or dying.
243    Dead = 1,
244
245    /// Unknown state variant.
246    NoIdea1 = 2,
247
248    /// Unknown state variant.
249    NoIdea2 = 3,
250
251    /// Unknown state variant.
252    NoIdea3 = 4,
253
254    /// Unknown state variant.
255    NoIdea4 = 5,
256
257    /// Unknown state variant.
258    NoIdea5 = 6,
259}
260
261//---------------------------------------------------------------------------//
262//                      Implementation of MatchedCombat
263//---------------------------------------------------------------------------//
264
265impl Decodeable for MatchedCombat {
266
267    fn decode<R: ReadBytes>(data: &mut R, extra_data: &Option<DecodeableExtraData>) -> Result<Self> {
268        let extra_data = extra_data.as_ref().ok_or(RLibError::DecodingMissingExtraData)?;
269        let game_info = extra_data.game_info.ok_or_else(|| RLibError::DecodingMissingExtraDataField("game_info".to_owned()))?;
270
271        let mut matched = Self::default();
272        matched.version = data.read_u32()?;
273
274        match matched.version {
275            1 => match game_info.key() {
276                KEY_WARHAMMER_3 => matched.read_v1_wh3(data)?,
277                KEY_THREE_KINGDOMS => matched.read_v1_3k(data)?,
278                _ => Err(RLibError::DecodingMatchedCombatUnsupportedVersion(matched.version as usize))?,
279            }
280            3 => matched.read_v3(data)?,
281            _ => Err(RLibError::DecodingMatchedCombatUnsupportedVersion(matched.version as usize))?,
282        }
283
284        // If we are not in the last byte, it means we didn't parse the entire file, which means this file is corrupt.
285        check_size_mismatch(data.stream_position()? as usize, data.len()? as usize)?;
286
287        Ok(matched)
288    }
289}
290
291impl Encodeable for MatchedCombat {
292
293    fn encode<W: WriteBytes>(&mut self, buffer: &mut W, extra_data: &Option<EncodeableExtraData>) -> Result<()> {
294        let extra_data = extra_data.as_ref().ok_or(RLibError::EncodingMissingExtraData)?;
295        let game_info = extra_data.game_info.ok_or_else(|| RLibError::DecodingMissingExtraDataField("game_info".to_owned()))?;
296
297        buffer.write_u32(self.version)?;
298
299        match self.version {
300            1 => match game_info.key() {
301                KEY_WARHAMMER_3 => self.write_v1_wh3(buffer)?,
302                KEY_THREE_KINGDOMS => self.write_v1_3k(buffer)?,
303                _ => Err(RLibError::DecodingMatchedCombatUnsupportedVersion(self.version as usize))?,
304            }
305            3 => self.write_v3(buffer)?,
306            _ => Err(RLibError::DecodingMatchedCombatUnsupportedVersion(self.version as usize))?,
307        };
308
309        Ok(())
310    }
311}
312
313impl TryFrom<u32> for StateParticipant {
314    type Error = RLibError;
315
316    fn try_from(value: u32) -> std::result::Result<Self, Self::Error> {
317        match value {
318            _ if value == Self::Alive as u32 => Ok(Self::Alive),
319            _ if value == Self::Dead as u32 => Ok(Self::Dead),
320            _ if value == Self::NoIdea1 as u32 => Ok(Self::NoIdea1),
321            _ if value == Self::NoIdea2 as u32 => Ok(Self::NoIdea2),
322            _ if value == Self::NoIdea3 as u32 => Ok(Self::NoIdea3),
323            _ if value == Self::NoIdea4 as u32 => Ok(Self::NoIdea4),
324            _ if value == Self::NoIdea5 as u32 => Ok(Self::NoIdea5),
325            _ => Err(RLibError::InvalidStateParticipantValue(value)),
326        }
327    }
328}