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/// Ministerial position of the heir, which the game gives the leader's eldest son if the tables give it nobody.
33pub const HEIR_POSITION: &str = "heir";
34
35//---------------------------------------------------------------------------//
36// Types
37//---------------------------------------------------------------------------//
38
39/// A group of identical posts of a faction's government.
40#[derive(Debug, Clone, PartialEq, Eq, Default, Getters, CopyGetters)]
41pub struct PostGroup {
42
43 /// Ministerial position key.
44 #[getset(get = "pub")]
45 position: String,
46
47 /// `unique_id` of the `ministerial_positions_culture_details` row that gives the posts.
48 #[getset(get_copy = "pub")]
49 unique_id: i64,
50
51 /// Number of posts (`ministerial_positions.maximum_concurrent_ministers`).
52 #[getset(get_copy = "pub")]
53 count: u32,
54}
55
56/// A political party of a faction.
57#[derive(Debug, Clone, PartialEq, Eq, Default, Getters, CopyGetters)]
58pub struct PoliticalParty {
59
60 /// Party key.
61 #[getset(get = "pub")]
62 key: String,
63
64 /// Starting power.
65 #[getset(get_copy = "pub")]
66 power: u32,
67
68 /// Players can lead the party: it's the faction's ruling one.
69 #[getset(get_copy = "pub")]
70 playable: bool,
71}
72
73//---------------------------------------------------------------------------//
74// Implementations
75//---------------------------------------------------------------------------//
76
77/// Returns the post groups of a faction's government.
78///
79/// The groups are the `ministerial_positions_culture_details` rows for every faction (without culture, subculture
80/// and faction), the faction's culture (without subculture and faction), its subculture (without faction) and the
81/// faction itself: the campaign's rows if it has any for the faction, else the rows without campaign.
82///
83/// # Arguments
84///
85/// * `tables` - Tables with `ministerial_positions_culture_details` and `ministerial_positions`.
86/// * `index` - The campaign's startpos rows.
87/// * `faction` - The faction.
88///
89/// # Returns
90///
91/// The post groups, in the order the game lists them: the table's key order (see
92/// [`crate::startpos::tables::Table::rows_in_key_order`]).
93///
94/// # Errors
95///
96/// Returns an error if a table or column is missing.
97pub fn government_posts(tables: &StartposTables, index: &CampaignIndex, faction: &FactionEntry) -> Result<Vec<PostGroup>> {
98 let mut concurrent = HashMap::new();
99 for row in tables.table("ministerial_positions")?.rows() {
100 concurrent.insert(row.str("minister_key")?.into_owned(), row.int("maximum_concurrent_ministers")?);
101 }
102
103 // Rows of the campaign replace the ones without campaign, if the campaign has any for the faction.
104 let mut generic = vec![];
105 let mut own = vec![];
106 for row in tables.table("ministerial_positions_culture_details")?.rows_in_key_order("unique_id")? {
107 let campaign = row.str("campaign")?;
108 if !campaign.is_empty() && campaign != *index.campaign() {
109 continue;
110 }
111
112 let row_faction = row.str("faction_key")?;
113 let row_subculture = row.str("subculture_key")?;
114 // Rows without culture, subculture nor faction (Thrones' generic posts) are for everyone.
115 let row_culture = row.str("culture_key")?;
116 let matches = row_faction == *faction.key()
117 || (row_faction.is_empty() && row_subculture == *faction.subculture())
118 || (row_faction.is_empty() && row_subculture.is_empty() && (row_culture.is_empty() || row_culture == *faction.culture()));
119 if !matches {
120 continue;
121 }
122
123 let position = row.str("ministerial_position_key")?.into_owned();
124 let group = PostGroup {
125 count: concurrent.get(&position).and_then(|&count| u32::try_from(count).ok()).unwrap_or_default(),
126 unique_id: row.int("unique_id")?,
127 position,
128 };
129 if campaign.is_empty() { generic.push(group) } else { own.push(group) }
130 }
131
132 let groups = if own.is_empty() { generic } else { own };
133 Ok(groups)
134}
135
136/// Returns the political parties of a faction with their starting power.
137///
138/// The faction's `senator_total` is split between its parties (`faction_political_parties_junctions`) in
139/// proportion to their `initial_power`, or evenly if they all start with no power.
140///
141/// # Arguments
142///
143/// * `tables` - Tables with `faction_political_parties_junctions`, `political_parties` and `factions`.
144/// * `faction` - Faction key.
145///
146/// # Returns
147///
148/// The parties, in table order. Empty for factions without parties.
149///
150/// # Errors
151///
152/// Returns an error if a table or column is missing.
153pub fn political_parties(tables: &StartposTables, faction: &str) -> Result<Vec<PoliticalParty>> {
154 let mut senator_total = 0;
155 for row in tables.table("factions")?.rows() {
156 if row.str("key")? == faction {
157 senator_total = row.int("senator_total")?;
158 }
159 }
160
161 let mut party_rows = HashMap::new();
162 for row in tables.table("political_parties")?.rows() {
163 party_rows.insert(row.str("key")?.into_owned(), (row.int("initial_power")?, row.bool("playable")?));
164 }
165
166 let mut parties = vec![];
167 for row in tables.table("faction_political_parties_junctions")?.rows() {
168 if row.str("faction_key")? == faction {
169 let key = row.str("political_party_key")?.into_owned();
170 let (power, playable) = party_rows.get(&key).copied().unwrap_or_default();
171 parties.push((power, playable, key));
172 }
173 }
174
175 let total_power = parties.iter().map(|(power, ..)| power).sum::<i64>();
176 let party_count = i64::try_from(parties.len()).unwrap_or(i64::MAX);
177 Ok(parties.into_iter().map(|(power, playable, key)| {
178 let share = if total_power == 0 { senator_total / party_count.max(1) } else { senator_total * power / total_power };
179 PoliticalParty { key, power: u32::try_from(share.max(0)).unwrap_or(u32::MAX), playable }
180 }).collect())
181}
182
183/// Returns the leader of a faction: its first living character with the `faction_leader` position, or its first
184/// living character if no row has it. The startpos lists the leader first in the faction's characters.
185///
186/// The leader holds the faction's `faction_leader` post (`CHARACTER` #8 is the post id, with #28 = 4 and #29 = true).
187///
188/// # Arguments
189///
190/// * `index` - The campaign's startpos rows.
191/// * `faction` - Faction key.
192///
193/// # Returns
194///
195/// The leader, or `None` for factions without living characters.
196pub fn faction_leader<'a>(index: &'a CampaignIndex, faction: &'a str) -> Option<&'a CharacterEntry> {
197 let mut living = index.characters_of(faction).filter(|character| character.death_type().is_empty()).peekable();
198 let first = living.peek().copied();
199 living.find(|character| character.ministerial_position() == FACTION_LEADER_POSITION).or(first)
200}
201
202/// Returns the holders of a faction's posts of a position, by `start_pos_characters` id, in table order.
203///
204/// The leader holds the leader post (see [`faction_leader`]). Other posts go to the faction's living characters whose
205/// `ministerial_position` is theirs; the heir post, if the tables give it nobody, to the leader's eldest living son.
206/// Verified on Thrones' campaign: the 30 captains, chamberlains and justicars and the 53 leaders the tables give, and
207/// 23 of 34 heirs the game picked (it breaks ties between sons with its RNG).
208///
209/// # Errors
210///
211/// Returns an error if a table or column is missing.
212pub fn post_holders(tables: &StartposTables, index: &CampaignIndex, faction: &FactionEntry, position: &str) -> Result<Vec<String>> {
213 let leader = faction_leader(index, faction.key());
214 if position == FACTION_LEADER_POSITION {
215 return Ok(leader.map(|leader| leader.id().to_owned()).into_iter().collect());
216 }
217
218 let living = || index.characters_of(faction.key()).filter(|character| character.death_type().is_empty());
219 let holders = living().filter(|character| character.ministerial_position() == position).map(|character| character.id().to_owned()).collect::<Vec<_>>();
220 if !holders.is_empty() || position != HEIR_POSITION {
221 return Ok(holders);
222 }
223
224 let Some(leader) = leader else { return Ok(vec![]) };
225 let mut sons = vec![];
226 for row in tables.table("start_pos_family_relationships")?.rows() {
227 if row.str("relationship")? == "child" && row.str("related_to")? == *leader.id() {
228 let child = row.str("character")?;
229 if let Some(son) = living().find(|character| *character.id() == child && character.is_male()) {
230 sons.push(son);
231 }
232 }
233 }
234 sons.sort_by_key(|son| std::cmp::Reverse(son.age()));
235 Ok(sons.first().map(|son| son.id().to_owned()).into_iter().collect())
236}