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
100/// Order and runtime ids of the objects of the world.
101#[derive(Debug, Clone, Default)]
102pub struct WorldPlan<'a> {
103
104    /// Factions, in startpos order.
105    factions: Vec<&'a FactionEntry>,
106
107    /// Runtime ids of the factions, by key.
108    faction_ids: HashMap<String, u32>,
109
110    /// Regions, in startpos order (table order: the game uses a hash order).
111    regions: Vec<&'a RegionEntry>,
112
113    /// Runtime ids of the regions, by key.
114    region_ids: HashMap<String, u32>,
115
116    /// Order of the living characters of some factions, by faction key. Factions without one put their leader
117    /// first, then the others in table order.
118    character_orders: HashMap<String, Vec<&'a CharacterEntry>>,
119}
120
121//---------------------------------------------------------------------------//
122//                              Implementations
123//---------------------------------------------------------------------------//
124
125/// Writes the `WORLD` record of a campaign.
126///
127/// # Arguments
128///
129/// * `inputs` - Tables, startpos rows and layouts.
130///
131/// # Errors
132///
133/// Returns an error if a table, column or layout is missing.
134pub fn write_world(inputs: &WorldInputs) -> Result<RecordNode> {
135    write_world_with_plan(inputs, &WorldPlan::new(inputs.index))
136}
137
138/// Writes the `WORLD` record of a campaign with a given plan.
139///
140/// # Arguments
141///
142/// * `inputs` - Tables, startpos rows and layouts.
143/// * `plan` - Order and runtime ids of the world's objects.
144///
145/// # Errors
146///
147/// Returns an error if a table, column or layout is missing.
148pub fn write_world_with_plan<'a>(inputs: &WorldInputs<'a>, plan: &WorldPlan<'a>) -> Result<RecordNode> {
149    let forces = ForcePlan::new(inputs.tables, inputs.index, plan)?;
150    let residences = regions::settlement_residences(inputs, plan)?;
151
152    let posts = PostPlan::new(inputs, plan)?;
153    let sight = SightPlan::new(inputs, plan);
154    let characters = CharacterWriter::new(inputs, plan, &forces, &posts, &sight)?;
155    let provinces = provinces::write_province_and_sea_managers(inputs, plan, &sight)?;
156    let armies = ArmyWriter::new(inputs, &forces, &characters, &residences, provinces.next_recruitment)?;
157    let levy_pools = factions::plan_levy_pools(inputs, plan, &provinces.province_ids, provinces.next_pool)?;
158    let diplomacy = Diplomacy::new(inputs.tables, inputs.index, inputs.region_map)?;
159    let context = FactionContext { armies: &armies, characters: &characters, posts: &posts, levy_pools: &levy_pools, diplomacy: &diplomacy, sight: &sight };
160
161    let mut world = inputs.layout.builder(WORLD_PATH)?;
162    world.group()?
163        .set_record("FACTION_ARRAY", Some(factions::write_faction_array(inputs, plan, &context)?))?
164        .set_record("REBEL_FACTION", Some(factions::write_rebel_faction(inputs.layout, plan)?))?
165        .set_record("REGION_MANAGER", Some(regions::write_region_manager(inputs, plan, &forces, &sight)?))?
166        .set_record("PROVINCE_MANAGER", Some(provinces.provinces))?
167        .set_record("SEA_REGION_MANAGER", Some(provinces.seas))?
168        .set_record("FAMILY_TREE", Some(characters.write_family_tree()?))?
169        .set_record("SLAVES", Some(slaves::write_slaves(inputs, plan)?))?
170        .set_record("SPYING_ARRAY", Some(spying::write_spying_array(inputs, plan)?))?;
171    world.build()
172}
173
174impl<'a> WorldPlan<'a> {
175
176    /// Plans the world of a campaign: factions in startpos order, regions in table order.
177    pub fn new(index: &'a CampaignIndex) -> Self {
178        Self::with_orders(index.factions_in_startpos_order(), index.regions().iter().collect())
179    }
180
181    /// Plans the world of a campaign with the given object orders, for example the ones of a game-built startpos.
182    ///
183    /// # Arguments
184    ///
185    /// * `factions` - Factions, in the order of their runtime ids.
186    /// * `regions` - Regions, in the order of their runtime ids.
187    pub fn with_orders(factions: Vec<&'a FactionEntry>, regions: Vec<&'a RegionEntry>) -> Self {
188        let faction_ids = factions.iter().zip(REBEL_FACTION_ID + 1..).map(|(faction, id)| (faction.key().to_owned(), id)).collect();
189        let region_ids = regions.iter().zip(1..).map(|(region, id)| (region.key().to_owned(), id)).collect();
190        Self { factions, faction_ids, regions, region_ids, character_orders: HashMap::new() }
191    }
192
193    /// Sets the order of a faction's living characters, for example the one of a game-built startpos. Living
194    /// characters left out of it go after it, in table order.
195    ///
196    /// # Arguments
197    ///
198    /// * `faction` - Faction key.
199    /// * `characters` - The faction's characters, in the order of their runtime ids.
200    pub fn set_character_order(&mut self, faction: &str, characters: Vec<&'a CharacterEntry>) {
201        self.character_orders.insert(faction.to_owned(), characters);
202    }
203
204    /// Returns the order set for a faction's living characters, if any.
205    pub fn character_order(&self, faction: &str) -> Option<&[&'a CharacterEntry]> {
206        self.character_orders.get(faction).map(Vec::as_slice)
207    }
208
209    /// Returns the factions, in startpos order.
210    pub fn factions(&self) -> &[&'a FactionEntry] {
211        &self.factions
212    }
213
214    /// Returns the regions, in startpos order.
215    pub fn regions(&self) -> &[&'a RegionEntry] {
216        &self.regions
217    }
218
219    /// Returns the runtime id of a region.
220    pub fn region_id(&self, region: &str) -> Option<u32> {
221        self.region_ids.get(region).copied()
222    }
223
224    /// Returns the runtime id of a faction.
225    pub fn faction_id(&self, faction: &str) -> Option<u32> {
226        self.faction_ids.get(faction).copied()
227    }
228}
229
230/// Starts building a child record from its parent's path and its key.
231pub(crate) fn child<'a>(layout: &'a StartposLayout, parent: &str, key: &str) -> Result<RecordBuilder<'a>> {
232    layout.builder(&child_path(parent, key))
233}
234
235/// Returns the `start_pos_factions` row of a faction of the campaign.
236pub(crate) fn start_pos_faction_row<'a>(tables: &'a StartposTables, faction: &FactionEntry) -> Result<Row<'a>> {
237    for row in tables.table("start_pos_factions")?.rows() {
238        if row.str("ID")? == *faction.id() {
239            return Ok(row);
240        }
241    }
242    Err(RLibError::StartposInvalidValue("start_pos_factions".to_owned(), "ID".to_owned(), faction.id().to_owned()))
243}
244
245/// Returns the `start_pos_horde_details` rows of the campaign's characters.
246pub(crate) fn horde_details<'a>(inputs: &WorldInputs<'a>) -> Result<Vec<Row<'a>>> {
247    let characters = inputs.index.characters().iter().map(|character| character.id().as_str()).collect::<HashSet<_>>();
248    let mut rows = vec![];
249    for row in inputs.tables.table("start_pos_horde_details")?.rows() {
250        if characters.contains(row.str("general")?.as_ref()) {
251            rows.push(row);
252        }
253    }
254    Ok(rows)
255}
256
257/// Returns the current value of an id counter, and advances it.
258pub(crate) fn next_id(counter: &mut u32) -> u32 {
259    let id = *counter;
260    *counter += 1;
261    id
262}