Skip to main content

rpfm_extensions/startpos/world/
factions.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//! Per-faction values of the `FACTION` record that come from the tables: flags and colours
12//! (`FACTION_FLAG_AND_COLOURS`), capital, owned regions, victory regions, overlord and subjects, horde state and
13//! progenitor.
14//!
15//! Verified on Attila's main campaign: 118 of 118 factions match. Victory regions match as sets (the game stores
16//! 6 of them in another order).
17
18use getset::{CopyGetters, Getters};
19
20use rpfm_lib::error::{RLibError, Result};
21
22use super::{CampaignIndex, FactionEntry};
23use crate::startpos::tables::{Row, StartposTables};
24
25#[cfg(test)] #[path = "factions_test.rs"] mod factions_test;
26
27//---------------------------------------------------------------------------//
28//                              Types
29//---------------------------------------------------------------------------//
30
31/// A flag with its colours (`FACTION_FLAG_AND_COLOURS`).
32#[derive(Debug, Clone, PartialEq, Eq, Default, Getters, CopyGetters)]
33pub struct FactionFlag {
34
35    /// Flag folder path.
36    #[getset(get = "pub")]
37    path: String,
38
39    /// Primary, secondary and uniform colours, as RGB.
40    #[getset(get_copy = "pub")]
41    colours: [[u8; 3]; 3],
42}
43
44/// Per-faction values derived from the tables.
45#[derive(Debug, Clone, PartialEq, Eq, Default, Getters, CopyGetters)]
46pub struct FactionSummary {
47
48    /// Faction key.
49    #[getset(get = "pub")]
50    key: String,
51
52    /// Capital region key.
53    #[getset(get = "pub")]
54    capital: Option<String>,
55
56    /// Number of owned regions.
57    #[getset(get_copy = "pub")]
58    region_count: usize,
59
60    /// `start_pos_victory_conditions` regions of the faction, in table order.
61    #[getset(get = "pub")]
62    victory_regions: Vec<String>,
63
64    /// Faction this one is a client state of.
65    #[getset(get = "pub")]
66    client_state_of: Option<String>,
67
68    /// Faction this one is a vassal of.
69    #[getset(get = "pub")]
70    vassal_of: Option<String>,
71
72    /// Client states and vassals of the faction, in startpos faction order.
73    #[getset(get = "pub")]
74    subjects: Vec<String>,
75
76    /// The faction is at war, or in a vassal relation (either side).
77    #[getset(get_copy = "pub")]
78    has_war_or_vassal: bool,
79
80    /// For factions that can be hordes (`factions.can_be_horde`), whether they start as a horde (own no regions).
81    #[getset(get_copy = "pub")]
82    horde: Option<bool>,
83
84    /// The faction has no living characters.
85    #[getset(get_copy = "pub")]
86    without_characters: bool,
87
88    /// `start_pos_characters` id of the faction's progenitor, if any.
89    #[getset(get = "pub")]
90    progenitor: Option<String>,
91}
92
93//---------------------------------------------------------------------------//
94//                              Implementations
95//---------------------------------------------------------------------------//
96
97/// Returns the two flags of a faction: the normal one and the republican one.
98///
99/// # Arguments
100///
101/// * `tables` - Tables with `factions`.
102/// * `faction` - Faction key.
103///
104/// # Returns
105///
106/// The flag from `flags_path` with the primary, secondary and uniform colours, and the flag from
107/// `republican_flag_path` with the alternative colours.
108///
109/// # Errors
110///
111/// Returns an error if a table or column is missing, the faction isn't in `factions` or a colour isn't valid hex.
112pub fn faction_flags(tables: &StartposTables, faction: &str) -> Result<[FactionFlag; 2]> {
113    let row = faction_row(tables, faction)?;
114    Ok([
115        flag(&row, "flags_path", ["primary_colour_hex", "secondary_colour_hex", "uniform_colour_hex"])?,
116        flag(&row, "republican_flag_path", ["alt_primary_colour_hex", "alt_secondary_colour_hex", "alt_uniform_colour_hex"])?,
117    ])
118}
119
120/// Returns the per-faction values of a faction.
121///
122/// # Arguments
123///
124/// * `tables` - Tables with `start_pos_victory_conditions`, `start_pos_diplomacy` and `factions`.
125/// * `index` - The campaign's startpos rows.
126/// * `faction` - The faction.
127///
128/// # Errors
129///
130/// Returns an error if a table or column is missing.
131pub fn faction_summary(tables: &StartposTables, index: &CampaignIndex, faction: &FactionEntry) -> Result<FactionSummary> {
132    let key = faction.key();
133    let mut summary = FactionSummary {
134        key: key.to_owned(),
135        capital: index.capital_of(key).map(|region| region.key().to_owned()),
136        region_count: index.regions_of(key).count(),
137        without_characters: index.characters_of(key).all(|character| !character.death_type().is_empty()),
138        progenitor: index.characters_of(key).find(|character| character.progenitor()).map(|character| character.id().to_owned()),
139        ..Default::default()
140    };
141
142    for row in tables.table("start_pos_victory_conditions")?.rows() {
143        if row.str("start_pos_faction")? == *faction.id() {
144            summary.victory_regions.push(row.str("region")?.into_owned());
145        }
146    }
147
148    for row in tables.table("start_pos_diplomacy")?.rows() {
149        let (Some(first), Some(second)) = (index.faction_by_id(&row.str("faction1")?), index.faction_by_id(&row.str("faction2")?)) else { continue };
150        if first.key() != key && second.key() != key {
151            continue;
152        }
153
154        let stance = row.str("stance")?;
155        summary.has_war_or_vassal |= stance == "war" || stance == "vassal";
156        match stance.as_ref() {
157            "client_state" | "vassal" if second.key() == key => summary.subjects.push(first.key().to_owned()),
158            "client_state" => summary.client_state_of = Some(second.key().to_owned()),
159            "vassal" => summary.vassal_of = Some(second.key().to_owned()),
160            _ => {},
161        }
162    }
163
164    let order = index.factions_in_startpos_order();
165    summary.subjects.sort_by_key(|subject| order.iter().position(|faction| faction.key() == subject));
166
167    if faction_row(tables, key)?.bool("can_be_horde")? {
168        summary.horde = Some(summary.region_count == 0);
169    }
170
171    Ok(summary)
172}
173
174/// Returns the `factions` row of a faction.
175pub(crate) fn faction_row<'a>(tables: &'a StartposTables, faction: &str) -> Result<Row<'a>> {
176    for row in tables.table("factions")?.rows() {
177        if row.str("key")? == faction {
178            return Ok(row);
179        }
180    }
181    Err(RLibError::StartposInvalidValue("factions".to_owned(), "key".to_owned(), faction.to_owned()))
182}
183
184/// Builds a flag from a flag path column and three colour columns of a `factions` row.
185pub(crate) fn flag(row: &Row, path_column: &str, colour_columns: [&str; 3]) -> Result<FactionFlag> {
186    let mut colours = [[0; 3]; 3];
187    for (colour, column) in colours.iter_mut().zip(colour_columns) {
188        *colour = row.colour(column)?;
189    }
190    Ok(FactionFlag { path: row.str(path_column)?.into_owned(), colours })
191}