Skip to main content

rpfm_lib/files/group_formations/
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 Group Formations file format for Total War games.
12//!
13//! Group formations define fixed formation templates that both the AI and player can use to
14//! deploy their units on the battlefield. Each formation is designed for specific tactical
15//! scenarios (attack, defend, naval, etc.) and specifies unit placement, spacing, arrangement
16//! patterns, and which unit types should be placed in each position.
17//!
18//! # File Format
19//!
20//! The group formations file (`groupformations.bin`) is a binary file containing formation
21//! definitions that can be used by both AI and players to deploy armies tactically. Each
22//! formation can specify:
23//!
24//! - AI purpose flags (attack, defend, river crossing, naval, etc.)
25//! - Priority and unit category requirements
26//! - Supported factions and subcultures
27//! - Formation blocks defining unit positions (absolute, relative, or spanning)
28//! - Entity preferences specifying which unit types go where
29//!
30//! # Game Support
31//!
32//! This file format is not versioned in the traditional sense. Instead, different games have
33//! different implementations in the `versions/` subdirectory:
34//!
35//! - Shogun 2: Basic formation system with entity types and arrangements
36//! - Rome 2 (and later): Extended system with entity weights, subcultures, and more AI purposes
37//!
38//! # File Location
39//!
40//! Group formations files are typically found at:
41//! - `groupformations.bin` in the root of a game's pack
42//!
43//! # Usage Example
44//!
45//! ```rust,ignore
46//! use rpfm_lib::files::{Decodeable, group_formations::*};
47//!
48//! // Decode a group formations file
49//! let mut data = std::io::Cursor::new(file_data);
50//! let extra_data = Some(DecodeableExtraData {
51//!     game_info: Some(game_info),
52//!     ..Default::default()
53//! });
54//! let formations = GroupFormations::decode(&mut data, &extra_data)?;
55//!
56//! // Access formation data
57//! for formation in formations.formations() {
58//!     println!("Formation: {}", formation.name());
59//!     println!("Priority: {}", formation.ai_priority());
60//! }
61//! ```
62
63use getset::*;
64use serde_derive::{Serialize, Deserialize};
65
66use crate::binary::{ReadBytes, WriteBytes};
67use crate::error::{Result, RLibError};
68use crate::files::{Decodeable, EncodeableExtraData, Encodeable};
69use crate::games::supported_games::*;
70use crate::utils::*;
71
72use super::DecodeableExtraData;
73
74/// Fixed path to the Group Formations file.
75pub const PATH: &str = "groupformations.bin";
76
77pub mod versions;
78
79#[cfg(test)] mod test_group_formations;
80
81//---------------------------------------------------------------------------//
82//                              Enum & Structs
83//---------------------------------------------------------------------------//
84
85/// Represents an entire Group Formations file decoded in memory.
86///
87/// Contains a list of formation templates that both AI and players can use to
88/// deploy armies on the battlefield for various tactical scenarios.
89#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
90#[getset(get = "pub", get_mut = "pub", set = "pub")]
91pub struct GroupFormations {
92    /// List of all formation definitions in the file.
93    formations: Vec<GroupFormation>,
94}
95
96/// A single formation definition specifying how units should be arranged.
97///
98/// Each formation includes AI usage criteria, unit requirements, and a set of
99/// formation blocks that define where different unit types should be positioned.
100#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
101#[getset(get = "pub", get_mut = "pub", set = "pub")]
102pub struct GroupFormation {
103    /// Name identifier for this formation.
104    name: String,
105
106    /// AI priority value for selecting this formation (higher = more preferred).
107    ai_priority: f32,
108
109    /// Bitflags indicating when this formation should be used (attack, defend, naval, etc.).
110    ai_purpose: AIPurpose,
111
112    /// Unknown field, present in Three Kingdoms.
113    uk_2: u32,
114
115    /// Minimum percentage requirements for unit categories in this formation.
116    min_unit_category_percentage: Vec<MinUnitCategoryPercentage>,
117
118    /// List of supported subcultures (introduced in Rome 2).
119    ///
120    /// If non-empty, this formation is only available to armies from these subcultures.
121    ai_supported_subcultures: Vec<String>,
122
123    /// List of supported factions (introduced in Rome 2).
124    ///
125    /// If non-empty, this formation is only available to armies from these factions.
126    ai_supported_factions: Vec<String>,
127
128    /// Formation blocks defining unit positions and arrangements.
129    group_formation_blocks: Vec<GroupFormationBlock>,
130}
131
132/// Specifies a minimum percentage requirement for a unit category in a formation.
133///
134/// For example, a formation might require at least 30% cavalry units.
135#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
136#[getset(get = "pub", get_mut = "pub", set = "pub")]
137pub struct MinUnitCategoryPercentage {
138    /// The unit category (cavalry, infantry melee, infantry ranged, etc.).
139    category: UnitCategory,
140
141    /// Minimum percentage (0-100) of the army that must belong to this category.
142    percentage: u32,
143}
144
145/// A formation block defining a specific position or region in the formation.
146///
147/// Each block has an ID and contains either absolute positioning, relative positioning
148/// to another block, or spans multiple other blocks.
149#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
150#[getset(get = "pub", get_mut = "pub", set = "pub")]
151pub struct GroupFormationBlock {
152    /// Unique identifier for this block, used for relative positioning references.
153    block_id: u32,
154
155    /// The block type and its associated data.
156    block: Block,
157}
158
159/// Types of formation blocks that can be used to define unit positions.
160///
161/// - `ContainerAbsolute`: Positioned at fixed coordinates.
162/// - `ContainerRelative`: Positioned relative to another block.
163/// - `Spanning`: Encompasses multiple other blocks.
164#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
165pub enum Block {
166    /// A container positioned at absolute coordinates.
167    ContainerAbsolute(ContainerAbsolute),
168
169    /// A container positioned relative to another block.
170    ContainerRelative(ContainerRelative),
171
172    /// A spanning block that encompasses multiple other blocks.
173    Spanning(Spanning)
174}
175
176/// A container block positioned at absolute coordinates on the battlefield.
177///
178/// Defines how units should be arranged at a specific location, including their
179/// spacing, arrangement pattern, and which types of units should occupy this position.
180#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
181#[getset(get = "pub", get_mut = "pub", set = "pub")]
182pub struct ContainerAbsolute {
183    /// Priority for filling this block (higher priority blocks are filled first).
184    block_priority: f32,
185
186    /// How units should be arranged (line, column, crescent, etc.).
187    entity_arrangement: EntityArrangement,
188
189    /// Spacing between units in this block.
190    inter_entity_spacing: f32,
191
192    /// Y-axis offset for crescent formations.
193    crescent_y_offset: f32,
194
195    /// X coordinate of this block's position.
196    position_x: f32,
197
198    /// Y coordinate of this block's position.
199    position_y: f32,
200
201    /// Minimum number of units required to use this block.
202    minimum_entity_threshold: i32,
203
204    /// Maximum number of units that can be placed in this block.
205    maximum_entity_threshold: i32,
206
207    /// Ordered list of preferred unit types for this block.
208    entity_preferences: Vec<EntityPreference>,
209}
210
211/// A container block positioned relative to another block.
212///
213/// Similar to `ContainerAbsolute` but positioned at an offset from a reference block,
214/// allowing formations to be built up from interconnected positioned blocks.
215#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
216#[getset(get = "pub", get_mut = "pub", set = "pub")]
217pub struct ContainerRelative {
218    /// Priority for filling this block (higher priority blocks are filled first).
219    block_priority: f32,
220
221    /// ID of the block this is positioned relative to.
222    relative_block_id: u32,
223
224    /// How units should be arranged (line, column, crescent, etc.).
225    entity_arrangement: EntityArrangement,
226
227    /// Spacing between units in this block.
228    inter_entity_spacing: f32,
229
230    /// Y-axis offset for crescent formations.
231    crescent_y_offset: f32,
232
233    /// X offset relative to the reference block.
234    position_x: f32,
235
236    /// Y offset relative to the reference block.
237    position_y: f32,
238
239    /// Minimum number of units required to use this block.
240    minimum_entity_threshold: i32,
241
242    /// Maximum number of units that can be placed in this block.
243    maximum_entity_threshold: i32,
244
245    /// Ordered list of preferred unit types for this block.
246    entity_preferences: Vec<EntityPreference>,
247}
248
249/// Defines a preference for a specific type of unit to occupy a formation block.
250///
251/// Multiple preferences can be defined in priority order, so the AI will try to place
252/// the highest priority matching units first.
253#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
254#[getset(get = "pub", get_mut = "pub", set = "pub")]
255pub struct EntityPreference {
256    /// Priority for this entity type (higher = more preferred).
257    priority: f32,
258
259    /// The type of unit entity (infantry, cavalry, artillery, etc.).
260    ///
261    /// Note: This is called EntityClass in Rome 2 and EntityDescription in Shogun 2,
262    /// but represents the same concept.
263    entity: Entity,
264
265    /// Weight class of the unit (light, medium, heavy, etc.). Introduced in Rome 2.
266    entity_weight: EntityWeight,
267
268    /// Unknown fields present in Three Kingdoms.
269    uk_1: u32,
270    /// Unknown field present in Three Kingdoms.
271    uk_2: u32,
272    /// Unknown field present in Three Kingdoms.
273    uk_3: u32,
274
275    /// Entity class string identifier (used in Three Kingdoms).
276    entity_class: String,
277}
278
279/// A spanning block that encompasses multiple other blocks.
280///
281/// Used to group related blocks together for organization or special handling.
282#[derive(Default, PartialEq, Clone, Debug, Getters, MutGetters, Setters, Serialize, Deserialize)]
283#[getset(get = "pub", get_mut = "pub", set = "pub")]
284pub struct Spanning {
285    /// IDs of the blocks that this spanning block encompasses.
286    spanned_block_ids: Vec<u32>,
287}
288
289/// AI purpose flags indicating when a formation should be used.
290///
291/// - V1: Shogun 2 flag layout (different bit assignments from Rome 2+).
292/// - V2: Rome 2 and later flag layout.
293#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
294pub enum AIPurpose {
295    V1(versions::v1::AIPurposeFlags),
296    V2(versions::v2::AIPurposeFlags),
297}
298
299/// How units should be arranged within a formation block (line, column, crescent, etc.).
300///
301/// Identical across all game versions.
302#[derive(Default, Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
303#[repr(u32)]
304pub enum EntityArrangement {
305    #[default] Line = 0,
306    Column = 1,
307    CrescentFront = 2,
308    CrescentBack = 3,
309}
310
311/// Unit category classifications (cavalry, infantry melee, infantry ranged, naval, etc.).
312///
313/// Identical across all game versions.
314#[derive(Default, Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
315#[repr(u32)]
316pub enum UnitCategory {
317    #[default] Cavalry = 0,
318    InvantryMelee = 13,
319    InfantryRanged = 14,
320    NavalHeavy = 15,
321    NavalMedium = 16,
322    NavalLight = 17,
323}
324
325/// Entity type classifications.
326///
327/// - V1: Shogun 2 entity types (65+ specific unit classes like CavalryHeavy, InfantryLine, etc.).
328/// - V2: Rome 2 and later entity types (18 abstract classes like InfMel, CavShk, etc.).
329#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
330pub enum Entity {
331    V1(versions::v1::EntityType),
332    V2(versions::v2::EntityType),
333}
334
335/// Entity weight classifications (light, medium, heavy, etc.).
336///
337/// Introduced in Rome 2. Identical across all post-Shogun 2 game versions.
338/// Shogun 2 does not use entity weights.
339#[derive(Default, Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
340#[repr(u32)]
341pub enum EntityWeight {
342    VeryLight = 0,
343    Light = 1,
344    Medium = 2,
345    Heavy = 3,
346    VeyHeavy = 4,
347    SuperHeavy = 5,
348    #[default] Any = 6,
349}
350
351//---------------------------------------------------------------------------//
352//                          Implementation of GroupFormations
353//---------------------------------------------------------------------------//
354
355impl Default for Block {
356    fn default() -> Self {
357        Self::ContainerAbsolute(ContainerAbsolute::default())
358    }
359}
360
361impl Default for AIPurpose {
362    fn default() -> Self {
363        Self::V1(versions::v1::AIPurposeFlags::default())
364    }
365}
366
367impl Default for Entity {
368    fn default() -> Self {
369        Self::V2(versions::v2::EntityType::default())
370    }
371}
372
373impl TryFrom<u32> for EntityArrangement {
374    type Error = RLibError;
375    fn try_from(value: u32) -> Result<Self> {
376        match value {
377            _ if value == Self::Line as u32 => Ok(Self::Line),
378            _ if value == Self::Column as u32 => Ok(Self::Column),
379            _ if value == Self::CrescentFront as u32 => Ok(Self::CrescentFront),
380            _ if value == Self::CrescentBack as u32 => Ok(Self::CrescentBack),
381            _ => Err(RLibError::DecodingGroupFormationsUnknownEnumValue("EntityArrangement".to_string(), value)),
382        }
383    }
384}
385
386impl From<EntityArrangement> for u32 {
387    fn from(value: EntityArrangement) -> u32 {
388        value as u32
389    }
390}
391
392impl TryFrom<u32> for UnitCategory {
393    type Error = RLibError;
394    fn try_from(value: u32) -> Result<Self> {
395        match value {
396            _ if value == Self::Cavalry as u32 => Ok(Self::Cavalry),
397            _ if value == Self::InvantryMelee as u32 => Ok(Self::InvantryMelee),
398            _ if value == Self::InfantryRanged as u32 => Ok(Self::InfantryRanged),
399            _ if value == Self::NavalHeavy as u32 => Ok(Self::NavalHeavy),
400            _ if value == Self::NavalMedium as u32 => Ok(Self::NavalMedium),
401            _ if value == Self::NavalLight as u32 => Ok(Self::NavalLight),
402            _ => Err(RLibError::DecodingGroupFormationsUnknownEnumValue("UnitCategory".to_string(), value)),
403        }
404    }
405}
406
407impl From<UnitCategory> for u32 {
408    fn from(value: UnitCategory) -> u32 {
409        value as u32
410    }
411}
412
413impl TryFrom<u32> for EntityWeight {
414    type Error = RLibError;
415    fn try_from(value: u32) -> Result<Self> {
416        match value {
417            _ if value == Self::VeryLight as u32 => Ok(Self::VeryLight),
418            _ if value == Self::Light as u32 => Ok(Self::Light),
419            _ if value == Self::Medium as u32 => Ok(Self::Medium),
420            _ if value == Self::Heavy as u32 => Ok(Self::Heavy),
421            _ if value == Self::VeyHeavy as u32 => Ok(Self::VeyHeavy),
422            _ if value == Self::SuperHeavy as u32 => Ok(Self::SuperHeavy),
423            _ if value == Self::Any as u32 => Ok(Self::Any),
424            _ => Err(RLibError::DecodingGroupFormationsUnknownEnumValue("EntityWeight".to_string(), value)),
425        }
426    }
427}
428
429impl From<EntityWeight> for u32 {
430    fn from(value: EntityWeight) -> u32 {
431        value as u32
432    }
433}
434
435impl Decodeable for GroupFormations {
436
437    fn decode<R: ReadBytes>(data: &mut R, extra_data: &Option<DecodeableExtraData>) -> Result<Self> {
438        let extra_data = extra_data.as_ref().ok_or(RLibError::DecodingMissingExtraData)?;
439        let game_info = extra_data.game_info.ok_or_else(|| RLibError::DecodingMissingExtraDataField("game_info".to_owned()))?;
440
441        let mut decoded = Self::default();
442        let data_len = data.len()?;
443
444        match game_info.key() {
445            KEY_PHARAOH_DYNASTIES |
446            KEY_PHARAOH |
447            KEY_TROY => decoded.decode_troy(data)?,
448            KEY_THREE_KINGDOMS |
449            KEY_WARHAMMER_3 => decoded.decode_wh3(data)?,
450            //KEY_WARHAMMER_2 |
451            //KEY_WARHAMMER |
452            KEY_THRONES_OF_BRITANNIA |
453            KEY_ATTILA |
454            KEY_ROME_2 => decoded.decode_rom_2(data)?,
455            KEY_SHOGUN_2 => decoded.decode_sho_2(data)?,
456            //KEY_NAPOLEON |
457            //KEY_EMPIRE => data.read_sized_string_u16()?,
458            _ => return Err(RLibError::DecodingUnsupportedGameSelected(game_info.key().to_string())),
459        }
460
461        check_size_mismatch(data.stream_position()? as usize, data_len as usize)?;
462
463        Ok(decoded)
464    }
465}
466
467impl Encodeable for GroupFormations {
468
469    fn encode<W: WriteBytes>(&mut self, buffer: &mut W, extra_data: &Option<EncodeableExtraData>) -> Result<()> {
470        let extra_data = extra_data.as_ref().ok_or(RLibError::EncodingMissingExtraData)?;
471        let game_info = extra_data.game_info.ok_or_else(|| RLibError::DecodingMissingExtraDataField("game_info".to_owned()))?;
472
473        match game_info.key() {
474            KEY_PHARAOH_DYNASTIES |
475            KEY_PHARAOH |
476            KEY_TROY => self.encode_troy(buffer)?,
477            KEY_THREE_KINGDOMS |
478            KEY_WARHAMMER_3 => self.encode_wh3(buffer)?,
479            //KEY_WARHAMMER_2 |
480            //KEY_WARHAMMER |
481            KEY_THRONES_OF_BRITANNIA |
482            KEY_ATTILA |
483            KEY_ROME_2 => self.encode_rom_2(buffer)?,
484            KEY_SHOGUN_2 => self.encode_sho_2(buffer)?,
485            //KEY_NAPOLEON |
486            //KEY_EMPIRE => buffer.write_sized_string_u16(formation.name())?,
487            _ => return Err(RLibError::DecodingUnsupportedGameSelected(game_info.key().to_string())),
488        };
489
490        Ok(())
491    }
492}