Skip to main content

rpfm_extensions/startpos/world/
legacy.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//! Army legacies (`MILITARY_FORCE/MILITARY_FORCE_LEGACY`): the name, kind, emblem and number of each force.
12//!
13//! Garrisons get fixed names. Generals get their `legacy_override` text if the character has one, otherwise a name
14//! of their subculture never used before in the faction. Generals also get an emblem. The game draws the names and
15//! emblems from its RNG; these functions take the first unused ones in table order. Verified on Attila's main
16//! campaign: kinds, override names and numbers match for 343 of 343 forces, and every drawn name and emblem comes
17//! from the expected pool without repeats.
18
19use getset::{CopyGetters, Getters};
20
21use std::collections::HashSet;
22
23use rpfm_lib::error::Result;
24
25use super::{CampaignIndex, FactionEntry};
26use crate::startpos::tables::StartposTables;
27
28#[cfg(test)] #[path = "legacy_test.rs"] pub(crate) mod legacy_test;
29
30/// Localisation key prefix of the names of `military_force_legacy_names`.
31const NAME_KEY_PREFIX: &str = "military_force_legacy_names_localised_name_";
32
33/// Localisation key prefix of the `start_pos_characters.legacy_override` texts.
34const OVERRIDE_KEY_PREFIX: &str = "start_pos_characters_legacy_override_";
35
36/// Name of garrison armies.
37const GARRISON_ARMY_NAME: &str = "random_localisation_strings_string_military_force_legacy_name_garrison_army";
38
39/// Name of garrison navies.
40const GARRISON_NAVY_NAME: &str = "random_localisation_strings_string_military_force_legacy_name_garrison_navy";
41
42//---------------------------------------------------------------------------//
43//                              Types
44//---------------------------------------------------------------------------//
45
46/// The kind of a force, as the legacy stores it (`MILITARY_FORCE_LEGACY` #4).
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ForceKind {
49
50    /// A general's army.
51    Army = 0,
52
53    /// A general's navy.
54    Navy = 1,
55
56    /// A settlement's garrison army.
57    GarrisonArmy = 2,
58
59    /// A settlement's garrison navy.
60    GarrisonNavy = 3,
61}
62
63/// A force of a faction, as needed to name it.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct LegacyForce {
66
67    /// Kind of force.
68    pub kind: ForceKind,
69
70    /// `start_pos_characters` id of the commanding general, for armies and navies.
71    pub general: Option<String>,
72}
73
74/// The legacy of a force.
75#[derive(Debug, Clone, PartialEq, Eq, Default, Getters, CopyGetters)]
76pub struct Legacy {
77
78    /// Localisation key of the force's name.
79    #[getset(get = "pub")]
80    name: String,
81
82    /// The name's key again for forces named by a `legacy_override`, empty otherwise (`MILITARY_FORCE_LEGACY` #3).
83    #[getset(get = "pub")]
84    override_name: String,
85
86    /// Emblem key, empty for garrisons.
87    #[getset(get = "pub")]
88    emblem: String,
89
90    /// Force number: overridden legacies are numbered 1, 2... in force order; the others are 1.
91    #[getset(get_copy = "pub")]
92    number: u32,
93}
94
95//---------------------------------------------------------------------------//
96//                              Implementations
97//---------------------------------------------------------------------------//
98
99/// Returns the legacies of a faction's forces.
100///
101/// # Arguments
102///
103/// * `tables` - Tables with `military_force_legacy_names` and `military_force_legacy_emblems`.
104/// * `index` - The campaign's startpos rows.
105/// * `faction` - The faction.
106/// * `forces` - The faction's forces, in startpos order.
107/// * `overrides` - `start_pos_characters` ids of the characters with a `legacy_override` text (a localised column,
108///   so it comes from the campaign's localisation, not from the table).
109///
110/// # Returns
111///
112/// One legacy per force, in the same order.
113///
114/// # Errors
115///
116/// Returns an error if a table or column is missing.
117pub fn legacies(tables: &StartposTables, index: &CampaignIndex, faction: &FactionEntry, forces: &[LegacyForce], overrides: &HashSet<String>) -> Result<Vec<Legacy>> {
118    let mut army_names = name_pool(tables, index, faction, true)?.into_iter();
119    let mut navy_names = name_pool(tables, index, faction, false)?.into_iter();
120    let mut army_emblems = emblem_pool(tables, index, faction, true)?.into_iter();
121    let mut navy_emblems = emblem_pool(tables, index, faction, false)?.into_iter();
122    let mut override_count = 0;
123
124    Ok(forces.iter().map(|force| {
125        let overridden = force.general.as_ref().filter(|general| overrides.contains(*general));
126        match (force.kind, overridden) {
127            (ForceKind::GarrisonArmy, _) => Legacy { name: GARRISON_ARMY_NAME.to_owned(), number: 1, ..Default::default() },
128            (ForceKind::GarrisonNavy, _) => Legacy { name: GARRISON_NAVY_NAME.to_owned(), number: 1, ..Default::default() },
129            (kind, Some(general)) => {
130                override_count += 1;
131                let name = format!("{OVERRIDE_KEY_PREFIX}{general}");
132                Legacy {
133                    override_name: name.to_owned(),
134                    name,
135                    emblem: next_emblem(kind, &mut army_emblems, &mut navy_emblems),
136                    number: override_count,
137                }
138            },
139            (kind, None) => Legacy {
140                name: if kind == ForceKind::Navy { navy_names.next() } else { army_names.next() }.unwrap_or_default(),
141                emblem: next_emblem(kind, &mut army_emblems, &mut navy_emblems),
142                number: 1,
143                ..Default::default()
144            },
145        }
146    }).collect())
147}
148
149/// Returns the skill set of a faction's armies or navies (`MILITARY_FORCE_LEGACY/CAMPAIGN_SKILLS` #1).
150///
151/// The key replaces `sub_cult_` in the subculture key with `army_legacy_` or `navy_legacy_`
152/// (`att_sub_cult_roman_west` gives `att_army_legacy_roman_west`).
153pub fn legacy_skill_set(faction: &FactionEntry, navy: bool) -> String {
154    faction.subculture().replace("sub_cult_", if navy { "navy_legacy_" } else { "army_legacy_" })
155}
156
157/// Returns the name localisation keys available to a faction's armies or navies.
158///
159/// # Arguments
160///
161/// * `tables` - Tables with `military_force_legacy_names`.
162/// * `index` - The campaign's startpos rows.
163/// * `faction` - The faction.
164/// * `for_army` - `true` for army names, `false` for navy names.
165///
166/// # Returns
167///
168/// The keys, in table order.
169///
170/// # Errors
171///
172/// Returns an error if a table or column is missing.
173pub fn name_pool(tables: &StartposTables, index: &CampaignIndex, faction: &FactionEntry, for_army: bool) -> Result<Vec<String>> {
174    let mut names = vec![];
175    for row in tables.table("military_force_legacy_names")?.rows() {
176        let campaign = row.str("campaign")?;
177        if row.str("subculture")? == *faction.subculture() && row.bool("for_army")? == for_army && (campaign.is_empty() || campaign == *index.campaign()) {
178            names.push(format!("{NAME_KEY_PREFIX}{}{}", row.str("key")?, faction.subculture()));
179        }
180    }
181    Ok(names)
182}
183
184/// Returns the emblems available to a faction's armies or navies.
185///
186/// They're the rows of the faction, of its subculture (rows without faction) and of its culture (rows without
187/// faction and subculture), with no campaign or the current one.
188///
189/// # Arguments
190///
191/// * `tables` - Tables with `military_force_legacy_emblems`.
192/// * `index` - The campaign's startpos rows.
193/// * `faction` - The faction.
194/// * `is_army` - `true` for army emblems, `false` for navy emblems.
195///
196/// # Returns
197///
198/// The emblem keys, in table order.
199///
200/// # Errors
201///
202/// Returns an error if a table or column is missing.
203pub fn emblem_pool(tables: &StartposTables, index: &CampaignIndex, faction: &FactionEntry, is_army: bool) -> Result<Vec<String>> {
204    let mut emblems = vec![];
205    for row in tables.table("military_force_legacy_emblems")?.rows() {
206        let campaign = row.str("campaign")?;
207        let row_faction = row.str("faction_key")?;
208        let row_subculture = row.str("subculture_key")?;
209        let matches = row_faction == *faction.key()
210            || (row_faction.is_empty() && row_subculture == *faction.subculture())
211            || (row_faction.is_empty() && row_subculture.is_empty() && row.str("culure_key")? == *faction.culture());
212        if matches && row.bool("is_army")? == is_army && (campaign.is_empty() || campaign == *index.campaign()) {
213            emblems.push(row.str("key")?.into_owned());
214        }
215    }
216    Ok(emblems)
217}
218
219/// Takes the next unused emblem for a general's army or navy.
220fn next_emblem(kind: ForceKind, army: &mut impl Iterator<Item = String>, navy: &mut impl Iterator<Item = String>) -> String {
221    if kind == ForceKind::Navy { navy.next() } else { army.next() }.unwrap_or_default()
222}