Skip to main content

rpfm_extensions/startpos/world/
bodyguards.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//! Default bodyguard unit of each faction (`CHARACTER_DETAILS/GENERAL_BODYGUARD_DETAILS`).
12//!
13//! Generals without an army store their bodyguard unit and its size; generals leading an army store an empty
14//! record (their bodyguard is the army's first unit). Verified on Attila's main campaign: 43 of 43 factions with
15//! army-less generals match.
16
17use getset::{CopyGetters, Getters};
18
19use std::collections::{HashMap, HashSet};
20
21use rpfm_lib::error::Result;
22
23use super::FactionEntry;
24use crate::startpos::tables::StartposTables;
25
26#[cfg(test)] #[path = "bodyguards_test.rs"] mod bodyguards_test;
27
28//---------------------------------------------------------------------------//
29//                              Types
30//---------------------------------------------------------------------------//
31
32/// The default bodyguard of a faction's generals.
33#[derive(Debug, Clone, PartialEq, Eq, Default, Getters, CopyGetters)]
34pub struct Bodyguard {
35
36    /// Unit key.
37    #[getset(get = "pub")]
38    unit: String,
39
40    /// Men in the unit (`main_units.num_men`), stored twice in the startpos (current and maximum).
41    #[getset(get_copy = "pub")]
42    men: u32,
43}
44
45//---------------------------------------------------------------------------//
46//                              Implementations
47//---------------------------------------------------------------------------//
48
49/// Returns the default bodyguard of a faction's generals, or the flagship of its admirals.
50///
51/// The candidates are the `commander_unit_permissions` units of the faction, of its subculture (rows without
52/// faction) and of its culture (rows without faction and subculture), allowed for the faction's military group
53/// and naval only for flagships. The bodyguard is the candidate no other candidate upgrades into
54/// (`technology_unit_upgrades`), that is, the start of the upgrade chain; the first one in table order if there are
55/// several.
56///
57/// # Arguments
58///
59/// * `tables` - Tables with `commander_unit_permissions`, `units_to_groupings_military_permissions`,
60///   `technology_unit_upgrades` and `main_units`.
61/// * `faction` - The faction.
62/// * `naval` - Look for a flagship instead of a land bodyguard.
63///
64/// # Returns
65///
66/// The bodyguard, or `None` if the faction has no candidate.
67///
68/// # Errors
69///
70/// Returns an error if a table or column is missing.
71pub fn bodyguard(tables: &StartposTables, faction: &FactionEntry, naval: bool) -> Result<Option<Bodyguard>> {
72    let mut allowed = HashSet::new();
73    for row in tables.table("units_to_groupings_military_permissions")?.rows() {
74        if row.str("military_group")? == *faction.military_group() {
75            allowed.insert(row.str("unit")?.into_owned());
76        }
77    }
78
79    let mut naval_units = HashSet::new();
80    let mut men = HashMap::new();
81    for row in tables.table("main_units")?.rows() {
82        let unit = row.str("unit")?.into_owned();
83        if row.bool("is_naval")? {
84            naval_units.insert(unit.to_owned());
85        }
86        men.insert(unit, row.int("num_men")?);
87    }
88
89    let mut candidates = vec![];
90    for row in tables.table("commander_unit_permissions")?.rows() {
91        let row_faction = row.str("faction_key")?;
92        let row_subculture = row.str("subculture_key")?;
93        let matches = row_faction == *faction.key()
94            || (row_faction.is_empty() && row_subculture == *faction.subculture())
95            || (row_faction.is_empty() && row_subculture.is_empty() && row.str("culture_key")? == *faction.culture());
96
97        let unit = row.str("unit_key")?.into_owned();
98        if matches && allowed.contains(&unit) && naval_units.contains(&unit) == naval && !candidates.contains(&unit) {
99            candidates.push(unit);
100        }
101    }
102
103    let mut upgraded = HashSet::new();
104    for row in tables.table("technology_unit_upgrades")?.rows() {
105        if candidates.contains(&row.str("unit")?.into_owned()) {
106            upgraded.insert(row.str("target_unit")?.into_owned());
107        }
108    }
109
110    Ok(candidates.into_iter()
111        .find(|unit| !upgraded.contains(unit))
112        .map(|unit| Bodyguard {
113            men: men.get(&unit).and_then(|&men| u32::try_from(men).ok()).unwrap_or_default(),
114            unit,
115        }))
116}