Skip to main content

rpfm_extensions/startpos/world/
garrisons.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//! Garrisons of the settlements (`REGION/SETTLEMENT/GARRISON_RESIDENCE` #8 army, #9 navy).
12//!
13//! Each owned settlement gets a garrison army if any land unit comes out and a garrison navy if any ship does, each led by a
14//! generated `colonel`. Their units come from the armed citizenry of the settlement's buildings. Verified on
15//! Attila's main campaign: 173 of 175 settlements match. The other two (Ascaucalis and Eildon) differ by one unit
16//! for reasons not found in the tables.
17
18use getset::Getters;
19
20use std::collections::{HashMap, HashSet};
21
22use rpfm_lib::error::Result;
23
24use super::CampaignIndex;
25use crate::startpos::tables::StartposTables;
26
27#[cfg(test)] #[path = "garrisons_test.rs"] pub(crate) mod garrisons_test;
28
29//---------------------------------------------------------------------------//
30//                              Types
31//---------------------------------------------------------------------------//
32
33/// The garrison of a settlement.
34#[derive(Debug, Clone, PartialEq, Eq, Default, Getters)]
35#[getset(get = "pub")]
36pub struct Garrison {
37
38    /// Region key.
39    region: String,
40
41    /// Owner faction key.
42    faction: String,
43
44    /// Land units, in building order.
45    army: Vec<String>,
46
47    /// Ships, in building order.
48    navy: Vec<String>,
49}
50
51/// Unit data needed to build garrisons.
52struct UnitRules {
53
54    /// Building level and armed citizenry group of each `building_level_armed_citizenry_junctions` row, in the
55    /// table's key order (see [`crate::startpos::tables::Table::rows_in_key_order`]).
56    building_groups: Vec<(String, String)>,
57
58    /// Members of each armed citizenry group, sorted by priority and then row id.
59    group_members: HashMap<String, Vec<String>>,
60
61    /// Units allowed for each military group.
62    permissions: HashMap<String, HashSet<String>>,
63
64    /// Units that are ships.
65    naval: HashSet<String>,
66
67    /// Every unit key in `main_units`.
68    units: HashSet<String>,
69
70    /// Land units that can't move (fixed emplacements like the onager bastions), which never join a garrison.
71    static_units: HashSet<String>,
72}
73
74//---------------------------------------------------------------------------//
75//                              Implementations
76//---------------------------------------------------------------------------//
77
78/// Builds the garrisons of every settlement owned by a faction (rebel-owned settlements get none).
79///
80/// Every `building_level_armed_citizenry_junctions` row, in the table's key order, gives one unit per building of its
81/// level in the settlement: the group's own unit if the owner's military group allows it, otherwise the first allowed
82/// member of the group. Groups that aren't units, groups without allowed members and units that can't move are
83/// skipped. The game creates a garrison's units grouped by key in an order not reproduced yet.
84///
85/// # Arguments
86///
87/// * `tables` - Tables with `building_level_armed_citizenry_junctions`, `armed_citizenry_units_to_unit_groups_junctions`,
88///   `units_to_groupings_military_permissions`, `main_units`, `land_units` and `battlefield_engines`.
89/// * `index` - The campaign's startpos rows.
90///
91/// # Returns
92///
93/// One garrison per owned settlement, in settlement order.
94///
95/// # Errors
96///
97/// Returns an error if a table or column is missing.
98pub fn garrisons(tables: &StartposTables, index: &CampaignIndex) -> Result<Vec<Garrison>> {
99    let rules = UnitRules::new(tables)?;
100    let mut garrisons = vec![];
101
102    for settlement in index.settlements() {
103        let Some(faction) = index.region(settlement.region()).and_then(|region| region.owner().as_deref()).and_then(|key| index.faction(key)) else { continue };
104        let allowed = rules.permissions.get(faction.military_group());
105        let mut garrison = Garrison {
106            region: settlement.region().to_owned(),
107            faction: faction.key().to_owned(),
108            ..Default::default()
109        };
110
111        let buildings = settlement.all_buildings().collect::<Vec<_>>();
112        for (level, group) in &rules.building_groups {
113            for _ in buildings.iter().filter(|building| **building == level) {
114                if let Some(unit) = rules.unit_for_group(group, allowed) {
115                    if rules.naval.contains(unit) {
116                        garrison.navy.push(unit.to_owned());
117                    } else {
118                        garrison.army.push(unit.to_owned());
119                    }
120                }
121            }
122        }
123
124        garrisons.push(garrison);
125    }
126
127    Ok(garrisons)
128}
129
130impl UnitRules {
131
132    /// Collects the unit data from the tables.
133    fn new(tables: &StartposTables) -> Result<Self> {
134        let mut building_groups = vec![];
135        for row in tables.table("building_level_armed_citizenry_junctions")?.rows_in_key_order("id")? {
136            building_groups.push((row.str("building_level")?.into_owned(), row.str("unit_group")?.into_owned()));
137        }
138
139        let mut members = HashMap::<String, Vec<(i64, i64, String)>>::new();
140        for row in tables.table("armed_citizenry_units_to_unit_groups_junctions")?.rows() {
141            members.entry(row.str("unit_group")?.into_owned()).or_default().push((row.int("priority")?, row.int("id")?, row.str("unit")?.into_owned()));
142        }
143        let group_members = members.into_iter().map(|(group, mut list)| {
144            list.sort();
145            (group, list.into_iter().map(|(_, _, unit)| unit).collect())
146        }).collect();
147
148        let mut permissions = HashMap::<String, HashSet<String>>::new();
149        for row in tables.table("units_to_groupings_military_permissions")?.rows() {
150            permissions.entry(row.str("military_group")?.into_owned()).or_default().insert(row.str("unit")?.into_owned());
151        }
152
153        let mut fixed_engines = HashSet::new();
154        for row in tables.table("battlefield_engines")?.rows() {
155            if !row.bool("can_move")? {
156                fixed_engines.insert(row.str("key")?.into_owned());
157            }
158        }
159
160        let mut static_land_units = HashSet::new();
161        for row in tables.table("land_units")?.rows() {
162            if fixed_engines.contains(row.str("engine")?.as_ref()) {
163                static_land_units.insert(row.str("key")?.into_owned());
164            }
165        }
166
167        let mut naval = HashSet::new();
168        let mut units = HashSet::new();
169        let mut static_units = HashSet::new();
170        for row in tables.table("main_units")?.rows() {
171            let unit = row.str("unit")?.into_owned();
172            if row.bool("is_naval")? {
173                naval.insert(unit.to_owned());
174            } else if static_land_units.contains(row.str("land_unit")?.as_ref()) {
175                static_units.insert(unit.to_owned());
176            }
177            units.insert(unit);
178        }
179
180        Ok(Self { building_groups, group_members, permissions, naval, units, static_units })
181    }
182
183    /// Returns the unit an armed citizenry group gives to a faction with the given allowed units, if any.
184    fn unit_for_group<'a>(&'a self, group: &'a str, allowed: Option<&HashSet<String>>) -> Option<&'a str> {
185        if !self.units.contains(group) {
186            return None;
187        }
188
189        let is_allowed = |unit: &str| allowed.is_some_and(|allowed| allowed.contains(unit));
190        let unit = if is_allowed(group) {
191            group
192        } else {
193            self.group_members.get(group)?.iter().map(String::as_str).find(|unit| is_allowed(unit))?
194        };
195
196        (!self.static_units.contains(unit)).then_some(unit)
197    }
198}