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