Skip to main content

rpfm_extensions/startpos/world/writer/
mod.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//! Writer of the `WORLD` record of a startpos.
12//!
13//! The writer first plans the world (the order and runtime id of every object), then writes each record from its
14//! layout, filling the values the world generators compute. Runtime ids use one counter per kind of object, as the
15//! game looks them up in separate registries.
16//!
17//! # Submodules
18//!
19//! - [`armies`]: the forces of each faction.
20//! - [`characters`]: the characters of each faction.
21//! - [`factions`]: the `FACTION` records.
22//! - [`forces`]: the plan of the characters and forces.
23//! - [`government`]: the plan of the government posts.
24//! - [`provinces`]: the provinces and the sea regions.
25//! - [`regions`]: the `REGION` records and their settlements.
26//! - [`sight`]: the vision of regions, settlements and forces, and the factions' shroud.
27
28use std::collections::{HashMap, HashSet};
29
30use rpfm_lib::error::{RLibError, Result};
31use rpfm_lib::files::esf::RecordNode;
32
33use self::armies::ArmyWriter;
34use self::characters::CharacterWriter;
35use self::factions::FactionContext;
36use self::forces::ForcePlan;
37use self::government::PostPlan;
38use self::sight::SightPlan;
39use super::{CampaignIndex, CharacterEntry, FactionEntry, RegionEntry};
40use super::diplomacy::Diplomacy;
41use crate::startpos::layout::{child_path, RecordBuilder, StartposLayout};
42use crate::startpos::region_map::RegionMap;
43use crate::startpos::settlements::SettlementLayout;
44use crate::startpos::tables::{Row, StartposTables};
45
46pub mod armies;
47pub mod characters;
48pub mod factions;
49pub mod forces;
50pub mod government;
51pub mod provinces;
52pub mod regions;
53pub mod sight;
54pub mod slaves;
55pub mod spying;
56
57#[cfg(test)] pub(crate) mod writer_test;
58
59/// Path of the `WORLD` record.
60pub const WORLD_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD";
61
62/// Columns of `start_pos_horde_details` with building levels.
63pub(crate) const HORDE_BUILDING_COLUMNS: [&str; 11] = [
64    "primary_building", "secondary_building_1", "secondary_building_2", "secondary_building_3", "secondary_building_4",
65    "secondary_building_5", "secondary_building_6", "secondary_building_7", "secondary_building_8",
66    "secondary_building_9", "secondary_building_10",
67];
68
69/// Runtime id of the rebel faction. Real factions are numbered from the next one, in startpos order.
70pub const REBEL_FACTION_ID: u32 = 1;
71
72//---------------------------------------------------------------------------//
73//                              Types
74//---------------------------------------------------------------------------//
75
76/// What the world writer needs.
77#[derive(Debug, Clone, Copy)]
78pub struct WorldInputs<'a> {
79
80    /// Tables of the game and the campaign.
81    pub tables: &'a StartposTables,
82
83    /// The campaign's startpos rows.
84    pub index: &'a CampaignIndex,
85
86    /// Record layouts of the game.
87    pub layout: &'a StartposLayout,
88
89    /// Settlement layouts of the campaign map (walls only for major settlements).
90    pub settlements: &'a [SettlementLayout],
91
92    /// Region of each cell of the campaign map.
93    pub region_map: &'a RegionMap,
94
95    /// `start_pos_characters` ids of the characters with a `legacy_override` text (a localised column, so it comes
96    /// from the campaign's localisation).
97    pub legacy_overrides: &'a HashSet<String>,
98
99    /// (owner, partner) of the trade routes of the starting agreements, or `None` to trade on every agreement. The
100    /// game drops the agreements whose capitals have no route between them.
101    pub trading: Option<&'a HashSet<(String, String)>>,
102
103    /// Key of the game, for the values only some games store (see the writers that read it).
104    pub game: &'a str,
105}
106
107/// Order and runtime ids of the objects of the world.
108#[derive(Debug, Clone, Default)]
109pub struct WorldPlan<'a> {
110
111    /// Factions, in startpos order.
112    factions: Vec<&'a FactionEntry>,
113
114    /// Runtime ids of the factions, by key.
115    faction_ids: HashMap<String, u32>,
116
117    /// Regions, in startpos order (table order: the game uses a hash order).
118    regions: Vec<&'a RegionEntry>,
119
120    /// Runtime ids of the regions, by key.
121    region_ids: HashMap<String, u32>,
122
123    /// Order of the living characters of some factions, by faction key. Factions without one put their leader
124    /// first, then the others in table order.
125    character_orders: HashMap<String, Vec<&'a CharacterEntry>>,
126}
127
128//---------------------------------------------------------------------------//
129//                              Implementations
130//---------------------------------------------------------------------------//
131
132/// Writes the `WORLD` record of a campaign.
133///
134/// # Arguments
135///
136/// * `inputs` - Tables, startpos rows and layouts.
137///
138/// # Errors
139///
140/// Returns an error if a table, column or layout is missing.
141pub fn write_world(inputs: &WorldInputs) -> Result<RecordNode> {
142    write_world_with_plan(inputs, &WorldPlan::new(inputs.index))
143}
144
145/// Writes the `WORLD` record of a campaign with a given plan.
146///
147/// # Arguments
148///
149/// * `inputs` - Tables, startpos rows and layouts.
150/// * `plan` - Order and runtime ids of the world's objects.
151///
152/// # Errors
153///
154/// Returns an error if a table, column or layout is missing.
155pub fn write_world_with_plan<'a>(inputs: &WorldInputs<'a>, plan: &WorldPlan<'a>) -> Result<RecordNode> {
156    let forces = ForcePlan::new(inputs.tables, inputs.index, plan)?;
157    let residences = regions::settlement_residences(inputs, plan)?;
158
159    let posts = PostPlan::new(inputs, plan)?;
160    let sight = SightPlan::new(inputs, plan);
161    let characters = CharacterWriter::new(inputs, plan, &forces, &posts, &sight)?;
162    let provinces = provinces::write_province_and_sea_managers(inputs, plan, &sight)?;
163    let armies = ArmyWriter::new(inputs, &forces, &characters, &residences, provinces.next_recruitment)?;
164    let levy_pools = factions::plan_levy_pools(inputs, plan, &provinces.province_ids, provinces.next_pool)?;
165    let diplomacy = Diplomacy::new(inputs.tables, inputs.index, inputs.region_map, inputs.trading)?;
166    let context = FactionContext { armies: &armies, characters: &characters, posts: &posts, levy_pools: &levy_pools, diplomacy: &diplomacy, sight: &sight };
167
168    let mut world = inputs.layout.builder(WORLD_PATH)?;
169    world.group()?
170        .set_record("FACTION_ARRAY", Some(factions::write_faction_array(inputs, plan, &context)?))?
171        .set_record("REBEL_FACTION", Some(factions::write_rebel_faction(inputs.layout, plan)?))?
172        .set_record("REGION_MANAGER", Some(regions::write_region_manager(inputs, plan, &forces, &sight)?))?
173        .set_record("PROVINCE_MANAGER", Some(provinces.provinces))?
174        .set_record("SEA_REGION_MANAGER", Some(provinces.seas))?
175        .set_record("FAMILY_TREE", Some(characters.write_family_tree()?))?
176        .set_record("SLAVES", Some(slaves::write_slaves(inputs, plan)?))?
177        .set_record("SPYING_ARRAY", Some(spying::write_spying_array(inputs, plan)?))?;
178    world.build()
179}
180
181impl<'a> WorldPlan<'a> {
182
183    /// Plans the world of a campaign: factions in startpos order, regions in table order.
184    pub fn new(index: &'a CampaignIndex) -> Self {
185        Self::with_orders(index.factions_in_startpos_order(), index.regions().iter().collect())
186    }
187
188    /// Plans the world of a campaign with the given object orders, for example the ones of a game-built startpos.
189    ///
190    /// # Arguments
191    ///
192    /// * `factions` - Factions, in the order of their runtime ids.
193    /// * `regions` - Regions, in the order of their runtime ids.
194    pub fn with_orders(factions: Vec<&'a FactionEntry>, regions: Vec<&'a RegionEntry>) -> Self {
195        let faction_ids = factions.iter().zip(REBEL_FACTION_ID + 1..).map(|(faction, id)| (faction.key().to_owned(), id)).collect();
196        let region_ids = regions.iter().zip(1..).map(|(region, id)| (region.key().to_owned(), id)).collect();
197        Self { factions, faction_ids, regions, region_ids, character_orders: HashMap::new() }
198    }
199
200    /// Sets the order of a faction's living characters, for example the one of a game-built startpos. Living
201    /// characters left out of it go after it, in table order.
202    ///
203    /// # Arguments
204    ///
205    /// * `faction` - Faction key.
206    /// * `characters` - The faction's characters, in the order of their runtime ids.
207    pub fn set_character_order(&mut self, faction: &str, characters: Vec<&'a CharacterEntry>) {
208        self.character_orders.insert(faction.to_owned(), characters);
209    }
210
211    /// Returns the order set for a faction's living characters, if any.
212    pub fn character_order(&self, faction: &str) -> Option<&[&'a CharacterEntry]> {
213        self.character_orders.get(faction).map(Vec::as_slice)
214    }
215
216    /// Returns the factions, in startpos order.
217    pub fn factions(&self) -> &[&'a FactionEntry] {
218        &self.factions
219    }
220
221    /// Returns the regions, in startpos order.
222    pub fn regions(&self) -> &[&'a RegionEntry] {
223        &self.regions
224    }
225
226    /// Returns the runtime id of a region.
227    pub fn region_id(&self, region: &str) -> Option<u32> {
228        self.region_ids.get(region).copied()
229    }
230
231    /// Returns the runtime id of a faction.
232    pub fn faction_id(&self, faction: &str) -> Option<u32> {
233        self.faction_ids.get(faction).copied()
234    }
235}
236
237/// Starts building a child record from its parent's path and its key.
238pub(crate) fn child<'a>(layout: &'a StartposLayout, parent: &str, key: &str) -> Result<RecordBuilder<'a>> {
239    layout.builder(&child_path(parent, key))
240}
241
242/// Returns the `start_pos_factions` row of a faction of the campaign.
243pub(crate) fn start_pos_faction_row<'a>(tables: &'a StartposTables, faction: &FactionEntry) -> Result<Row<'a>> {
244    for row in tables.table("start_pos_factions")?.rows() {
245        if row.str("ID")? == *faction.id() {
246            return Ok(row);
247        }
248    }
249    Err(RLibError::StartposInvalidValue("start_pos_factions".to_owned(), "ID".to_owned(), faction.id().to_owned()))
250}
251
252/// Returns the `start_pos_horde_details` rows of the campaign's characters.
253pub(crate) fn horde_details<'a>(inputs: &WorldInputs<'a>) -> Result<Vec<Row<'a>>> {
254    let characters = inputs.index.characters().iter().map(|character| character.id().as_str()).collect::<HashSet<_>>();
255    let mut rows = vec![];
256    for row in inputs.tables.table("start_pos_horde_details")?.rows() {
257        if characters.contains(row.str("general")?.as_ref()) {
258            rows.push(row);
259        }
260    }
261    Ok(rows)
262}
263
264/// Returns the current value of an id counter, and advances it.
265pub(crate) fn next_id(counter: &mut u32) -> u32 {
266    let id = *counter;
267    *counter += 1;
268    id
269}