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}