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}