Skip to main content

rpfm_extensions/startpos/world/writer/
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//! Plan and writer of the government posts of the factions (`FACTION/GOVERNMENT/CHARACTER_POSTS`).
12//!
13//! Post ids are one counter over the factions, in startpos order, and each faction's groups follow the game's key
14//! order of `ministerial_positions_culture_details`.
15
16use std::collections::HashMap;
17
18use rpfm_lib::error::Result;
19use rpfm_lib::files::esf::{NodeType, RecordNode};
20
21use super::{child, WorldInputs, WorldPlan};
22use crate::startpos::world::government::{government_posts, post_holders, PostGroup, FACTION_LEADER_POSITION};
23
24/// Path of a faction's government.
25const GOVERNMENT_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/GOVERNMENT";
26
27/// Path of the post groups of a government.
28const POSTS_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/FACTION_ARRAY/FACTION/GOVERNMENT/CHARACTER_POSTS";
29
30//---------------------------------------------------------------------------//
31//                              Types
32//---------------------------------------------------------------------------//
33
34/// Government posts of every faction, with their runtime ids.
35#[derive(Debug, Clone, Default)]
36pub struct PostPlan {
37
38    /// Post groups of each faction with the id of their first post, by faction key.
39    groups: HashMap<String, Vec<(PostGroup, u32)>>,
40
41    /// Post of each holder (see [`post_holders`]), by `start_pos_characters` id.
42    posts_of: HashMap<String, u32>,
43}
44
45//---------------------------------------------------------------------------//
46//                              Implementations
47//---------------------------------------------------------------------------//
48
49impl PostPlan {
50
51    /// Plans the posts of every faction.
52    ///
53    /// # Errors
54    ///
55    /// Returns an error if a table or column is missing.
56    pub fn new(inputs: &WorldInputs, plan: &WorldPlan) -> Result<Self> {
57        let (mut groups, mut posts_of) = (HashMap::new(), HashMap::new());
58
59        // Games without government posts (Rome 2) have no plan.
60        if inputs.layout.record(&format!("{}/FACTION_ARRAY/FACTION/GOVERNMENT/CHARACTER_POSTS", super::WORLD_PATH)).is_none() {
61            return Ok(Self { groups, posts_of });
62        }
63        let mut next_post = 1;
64        for faction in plan.factions() {
65            let mut faction_groups = vec![];
66            for group in government_posts(inputs.tables, inputs.index, faction)? {
67                let count = group.count();
68                let holders = post_holders(inputs.tables, inputs.index, faction, group.position())?;
69                for (post, holder) in (next_post..next_post + count).zip(holders) {
70                    posts_of.entry(holder).or_insert(post);
71                }
72                faction_groups.push((group, next_post));
73                next_post += count;
74            }
75            groups.insert(faction.key().to_owned(), faction_groups);
76        }
77        Ok(Self { groups, posts_of })
78    }
79
80    /// Returns the post a character holds, by its `start_pos_characters` id, if any.
81    pub fn post_of(&self, character: &str) -> Option<u32> {
82        self.posts_of.get(character).copied()
83    }
84
85    /// Returns the `start_pos_characters` id of a post's holder, if it has one.
86    pub fn holder_of(&self, post: u32) -> Option<&str> {
87        self.posts_of.iter().find(|(_, held)| **held == post).map(|(holder, _)| holder.as_str())
88    }
89
90    /// Returns the post groups of a faction, with the id of their first post.
91    pub fn groups_of(&self, faction: &str) -> &[(PostGroup, u32)] {
92        self.groups.get(faction).map(Vec::as_slice).unwrap_or_default()
93    }
94
95    /// Returns the id of a faction's leader post, if it has one.
96    pub fn leader_post(&self, faction: &str) -> Option<u32> {
97        self.groups_of(faction).iter().find(|(group, _)| group.position() == FACTION_LEADER_POSITION).map(|(_, id)| *id)
98    }
99}
100
101/// Writes the posts of a faction's government (`CHARACTER_POSTS`): one group per post group, with its post count and
102/// that many posts, each with its holder's runtime id (0 for vacant posts).
103///
104/// # Arguments
105///
106/// * `inputs` - Tables, startpos rows and layouts.
107/// * `posts` - Government posts of the factions.
108/// * `faction` - Faction key.
109/// * `runtime_id` - Runtime id of a character, by `start_pos_characters` id.
110///
111/// # Errors
112///
113/// Returns an error if the layout lacks a record, or a group has more posts than the layout knows.
114pub fn write_character_posts(inputs: &WorldInputs, posts: &PostPlan, faction: &str, runtime_id: impl Fn(&str) -> Option<u32>) -> Result<RecordNode> {
115    let mut array = child(inputs.layout, GOVERNMENT_PATH, "CHARACTER_POSTS")?;
116    for (group, first_id) in posts.groups_of(faction) {
117        let entry = array.push_group()?;
118        entry.set_u32("^+1", group.count())?;
119        for index in 0..group.count() {
120            let holder = posts.holder_of(first_id + index).and_then(&runtime_id).unwrap_or_default();
121            let mut post = child(inputs.layout, POSTS_PATH, "CHARACTER_POST")?;
122            post.group()?
123                .set_u32("^+1", first_id + index)?
124                .set_str("^+2", group.position())?
125                .set_u32("^+3", holder)?
126                .set("^+5", NodeType::I64(group.unique_id()))?;
127            let key = if index == 0 { "CHARACTER_POST".to_owned() } else { format!("CHARACTER_POST@{}", index + 1) };
128            entry.set_record(&key, Some(post.build()?))?;
129        }
130    }
131    array.build()
132}