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