Skip to main content

rpfm_extensions/startpos/world/
government.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//! Government of each faction: its posts (`FACTION/GOVERNMENT/CHARACTER_POSTS`), its political parties
12//! (`FACTION/CAMPAIGN_POLITICS`) and its leader.
13//!
14//! Verified on Attila's main campaign: posts match for 118 of 118 factions, party sets for 118 of 118 and party
15//! powers for 100 of 100 parties, and the leader for the 60 factions with characters. The game stores the post
16//! groups and some party lists in hash order.
17
18use getset::{CopyGetters, Getters};
19
20use std::collections::HashMap;
21
22use rpfm_lib::error::Result;
23
24use super::{CampaignIndex, CharacterEntry, FactionEntry};
25use crate::startpos::tables::StartposTables;
26
27#[cfg(test)] #[path = "government_test.rs"] mod government_test;
28
29/// Ministerial position held by faction leaders.
30pub const FACTION_LEADER_POSITION: &str = "faction_leader";
31
32//---------------------------------------------------------------------------//
33//                              Types
34//---------------------------------------------------------------------------//
35
36/// A group of identical posts of a faction's government.
37#[derive(Debug, Clone, PartialEq, Eq, Default, Getters, CopyGetters)]
38pub struct PostGroup {
39
40    /// Ministerial position key.
41    #[getset(get = "pub")]
42    position: String,
43
44    /// `unique_id` of the `ministerial_positions_culture_details` row that gives the posts.
45    #[getset(get_copy = "pub")]
46    unique_id: i64,
47
48    /// Number of posts (`ministerial_positions.maximum_concurrent_ministers`).
49    #[getset(get_copy = "pub")]
50    count: u32,
51}
52
53/// A political party of a faction.
54#[derive(Debug, Clone, PartialEq, Eq, Default, Getters, CopyGetters)]
55pub struct PoliticalParty {
56
57    /// Party key.
58    #[getset(get = "pub")]
59    key: String,
60
61    /// Starting power.
62    #[getset(get_copy = "pub")]
63    power: u32,
64
65    /// Players can lead the party: it's the faction's ruling one.
66    #[getset(get_copy = "pub")]
67    playable: bool,
68}
69
70//---------------------------------------------------------------------------//
71//                              Implementations
72//---------------------------------------------------------------------------//
73
74/// Returns the post groups of a faction's government.
75///
76/// The groups are the `ministerial_positions_culture_details` rows for the faction's culture (without subculture
77/// and faction), its subculture (without faction) and the faction itself: the campaign's rows if it has any for the
78/// faction, else the rows without campaign.
79///
80/// # Arguments
81///
82/// * `tables` - Tables with `ministerial_positions_culture_details` and `ministerial_positions`.
83/// * `index` - The campaign's startpos rows.
84/// * `faction` - The faction.
85///
86/// # Returns
87///
88/// The post groups, in table order.
89///
90/// # Errors
91///
92/// Returns an error if a table or column is missing.
93pub fn government_posts(tables: &StartposTables, index: &CampaignIndex, faction: &FactionEntry) -> Result<Vec<PostGroup>> {
94    let mut concurrent = HashMap::new();
95    for row in tables.table("ministerial_positions")?.rows() {
96        concurrent.insert(row.str("minister_key")?.into_owned(), row.int("maximum_concurrent_ministers")?);
97    }
98
99    // Rows of the campaign replace the ones without campaign, if the campaign has any for the faction.
100    let mut generic = vec![];
101    let mut own = vec![];
102    for row in tables.table("ministerial_positions_culture_details")?.rows() {
103        let campaign = row.str("campaign")?;
104        if !campaign.is_empty() && campaign != *index.campaign() {
105            continue;
106        }
107
108        let row_faction = row.str("faction_key")?;
109        let row_subculture = row.str("subculture_key")?;
110        let matches = row_faction == *faction.key()
111            || (row_faction.is_empty() && row_subculture == *faction.subculture())
112            || (row_faction.is_empty() && row_subculture.is_empty() && row.str("culture_key")? == *faction.culture());
113        if !matches {
114            continue;
115        }
116
117        let position = row.str("ministerial_position_key")?.into_owned();
118        let group = PostGroup {
119            count: concurrent.get(&position).and_then(|&count| u32::try_from(count).ok()).unwrap_or_default(),
120            unique_id: row.int("unique_id")?,
121            position,
122        };
123        if campaign.is_empty() { generic.push(group) } else { own.push(group) }
124    }
125
126    let groups = if own.is_empty() { generic } else { own };
127    Ok(groups)
128}
129
130/// Returns the political parties of a faction with their starting power.
131///
132/// The faction's `senator_total` is split between its parties (`faction_political_parties_junctions`) in
133/// proportion to their `initial_power`, or evenly if they all start with no power.
134///
135/// # Arguments
136///
137/// * `tables` - Tables with `faction_political_parties_junctions`, `political_parties` and `factions`.
138/// * `faction` - Faction key.
139///
140/// # Returns
141///
142/// The parties, in table order. Empty for factions without parties.
143///
144/// # Errors
145///
146/// Returns an error if a table or column is missing.
147pub fn political_parties(tables: &StartposTables, faction: &str) -> Result<Vec<PoliticalParty>> {
148    let mut senator_total = 0;
149    for row in tables.table("factions")?.rows() {
150        if row.str("key")? == faction {
151            senator_total = row.int("senator_total")?;
152        }
153    }
154
155    let mut party_rows = HashMap::new();
156    for row in tables.table("political_parties")?.rows() {
157        party_rows.insert(row.str("key")?.into_owned(), (row.int("initial_power")?, row.bool("playable")?));
158    }
159
160    let mut parties = vec![];
161    for row in tables.table("faction_political_parties_junctions")?.rows() {
162        if row.str("faction_key")? == faction {
163            let key = row.str("political_party_key")?.into_owned();
164            let (power, playable) = party_rows.get(&key).copied().unwrap_or_default();
165            parties.push((power, playable, key));
166        }
167    }
168
169    let total_power = parties.iter().map(|(power, ..)| power).sum::<i64>();
170    let party_count = i64::try_from(parties.len()).unwrap_or(i64::MAX);
171    Ok(parties.into_iter().map(|(power, playable, key)| {
172        let share = if total_power == 0 { senator_total / party_count.max(1) } else { senator_total * power / total_power };
173        PoliticalParty { key, power: u32::try_from(share.max(0)).unwrap_or(u32::MAX), playable }
174    }).collect())
175}
176
177/// Returns the leader of a faction: its first living character with the `faction_leader` position, or its first
178/// living character if no row has it. The startpos lists the leader first in the faction's characters.
179///
180/// The leader holds the faction's `faction_leader` post (`CHARACTER` #8 is the post id, with #28 = 4 and #29 = true).
181///
182/// # Arguments
183///
184/// * `index` - The campaign's startpos rows.
185/// * `faction` - Faction key.
186///
187/// # Returns
188///
189/// The leader, or `None` for factions without living characters.
190pub fn faction_leader<'a>(index: &'a CampaignIndex, faction: &'a str) -> Option<&'a CharacterEntry> {
191    let mut living = index.characters_of(faction).filter(|character| character.death_type().is_empty()).peekable();
192    let first = living.peek().copied();
193    living.find(|character| character.ministerial_position() == FACTION_LEADER_POSITION).or(first)
194}