Skip to main content

rpfm_extensions/startpos/world/writer/
forces.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//! Plan of the characters and forces of the world: their order, runtime ids and commanders.
12//!
13//! Character ids go first to the living table characters (faction by faction, the leader first), then to one colonel
14//! per garrison force. Force ids go first to the generals' forces (in their commanders' order), then to the garrison
15//! forces (faction by faction, region by region, the army before the navy).
16//!
17//! # Unit order
18//!
19//! Unit ids follow the order the game creates a force's units in: a general's in the key order of their
20//! `start_pos_land_units` / `start_pos_naval_units` rows (verified on Thrones, 62 of 62 forces), the bodyguard last.
21//! Garrisons follow `building_level_armed_citizenry_junctions` key order, while the game groups their units by key in
22//! an order not reproduced yet.
23//!
24//! The game then lists an army's units, after the commander's own, in a stable order of the `ui_unit_groupings.order`
25//! of their `main_units.ui_unit_group_land` (verified on Thrones, 125 of 125 forces, and Attila's main campaign,
26//! 341 of 342: the other is a navy). This plan sorts generals' armies that way.
27
28use std::collections::HashMap;
29
30use getset::{CopyGetters, Getters};
31
32use rpfm_lib::error::Result;
33
34use super::WorldPlan;
35use crate::startpos::tables::{Row, StartposTables};
36use crate::startpos::world::{CampaignIndex, CharacterEntry, FactionEntry};
37use crate::startpos::world::bodyguards::bodyguard;
38use crate::startpos::world::characters::{placement, Placement};
39use crate::startpos::world::garrisons::garrisons;
40use crate::startpos::world::government::faction_leader;
41use crate::startpos::world::legacy::ForceKind;
42
43#[cfg(test)] #[path = "forces_test.rs"] pub(crate) mod forces_test;
44
45/// Percentage of men of a full unit.
46const FULL_UNIT: u32 = 100;
47
48/// Agent type of generals.
49const GENERAL_AGENT: &str = "general";
50
51//---------------------------------------------------------------------------//
52//                              Types
53//---------------------------------------------------------------------------//
54
55/// Characters and forces of the world, in startpos order.
56#[derive(Debug, Clone, Default, Getters)]
57#[getset(get = "pub")]
58pub struct ForcePlan<'a> {
59
60    /// Characters, in id order.
61    characters: Vec<PlannedCharacter<'a>>,
62
63    /// Forces, in id order.
64    forces: Vec<PlannedForce<'a>>,
65
66    /// Place of each land unit in the game's listing order (`ui_unit_groupings.order` of its
67    /// `main_units.ui_unit_group_land`), by unit key.
68    #[getset(skip)]
69    list_rank: HashMap<String, i64>,
70}
71
72/// A character of the world.
73#[derive(Debug, Clone, Getters, CopyGetters)]
74pub struct PlannedCharacter<'a> {
75
76    /// Runtime id.
77    #[getset(get_copy = "pub")]
78    id: u32,
79
80    /// Faction of the character.
81    #[getset(get_copy = "pub")]
82    faction: &'a FactionEntry,
83
84    /// Faction the character was created in: its table faction, also for wives moved to their husband's.
85    #[getset(get_copy = "pub")]
86    origin: &'a FactionEntry,
87
88    /// Table row of the character, `None` for generated colonels.
89    #[getset(get_copy = "pub")]
90    entry: Option<&'a CharacterEntry>,
91
92    /// Position of the character's force in the plan, if it commands one.
93    #[getset(get_copy = "pub")]
94    force: Option<usize>,
95
96    /// The character was moved to its faction after the world was created, so it comes after the faction's own.
97    moved: bool,
98}
99
100/// A force of the world.
101#[derive(Debug, Clone, Getters, CopyGetters)]
102pub struct PlannedForce<'a> {
103
104    /// Runtime id.
105    #[getset(get_copy = "pub")]
106    id: u32,
107
108    /// Faction of the force.
109    #[getset(get_copy = "pub")]
110    faction: &'a FactionEntry,
111
112    /// Kind of force.
113    #[getset(get_copy = "pub")]
114    kind: ForceKind,
115
116    /// Region of the settlement a garrison force defends.
117    #[getset(get = "pub")]
118    region: Option<String>,
119
120    /// Region of the settlement a general's army starts in.
121    #[getset(get = "pub")]
122    stationed_in: Option<String>,
123
124    /// Units, in startpos order: a general's bodyguard or flagship first.
125    #[getset(get = "pub")]
126    units: Vec<PlannedUnit>,
127
128    /// Runtime ids of the units, in startpos order.
129    unit_ids: Vec<u32>,
130
131    /// Runtime id of the force's first unit.
132    #[getset(get_copy = "pub")]
133    first_unit_id: u32,
134
135    /// Position of the force's commander in the plan.
136    #[getset(get_copy = "pub")]
137    commander: usize,
138
139    /// The force was moved to its faction with its commander, so it comes after the faction's own.
140    moved: bool,
141}
142
143/// A unit of a force.
144#[derive(Debug, Clone, PartialEq, Eq, Getters, CopyGetters)]
145pub struct PlannedUnit {
146
147    /// Unit key.
148    #[getset(get = "pub")]
149    unit: String,
150
151    /// Percentage of the unit's men present (`start_pos_land_units.soldiers`; 100 for the others).
152    #[getset(get_copy = "pub")]
153    soldiers: u32,
154
155    /// The unit is the commander's own: a general's bodyguard or flagship, or a colonel's first unit.
156    #[getset(get_copy = "pub")]
157    commands: bool,
158}
159
160//---------------------------------------------------------------------------//
161//                              Implementations
162//---------------------------------------------------------------------------//
163
164impl<'a> ForcePlan<'a> {
165
166    /// Plans the characters and forces of a world.
167    ///
168    /// # Arguments
169    ///
170    /// * `tables` - Tables of the game and the campaign.
171    /// * `index` - The campaign's startpos rows.
172    /// * `plan` - Order of the factions and regions.
173    ///
174    /// # Errors
175    ///
176    /// Returns an error if a table or column is missing.
177    pub fn new(tables: &StartposTables, index: &'a CampaignIndex, plan: &WorldPlan<'a>) -> Result<Self> {
178        let mut land_units = HashMap::<String, Vec<PlannedUnit>>::new();
179        for row in unit_rows(tables, "start_pos_land_units")? {
180            let soldiers = u32::try_from(row.int("soldiers")?).unwrap_or(FULL_UNIT);
181            land_units.entry(row.str("general")?.into_owned()).or_default().push(PlannedUnit::new(row.str("unit_type")?.into_owned(), soldiers));
182        }
183        let mut naval_units = HashMap::<String, Vec<PlannedUnit>>::new();
184        for row in unit_rows(tables, "start_pos_naval_units")? {
185            naval_units.entry(row.str("admiral")?.into_owned()).or_default().push(PlannedUnit::new(row.str("unit_type")?.into_owned(), FULL_UNIT));
186        }
187
188        let mut force_plan = Self { list_rank: list_ranks(tables), ..Self::default() };
189        for faction in plan.factions() {
190            let bodyguards = [bodyguard(tables, faction, false)?, bodyguard(tables, faction, true)?];
191            for character in characters_in_startpos_order(index, plan, faction) {
192                let placement = placement(tables, index, character)?;
193                let override_unit = Some(character.override_general_unit()).filter(|unit| !unit.is_empty());
194                let (kind, units) = match (land_units.get(character.id()), naval_units.get(character.id())) {
195                    (Some(units), _) => (Some(ForceKind::Army), units.as_slice()),
196                    (None, Some(units)) => (Some(ForceKind::Navy), units.as_slice()),
197                    // Generals with their own bodyguard unit lead an army of it even without unit rows.
198                    (None, None) if override_unit.is_some() && *character.agent() == GENERAL_AGENT => (Some(ForceKind::Army), [].as_slice()),
199                    (None, None) => (None, [].as_slice()),
200                };
201                let commander = force_plan.characters.len();
202                let force = kind.map(|kind| {
203                    let own_unit = override_unit.cloned().or_else(|| bodyguards[usize::from(kind == ForceKind::Navy)].as_ref().map(|bodyguard| bodyguard.unit().to_owned()));
204                    let mut all_units = own_unit.map(|unit| PlannedUnit { unit, soldiers: FULL_UNIT, commands: true }).into_iter().collect::<Vec<_>>();
205                    all_units.extend(units.iter().cloned());
206                    force_plan.push_force(faction, kind, None, all_units, commander)
207                });
208                if let (Some(force), Placement::Settlement(region)) = (force, placement) {
209                    if force_plan.forces[force].kind == ForceKind::Army {
210                        force_plan.forces[force].stationed_in = Some(region);
211                    }
212                }
213                force_plan.push_character(faction, Some(character), force);
214            }
215        }
216
217        let garrisons = garrisons(tables, index)?.into_iter().map(|garrison| (garrison.region().to_owned(), garrison)).collect::<HashMap<_, _>>();
218        for faction in plan.factions() {
219            for region in plan.regions().iter().filter(|region| region.owner().as_deref() == Some(faction.key().as_str())) {
220                let Some(garrison) = garrisons.get(region.key()) else { continue };
221
222                // Settlements without armed citizenry (Thrones' minor ones) have no garrison army.
223                if !garrison.army().is_empty() {
224                    force_plan.push_garrison(faction, ForceKind::GarrisonArmy, region.key(), garrison.army().to_vec());
225                }
226                if !garrison.navy().is_empty() {
227                    force_plan.push_garrison(faction, ForceKind::GarrisonNavy, region.key(), garrison.navy().to_vec());
228                }
229            }
230        }
231
232        force_plan.move_wives(index);
233        Ok(force_plan)
234    }
235
236    /// Moves the women married to a man of another faction, with their forces, to their husband's faction.
237    fn move_wives(&mut self, index: &'a CampaignIndex) {
238        for position in 0..self.characters.len() {
239            let character = &self.characters[position];
240            let Some(husband_faction) = character.entry.and_then(|entry| entry.husband_faction().as_deref()).and_then(|key| index.faction(key)) else { continue };
241            let force = character.force;
242            self.characters[position].faction = husband_faction;
243            self.characters[position].moved = true;
244            if let Some(force) = force {
245                self.forces[force].faction = husband_faction;
246                self.forces[force].moved = true;
247            }
248        }
249    }
250
251    /// Adds a garrison force and its colonel, who commands its first unit.
252    fn push_garrison(&mut self, faction: &'a FactionEntry, kind: ForceKind, region: &str, units: Vec<String>) {
253        let commander = self.characters.len();
254        let units = units.into_iter().enumerate().map(|(index, unit)| PlannedUnit { unit, soldiers: FULL_UNIT, commands: index == 0 }).collect();
255        let force = self.push_force(faction, kind, Some(region.to_owned()), units, commander);
256        self.push_character(faction, None, Some(force));
257    }
258
259    /// Adds a force, returning its position. Unit ids continue the ones of the previous force.
260    fn push_force(&mut self, faction: &'a FactionEntry, kind: ForceKind, region: Option<String>, units: Vec<PlannedUnit>, commander: usize) -> usize {
261        let id = u32::try_from(self.forces.len() + 1).unwrap_or(u32::MAX);
262        let first_unit_id = self.forces.last().map(|force| force.first_unit_id + u32::try_from(force.units.len()).unwrap_or(u32::MAX)).unwrap_or(1);
263        let mut force = PlannedForce { id, faction, kind, region, stationed_in: None, units, unit_ids: vec![], first_unit_id, commander, moved: false };
264        force.unit_ids = force.creation_ids();
265
266        // Armies list their units after the commander's own in a stable order of their listing rank.
267        if kind == ForceKind::Army && !self.list_rank.is_empty() {
268            let skip = usize::from(force.units.first().is_some_and(|unit| unit.commands));
269            let mut listed = force.units.drain(skip..).zip(force.unit_ids.drain(skip..)).collect::<Vec<_>>();
270            listed.sort_by_key(|(unit, _)| self.list_rank.get(&unit.unit).copied().unwrap_or(i64::MAX));
271            for (unit, id) in listed {
272                force.units.push(unit);
273                force.unit_ids.push(id);
274            }
275        }
276        self.forces.push(force);
277        self.forces.len() - 1
278    }
279
280    /// Adds a character.
281    fn push_character(&mut self, faction: &'a FactionEntry, entry: Option<&'a CharacterEntry>, force: Option<usize>) {
282        let id = u32::try_from(self.characters.len() + 1).unwrap_or(u32::MAX);
283        self.characters.push(PlannedCharacter { id, faction, origin: faction, entry, force, moved: false });
284    }
285
286    /// Returns the positions in the plan of the characters of a faction, in startpos order: its own, then the ones
287    /// moved to it.
288    pub fn character_positions(&self, faction: &str) -> Vec<usize> {
289        let of = |moved: bool| self.characters.iter().enumerate()
290            .filter(move |(_, character)| *character.faction.key() == faction && character.moved == moved)
291            .map(|(position, _)| position);
292        of(false).chain(of(true)).collect()
293    }
294
295    /// Returns the characters of a faction, in startpos order: its own, then the ones moved to it.
296    pub fn characters_of(&self, faction: &str) -> impl Iterator<Item = &PlannedCharacter<'a>> {
297        self.character_positions(faction).into_iter().map(|position| &self.characters[position])
298    }
299
300    /// Returns the forces of a faction, in startpos order: its own, then the ones moved to it.
301    pub fn forces_of(&self, faction: &str) -> impl Iterator<Item = &PlannedForce<'a>> {
302        let of = |moved: bool| {
303            let faction = faction.to_owned();
304            self.forces.iter().filter(move |force| *force.faction.key() == faction && force.moved == moved)
305        };
306        of(false).chain(of(true))
307    }
308
309    /// Returns the first general's army starting in a region's settlement, if any.
310    pub fn stationed_in(&self, region: &str) -> Option<&PlannedForce<'a>> {
311        self.forces.iter().find(|force| force.stationed_in.as_deref() == Some(region))
312    }
313
314    /// Returns the garrison force of a kind defending a region, if any.
315    pub fn garrison_of(&self, region: &str, kind: ForceKind) -> Option<&PlannedForce<'a>> {
316        self.forces.iter().find(|force| force.kind == kind && force.region.as_deref() == Some(region))
317    }
318}
319
320impl PlannedForce<'_> {
321
322    /// Returns the runtime ids of the force's units, in startpos order.
323    pub fn unit_ids(&self) -> Vec<u32> {
324        self.unit_ids.clone()
325    }
326
327    /// Returns the runtime ids of the force's units in creation order. A general's own unit is created after the
328    /// others, so it takes the last id although it comes first.
329    fn creation_ids(&self) -> Vec<u32> {
330        let count = u32::try_from(self.units.len()).unwrap_or(u32::MAX);
331        let general = matches!(self.kind, ForceKind::Army | ForceKind::Navy) && self.units.first().is_some_and(|unit| unit.commands);
332        (0..count).map(|index| match (general, index) {
333            (true, 0) => self.first_unit_id + count - 1,
334            (true, index) => self.first_unit_id + index - 1,
335            (false, index) => self.first_unit_id + index,
336        }).collect()
337    }
338
339    /// Returns the runtime id of the commander's own unit, if the force has one.
340    pub fn commander_unit_id(&self) -> Option<u32> {
341        self.units.iter().position(|unit| unit.commands).and_then(|position| self.unit_ids().get(position).copied())
342    }
343}
344
345impl PlannedUnit {
346
347    /// Creates a unit that isn't its commander's.
348    fn new(unit: String, soldiers: u32) -> Self {
349        Self { unit, soldiers, commands: false }
350    }
351}
352
353/// Returns the place of each land unit in the game's listing order, by unit key: the `ui_unit_groupings.order` of its
354/// `main_units.ui_unit_group_land`. Empty if the game lacks the columns.
355fn list_ranks(tables: &StartposTables) -> HashMap<String, i64> {
356    let ranks = || -> Result<HashMap<String, i64>> {
357        let mut group_order = HashMap::new();
358        for row in tables.table("ui_unit_groupings")?.rows() {
359            group_order.insert(row.str("key")?.into_owned(), row.int("order")?);
360        }
361        let mut ranks = HashMap::new();
362        for row in tables.table("main_units")?.rows() {
363            if let Some(order) = group_order.get(row.str("ui_unit_group_land")?.as_ref()) {
364                ranks.insert(row.str("unit")?.into_owned(), *order);
365            }
366        }
367        Ok(ranks)
368    };
369    ranks().unwrap_or_default()
370}
371
372/// Returns the rows of a startpos unit table in the order the game creates their units: the table's key order (see
373/// [`crate::startpos::tables::Table::rows_in_key_order`]) of its id column (`id`, or `ID` in some games).
374fn unit_rows<'t>(tables: &'t StartposTables, name: &str) -> Result<Vec<Row<'t>>> {
375    let table = tables.table(name)?;
376    let column = if table.column("id").is_ok() { "id" } else { "ID" };
377    table.rows_in_key_order(column)
378}
379
380/// Returns the living characters of a faction in startpos order: the plan's order if it has one for the faction,
381/// otherwise the leader, then the others in table order (the game uses a hash order for them). Living characters
382/// left out of the plan's order go after it, in table order.
383fn characters_in_startpos_order<'a>(index: &'a CampaignIndex, plan: &WorldPlan<'a>, faction: &'a FactionEntry) -> Vec<&'a CharacterEntry> {
384    let first = match plan.character_order(faction.key()) {
385        Some(order) => order.to_vec(),
386        None => faction_leader(index, faction.key()).into_iter().collect(),
387    };
388    let mut characters = Vec::<&CharacterEntry>::new();
389    for character in first.into_iter().chain(index.characters_of(faction.key()).filter(|character| character.death_type().is_empty())) {
390        if !characters.contains(&character) {
391            characters.push(character);
392        }
393    }
394    characters
395}