Skip to main content

rpfm_extensions/startpos/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 whole startposes (`CAMPAIGN_STARTPOS`): the world, and the records around it.
12//!
13//! Records without a generator yet keep the values of their layouts.
14//!
15//! The AI's history (`CAMPAIGN_MODEL/CAI_HISTORY`) is left out. CA's startposes (Attila, Thrones, Rome 2) have one,
16//! a log of the AI's first decisions, but no retail game writes or reads it: their `empire.retail.dll` don't even
17//! have its record names, and startposes built with the retail game, like The Dawnless Days', don't have it.
18
19use std::collections::HashMap;
20
21use rpfm_lib::error::Result;
22use rpfm_lib::files::esf::{BoolNode, Coordinates2DNode, ESF, F32Node, I32Node, NodeType, RecordNode, U32Node, VecU32Node};
23
24use super::cai::world::{CaiContext, CaiInputs};
25use super::cai::world_view::international_routes;
26use super::layout::{COMPACT_I32, MAX_COMPACT_U32};
27use super::missions::VictoryObjectives;
28use super::trade::TradeNetwork;
29use super::world::calendar::Calendar;
30use super::world::writer::forces::ForcePlan;
31use super::world::writer::{write_world_with_plan, WorldInputs, WorldPlan};
32use crate::lua::harness::LuaSavedValue;
33use self::dynamic_resources::DynamicResourcesInputs;
34
35mod counters;
36mod diplomacy;
37mod dynamic_resources;
38mod header;
39mod international_routes;
40mod map_data;
41mod preopen;
42mod script_variables;
43mod trade;
44pub(crate) mod victory;
45
46#[cfg(test)] pub(crate) mod writer_test;
47
48/// Path of the root record.
49pub const ROOT_PATH: &str = "CAMPAIGN_STARTPOS";
50
51/// Path of the campaign environment.
52pub const ENV_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV";
53
54/// Path of the campaign model.
55pub const MODEL_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL";
56
57/// The AI's history, which only CA's internal builds write (see the module docs).
58pub(crate) const AI_HISTORY: &str = "CAI_HISTORY";
59
60//---------------------------------------------------------------------------//
61//                              Types
62//---------------------------------------------------------------------------//
63
64/// What a startpos is built from.
65#[derive(Clone, Copy)]
66pub struct StartposInputs<'a> {
67
68    /// Inputs of the world: tables, startpos rows, layouts and map data.
69    pub world: &'a WorldInputs<'a>,
70
71    /// Trade network of the campaign map.
72    pub trade_network: &'a TradeNetwork,
73
74    /// Victory objectives of the campaign (its `victory_objectives.txt`).
75    pub victory_objectives: &'a [VictoryObjectives],
76
77    /// Decoded `map_data.esf` of the campaign map.
78    pub map_data: &'a ESF,
79
80    /// Decoded `hlp_data.esf` of the campaign map.
81    pub hlp_data: &'a ESF,
82
83    /// Decoded `dynamic_resources.esf` of the campaign map.
84    pub dynamic_resources: &'a ESF,
85
86    /// Key of the game, which numbers some records its own way (see [`victory`]).
87    pub game: &'a str,
88
89    /// Campaign key.
90    pub campaign: &'a str,
91
92    /// Campaign map key.
93    pub map: &'a str,
94
95    /// Localised name of the season the campaign starts in.
96    pub season_name: &'a str,
97
98    /// Localised screen names of the factions and names of the characters, by localisation key.
99    pub texts: &'a HashMap<String, String>,
100
101    /// RGB colours of the campaign map's region lookup image, by palette index.
102    pub region_palette: &'a [[u8; 3]],
103
104    /// Values the campaign's scripts save when the game builds the startpos, in order, or `None` if they couldn't run.
105    pub script_variables: Option<&'a [(String, LuaSavedValue)]>,
106}
107
108//---------------------------------------------------------------------------//
109//                              Implementations
110//---------------------------------------------------------------------------//
111
112/// Writes the startpos of a campaign.
113///
114/// # Arguments
115///
116/// * `inputs` - Tables, startpos rows, layouts and map data.
117/// * `plan` - Order and runtime ids of the world's objects.
118///
119/// # Errors
120///
121/// Returns an error if a table, column or layout is missing.
122pub fn write_startpos<'a>(inputs: &StartposInputs<'a>, plan: &WorldPlan<'a>) -> Result<ESF> {
123    let world = inputs.world;
124    let layout = world.layout;
125
126    let calendar = Calendar::new(world.tables, inputs.campaign)?;
127    let trading = international_routes::trading_factions(world.tables, world.index, plan, inputs.trade_network)?;
128    let world_record = write_world_with_plan(&WorldInputs { trading: Some(&trading), ..*world }, plan)?;
129    let estates = header::estates_by_owner(&world_record);
130    let trade_manager = trade::write_trade_manager(layout, world.tables, world.index, plan, inputs.trade_network)?;
131    let cai_inputs = CaiInputs {
132        tables: world.tables,
133        index: world.index,
134        layout,
135        world: &world_record,
136        map_data: inputs.map_data,
137        hlp_data: inputs.hlp_data,
138        trade_splines: inputs.trade_network.splines().len(),
139        trade_routes: &international_routes(layout, &trade_manager)?,
140        campaign: inputs.campaign,
141    };
142    let cai_context = CaiContext::new(&cai_inputs)?;
143    let cai = cai_context.write_interface(&cai_inputs)?;
144    let campaign_director = cai_context.write_campaign_director(layout)?;
145    let index_tables = counters::write_index_tables(layout, &[&world_record, &trade_manager])?;
146
147    let dynamic_resources_inputs = DynamicResourcesInputs { layout, tables: world.tables, index: world.index, plan, map_data: inputs.map_data, dynamic_resources: inputs.dynamic_resources };
148
149    let mut model = layout.builder(MODEL_PATH)?;
150    model.group()?
151        .set_record("WORLD", Some(world_record))?
152        .set_record("CAI_INTERFACE", Some(cai))?
153        .set_record("CAMPAIGN_DIRECTOR", Some(campaign_director))?
154        .set_record("CAMPAIGN_COMMAND_QUEUE_INDEX_TABLES", Some(index_tables))?
155        .set_record("CAMPAIGN_TRADE_MANAGER", Some(trade_manager))?
156        .set_record("DIPLOMACY_MANAGER", Some(diplomacy::write_diplomacy_manager(layout, plan)?))?
157        .set_record("CAMPAIGN_CALENDAR", Some(header::write_calendar(layout, &calendar)?))?
158        .set_record("CAMPAIGN_MAP_DATA", Some(map_data::write_map_data(world, plan, inputs.map)?))?
159        .set_record("DYNAMIC_RESOURCES_MANAGER", Some(dynamic_resources::write_dynamic_resources_manager(&dynamic_resources_inputs)?))?
160        .set_record("EPISODIC_RESTRICTIONS", Some(script_variables::write_episodic_restrictions(layout, inputs.script_variables)?))?;
161
162    let mut model = model.build()?;
163    remove_child_record(&mut model, AI_HISTORY);
164    let mut env = layout.builder(ENV_PATH)?;
165    env.group()?
166        .set_record("CAMPAIGN_SETUP", Some(preopen::write_campaign_setup(layout, plan, inputs.campaign)?))?
167        .set_record("CAMPAIGN_SETUP_LOCAL", Some(header::write_setup_local(layout, plan)?))?
168        .set_record("CAMPAIGN_MODEL", Some(model))?;
169
170    let forces = ForcePlan::new(world.tables, world.index, plan)?;
171    let mut root = layout.builder(ROOT_PATH)?;
172    root.group()?
173        .set_record("SAVE_GAME_HEADER", Some(header::write_save_game_header(inputs, plan, &forces, &calendar, &estates)?))?
174        .set_record("CAMPAIGN_PREOPEN_MAP_INFO", Some(preopen::write_preopen_map_info(inputs, plan, &forces, &calendar)?))?
175        .set_record("CAMPAIGN_ENV", Some(env.build()?))?;
176
177    let mut startpos = ESF::default();
178    startpos.set_root_node(NodeType::Record(Box::new(root.build()?)));
179    Ok(startpos)
180}
181
182/// Removes the child records with a name from every group of a record.
183fn remove_child_record(record: &mut RecordNode, name: &str) {
184    for group in record.children_mut() {
185        group.retain(|node| !matches!(node, NodeType::Record(child) if child.name() == name));
186    }
187}
188
189/// Returns a boolean leaf, with the compact encoding the game uses for them.
190pub(super) fn bool_node(value: bool) -> NodeType {
191    let mut node = BoolNode::default();
192    node.set_value(value);
193    node.set_optimized(true);
194    NodeType::Bool(node)
195}
196
197/// Returns an unsigned leaf, compact when the value fits.
198pub(super) fn u32_node(value: u32) -> NodeType {
199    let mut node = U32Node::default();
200    node.set_value(value);
201    node.set_optimized(value <= MAX_COMPACT_U32);
202    NodeType::U32(node)
203}
204
205/// Returns an unsigned array leaf, compact when it has values and every one fits.
206pub(super) fn u32s_node(values: Vec<u32>) -> NodeType {
207    let mut node = VecU32Node::default();
208    let optimized = !values.is_empty() && values.iter().all(|value| *value <= MAX_COMPACT_U32);
209    node.set_value(values);
210    node.set_optimized(optimized);
211    NodeType::U32Array(node)
212}
213
214/// Returns a signed leaf, compact when the value fits.
215pub(super) fn i32_node(value: i32) -> NodeType {
216    let mut node = I32Node::default();
217    node.set_value(value);
218    node.set_optimized(COMPACT_I32.contains(&value));
219    NodeType::I32(node)
220}
221
222/// Returns a float leaf, compact when it's zero.
223pub(super) fn f32_node(value: f32) -> NodeType {
224    let mut node = F32Node::default();
225    node.set_value(value);
226    node.set_optimized(value == 0.0);
227    NodeType::F32(node)
228}
229
230/// Returns a `Coord2d` leaf.
231pub(super) fn coordinates_node(x: f32, y: f32) -> NodeType {
232    let mut node = Coordinates2DNode::default();
233    node.set_x(x);
234    node.set_y(y);
235    NodeType::Coord2d(node)
236}