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}