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 and, if any ship comes out, a garrison navy, 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    /// Armed citizenry groups of each building level, with repeats, in table order.
55    building_groups: HashMap<String, Vec<String>>,
56
57    /// Members of each armed citizenry group, sorted by priority and then row id.
58    group_members: HashMap<String, Vec<String>>,
59
60    /// Units allowed for each military group.
61    permissions: HashMap<String, HashSet<String>>,
62
63    /// Units that are ships.
64    naval: HashSet<String>,
65
66    /// Every unit key in `main_units`.
67    units: HashSet<String>,
68
69    /// Land units that can't move (fixed emplacements like the onager bastions), which never join a garrison.
70    static_units: HashSet<String>,
71}
72
73//---------------------------------------------------------------------------//
74//                              Implementations
75//---------------------------------------------------------------------------//
76
77/// Builds the garrisons of every settlement owned by a faction (rebel-owned settlements get none).
78///
79/// For each building of the settlement (primary, port, then the others), every armed citizenry group of its level
80/// gives one unit: the group's own unit if the owner's military group allows it, otherwise the first allowed member
81/// of the group. Groups that aren't units, groups without allowed members and units that can't move are skipped.
82///
83/// # Arguments
84///
85/// * `tables` - Tables with `building_level_armed_citizenry_junctions`, `armed_citizenry_units_to_unit_groups_junctions`,
86///   `units_to_groupings_military_permissions`, `main_units`, `land_units` and `battlefield_engines`.
87/// * `index` - The campaign's startpos rows.
88///
89/// # Returns
90///
91/// One garrison per owned settlement, in settlement order.
92///
93/// # Errors
94///
95/// Returns an error if a table or column is missing.
96pub fn garrisons(tables: &StartposTables, index: &CampaignIndex) -> Result<Vec<Garrison>> {
97    let rules = UnitRules::new(tables)?;
98    let mut garrisons = vec![];
99
100    for settlement in index.settlements() {
101        let Some(faction) = index.region(settlement.region()).and_then(|region| region.owner().as_deref()).and_then(|key| index.faction(key)) else { continue };
102        let allowed = rules.permissions.get(faction.military_group());
103        let mut garrison = Garrison {
104            region: settlement.region().to_owned(),
105            faction: faction.key().to_owned(),
106            ..Default::default()
107        };
108
109        for building in settlement.all_buildings() {
110            for group in rules.building_groups.get(building).into_iter().flatten() {
111                if let Some(unit) = rules.unit_for_group(group, allowed) {
112                    if rules.naval.contains(unit) {
113                        garrison.navy.push(unit.to_owned());
114                    } else {
115                        garrison.army.push(unit.to_owned());
116                    }
117                }
118            }
119        }
120
121        garrisons.push(garrison);
122    }
123
124    Ok(garrisons)
125}
126
127impl UnitRules {
128
129    /// Collects the unit data from the tables.
130    fn new(tables: &StartposTables) -> Result<Self> {
131        let mut building_groups = HashMap::<String, Vec<String>>::new();
132        for row in tables.table("building_level_armed_citizenry_junctions")?.rows() {
133            building_groups.entry(row.str("building_level")?.into_owned()).or_default().push(row.str("unit_group")?.into_owned());
134        }
135
136        let mut members = HashMap::<String, Vec<(i64, i64, String)>>::new();
137        for row in tables.table("armed_citizenry_units_to_unit_groups_junctions")?.rows() {
138            members.entry(row.str("unit_group")?.into_owned()).or_default().push((row.int("priority")?, row.int("id")?, row.str("unit")?.into_owned()));
139        }
140        let group_members = members.into_iter().map(|(group, mut list)| {
141            list.sort();
142            (group, list.into_iter().map(|(_, _, unit)| unit).collect())
143        }).collect();
144
145        let mut permissions = HashMap::<String, HashSet<String>>::new();
146        for row in tables.table("units_to_groupings_military_permissions")?.rows() {
147            permissions.entry(row.str("military_group")?.into_owned()).or_default().insert(row.str("unit")?.into_owned());
148        }
149
150        let mut fixed_engines = HashSet::new();
151        for row in tables.table("battlefield_engines")?.rows() {
152            if !row.bool("can_move")? {
153                fixed_engines.insert(row.str("key")?.into_owned());
154            }
155        }
156
157        let mut static_land_units = HashSet::new();
158        for row in tables.table("land_units")?.rows() {
159            if fixed_engines.contains(row.str("engine")?.as_ref()) {
160                static_land_units.insert(row.str("key")?.into_owned());
161            }
162        }
163
164        let mut naval = HashSet::new();
165        let mut units = HashSet::new();
166        let mut static_units = HashSet::new();
167        for row in tables.table("main_units")?.rows() {
168            let unit = row.str("unit")?.into_owned();
169            if row.bool("is_naval")? {
170                naval.insert(unit.to_owned());
171            } else if static_land_units.contains(row.str("land_unit")?.as_ref()) {
172                static_units.insert(unit.to_owned());
173            }
174            units.insert(unit);
175        }
176
177        Ok(Self { building_groups, group_members, permissions, naval, units, static_units })
178    }
179
180    /// Returns the unit an armed citizenry group gives to a faction with the given allowed units, if any.
181    fn unit_for_group<'a>(&'a self, group: &'a str, allowed: Option<&HashSet<String>>) -> Option<&'a str> {
182        if !self.units.contains(group) {
183            return None;
184        }
185
186        let is_allowed = |unit: &str| allowed.is_some_and(|allowed| allowed.contains(unit));
187        let unit = if is_allowed(group) {
188            group
189        } else {
190            self.group_members.get(group)?.iter().map(String::as_str).find(|unit| is_allowed(unit))?
191        };
192
193        (!self.static_units.contains(unit)).then_some(unit)
194    }
195}