Skip to main content

rpfm_extensions/startpos/world/writer/
provinces.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 provinces (`WORLD/PROVINCE_MANAGER`) and the sea regions (`WORLD/SEA_REGION_MANAGER`).
12//!
13//! Both kinds of per-faction managers take recruitment manager ids from the counter the horde armies start: hordes
14//! first, then province managers, then sea managers.
15
16use std::collections::HashMap;
17
18use rpfm_lib::error::Result;
19use rpfm_lib::files::esf::RecordNode;
20
21use super::sight::{write_line_of_sight, SightPlan};
22use super::{child, horde_details, next_id, WorldInputs, WorldPlan, REBEL_FACTION_ID, WORLD_PATH};
23use crate::startpos::tables::{Row, StartposTables};
24use crate::startpos::vision::Vision;
25use crate::startpos::world::pools::{province_mercenary_pool, MercenaryUnit};
26use crate::startpos::world::provinces::{province_managers, sea_regions, ProvinceManager, SeaRegion};
27
28/// Path of a province.
29const PROVINCE_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/PROVINCE_MANAGER/PROVINCE_ARRAY/PROVINCE";
30
31/// Path of a faction's manager of a province.
32const PROVINCE_MANAGER_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/PROVINCE_MANAGER/PROVINCE_ARRAY/PROVINCE/FACTION_PROVINCE_MANAGER_ARRAY/FACTION_PROVINCE_MANAGER";
33
34/// Path of a sea region.
35const SEA_REGION_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/SEA_REGION_MANAGER/SEA_REGIONS_BLOCK/SEA_REGION";
36
37/// Path of a faction's manager of a sea region.
38const SEA_MANAGER_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/SEA_REGION_MANAGER/SEA_REGIONS_BLOCK/SEA_REGION/FACTION_SEA_REGION_MANAGER_BLOCK/FACTION_SEA_REGION_MANAGER";
39
40/// Limit of a province's public order, both ways (`province_public_order_*_threshold` campaign variables).
41const MAX_PUBLIC_ORDER: i64 = 100;
42
43/// Lowest public order of each populace happiness level, from jubilant to unhappy; lower values are outraged. The
44/// game hardcodes them: these fit every level of Attila's main campaign (jubilant, happy, indifferent, troubled).
45const HAPPINESS_THRESHOLDS: [i64; 6] = [80, 50, 20, -20, -50, -80];
46
47//---------------------------------------------------------------------------//
48//                              Types
49//---------------------------------------------------------------------------//
50
51/// Writes the provinces and the sea regions, numbering their managers as it goes.
52struct ProvinceWriter<'a, 'b> {
53
54    /// Tables, startpos rows and layouts.
55    inputs: &'b WorldInputs<'a>,
56
57    /// Order and runtime ids of the world's objects.
58    plan: &'b WorldPlan<'a>,
59
60    /// Vision of the world.
61    sight: &'b SightPlan,
62
63    /// `start_pos_regions` rows, by region key.
64    regions: HashMap<String, Row<'a>>,
65
66    /// Next recruitment manager id.
67    next_recruitment: u32,
68
69    /// Next mercenary pool id.
70    next_pool: u32,
71
72    /// Runtime ids of the provinces, by key.
73    province_ids: HashMap<String, u32>,
74}
75
76/// The province and sea region managers, with the ids later records continue from.
77#[derive(Debug, Clone)]
78pub struct ProvinceRecords {
79
80    /// The province manager.
81    pub provinces: RecordNode,
82
83    /// The sea region manager.
84    pub seas: RecordNode,
85
86    /// Next free recruitment manager id (for horde armies without horde details).
87    pub next_recruitment: u32,
88
89    /// Next free mercenary pool id (for levy pools).
90    pub next_pool: u32,
91
92    /// Runtime ids of the provinces, by key.
93    pub province_ids: HashMap<String, u32>,
94}
95
96//---------------------------------------------------------------------------//
97//                              Implementations
98//---------------------------------------------------------------------------//
99
100/// Writes the province manager and the sea region manager.
101///
102/// # Returns
103///
104/// The two records, with the ids later records continue from.
105///
106/// # Errors
107///
108/// Returns an error if a table, column or layout is missing.
109pub fn write_province_and_sea_managers(inputs: &WorldInputs, plan: &WorldPlan, sight: &SightPlan) -> Result<ProvinceRecords> {
110    let mut regions = HashMap::new();
111    for row in inputs.tables.table("start_pos_regions")?.rows() {
112        if row.str("campaign")? == *inputs.index.campaign() {
113            regions.insert(row.str("region")?.into_owned(), row);
114        }
115    }
116
117    let first_recruitment = u32::try_from(horde_details(inputs)?.len()).unwrap_or(u32::MAX).saturating_add(1);
118    // Mercenary pool ids continue the ones of the factions (the rebels' and one per faction).
119    let first_pool = u32::try_from(plan.factions().len()).unwrap_or(u32::MAX).saturating_add(REBEL_FACTION_ID + 1);
120    let mut writer = ProvinceWriter { inputs, plan, sight, regions, next_recruitment: first_recruitment, next_pool: first_pool, province_ids: HashMap::new() };
121    let provinces = writer.write_province_manager()?;
122    let seas = writer.write_sea_region_manager()?;
123    Ok(ProvinceRecords {
124        provinces,
125        seas,
126        next_recruitment: writer.next_recruitment,
127        next_pool: writer.next_pool,
128        province_ids: writer.province_ids,
129    })
130}
131
132impl ProvinceWriter<'_, '_> {
133
134    /// Writes the provinces, in `region_to_province_junctions` order (the game uses a hash order).
135    fn write_province_manager(&mut self) -> Result<RecordNode> {
136        let inputs = self.inputs;
137        let managers = province_managers(inputs.tables, inputs.index)?;
138
139        let provinces = campaign_provinces(inputs.tables, self.plan)?;
140
141        let mut next_manager = 1;
142        let mut array = child(inputs.layout, &format!("{WORLD_PATH}/PROVINCE_MANAGER"), "PROVINCE_ARRAY")?;
143        for (index, (province, regions)) in provinces.iter().enumerate() {
144            let province_id = u32::try_from(index + 1).unwrap_or(u32::MAX);
145            self.province_ids.insert(province.to_owned(), province_id);
146            let mut manager_array = child(inputs.layout, PROVINCE_PATH, "FACTION_PROVINCE_MANAGER_ARRAY")?;
147            for manager in managers.iter().filter(|manager| manager.province() == province) {
148                let record = self.write_province_faction_manager(manager, next_id(&mut next_manager))?;
149                manager_array.push_group()?.set_record("FACTION_PROVINCE_MANAGER", Some(record))?;
150            }
151
152            let mut record = self.inputs.layout.builder(PROVINCE_PATH)?;
153            record.group()?
154                .set_u32("^+1", province_id)?
155                .set_record("FACTION_PROVINCE_MANAGER_ARRAY", Some(manager_array.build()?))?
156                .set_record("PROVINCE_MERCENARY_POOL", Some(self.write_province_mercenary_pool(province)?))?
157                .set_record("PROVINCE_FERTILITY", Some(self.write_fertility(regions)?))?;
158            array.push_group()?
159                .set_str("^+1", province)?
160                .set_record("PROVINCE", Some(record.build()?))?;
161        }
162
163        let mut manager = child(inputs.layout, WORLD_PATH, "PROVINCE_MANAGER")?;
164        manager.group()?.set_record("PROVINCE_ARRAY", Some(array.build()?))?;
165        manager.build()
166    }
167
168    /// Writes a faction's manager of a province: its regions, public order and development.
169    fn write_province_faction_manager(&mut self, manager: &ProvinceManager, id: u32) -> Result<RecordNode> {
170        let layout = self.inputs.layout;
171        let public_order = self.sum(manager.regions(), "initial_public_order")?.clamp(-MAX_PUBLIC_ORDER, MAX_PUBLIC_ORDER);
172        let happiness = HAPPINESS_THRESHOLDS.iter().position(|threshold| public_order >= *threshold).unwrap_or(HAPPINESS_THRESHOLDS.len());
173
174        let mut info = child(layout, &format!("{PROVINCE_MANAGER_PATH}/FACTION_PROVINCE_PUBLIC_ORDER"), "FACTION_PROVINCE_PUBLIC_ORDER_INFO")?;
175        info.group()?
176            .set_i32("^+4", i32::try_from(public_order).unwrap_or_default())?
177            .set_i32("^+7", i32::try_from(happiness).unwrap_or_default())?;
178        let mut public_order_record = child(layout, PROVINCE_MANAGER_PATH, "FACTION_PROVINCE_PUBLIC_ORDER")?;
179        public_order_record.group()?.set_record("FACTION_PROVINCE_PUBLIC_ORDER_INFO", Some(info.build()?))?;
180
181        let mut development = child(layout, PROVINCE_MANAGER_PATH, "PROVINCE_DEVELOPMENT")?;
182        development.group()?.set_u32("^+1", u32::try_from(self.sum(manager.regions(), "development_points")?).unwrap_or_default())?;
183
184        let region_ids = manager.regions().iter().map(|region| self.plan.region_id(region).unwrap_or_default()).collect();
185        let mut record = layout.builder(PROVINCE_MANAGER_PATH)?;
186        record.group()?
187            .set_u32("^+1", id)?
188            .set_u32("^+2", self.faction_id(manager.faction().as_deref()))?
189            .set_u32s("^+3", region_ids)?
190            .set_record("RECRUITMENT_MANAGER", Some(self.write_recruitment_manager(PROVINCE_MANAGER_PATH)?))?
191            .set_record("FACTION_PROVINCE_PUBLIC_ORDER", Some(public_order_record.build()?))?
192            .set_record("PROVINCE_DEVELOPMENT", Some(development.build()?))?
193            .set_u32("CAMPAIGN_EFFECT_BUNDLES+4", self.plan.region_id(manager.main_region()).unwrap_or_default())?;
194        record.build()
195    }
196
197    /// Writes the mercenary pool of a province, with the next mercenary pool id.
198    fn write_province_mercenary_pool(&mut self, province: &str) -> Result<RecordNode> {
199        let id = next_id(&mut self.next_pool);
200        let path = format!("{PROVINCE_PATH}/PROVINCE_MERCENARY_POOL");
201        let units = province_mercenary_pool(self.inputs.tables, province)?;
202        let mut record = self.inputs.layout.builder(&path)?;
203        record.group()?
204            .set_u32("^+1", id)?
205            .set_record("MERCENARY_POOL", Some(write_mercenary_pool(self.inputs, &path, &units)?))?;
206        record.build()
207    }
208
209    /// Writes the fertility of a province: the sum of its regions' base fertility, and the list of its regions.
210    fn write_fertility(&self, regions: &[String]) -> Result<RecordNode> {
211        let path = format!("{PROVINCE_PATH}/PROVINCE_FERTILITY");
212        let mut block = child(self.inputs.layout, &path, "REGION_BLOCK")?;
213        for region in regions {
214            block.push_group()?.set_str("^+1", region)?;
215        }
216
217        let mut record = self.inputs.layout.builder(&path)?;
218        record.group()?
219            .set_i32("^+1", i32::try_from(self.sum(regions, "base_fertility")?).unwrap_or_default())?
220            .set_record("REGION_BLOCK", Some(block.build()?))?;
221        record.build()
222    }
223
224    /// Writes the sea regions with ports, in map order.
225    fn write_sea_region_manager(&mut self) -> Result<RecordNode> {
226        let inputs = self.inputs;
227        let seas = sea_regions(inputs.region_map, inputs.settlements, inputs.index);
228
229        let mut next_manager = 1;
230        let mut block = child(inputs.layout, &format!("{WORLD_PATH}/SEA_REGION_MANAGER"), "SEA_REGIONS_BLOCK")?;
231        for (index, sea) in seas.iter().enumerate() {
232            let record = self.write_sea_region(sea, u32::try_from(index + 1).unwrap_or(u32::MAX), &mut next_manager)?;
233            block.push_group()?
234                .set_str("^+1", sea.key())?
235                .set_record("SEA_REGION", Some(record))?;
236        }
237
238        let mut manager = child(inputs.layout, WORLD_PATH, "SEA_REGION_MANAGER")?;
239        manager.group()?.set_record("SEA_REGIONS_BLOCK", Some(block.build()?))?;
240        manager.build()
241    }
242
243    /// Writes a sea region: one manager per faction with ports on it, with the sorted ids of its port regions.
244    fn write_sea_region(&mut self, sea: &SeaRegion, id: u32, next_manager: &mut u32) -> Result<RecordNode> {
245        let layout = self.inputs.layout;
246        let mut managers = child(layout, SEA_REGION_PATH, "FACTION_SEA_REGION_MANAGER_BLOCK")?;
247        for (faction, regions) in sea.ports() {
248            let mut region_ids = regions.iter().map(|region| self.plan.region_id(region).unwrap_or_default()).collect::<Vec<_>>();
249            region_ids.sort_unstable();
250
251            // What the faction's port settlements see (an approximation of the game's sea vision).
252            let mut vision = Vision::default();
253            for region in regions {
254                if let Some(settlement) = self.sight.settlement(region) {
255                    vision.extend(settlement);
256                }
257            }
258
259            let mut manager = layout.builder(SEA_MANAGER_PATH)?;
260            manager.group()?
261                .set_u32("^+1", next_id(next_manager))?
262                .set_u32("^+2", self.faction_id(faction.as_deref()))?
263                .set_u32s("^+3", region_ids)?
264                .set_record("RECRUITMENT_MANAGER", Some(self.write_recruitment_manager(SEA_MANAGER_PATH)?))?
265                .set_record("LINE_OF_SIGHT", Some(write_line_of_sight(layout, SEA_MANAGER_PATH, self.sight.grid(), Some(&vision))?))?;
266            managers.push_group()?.set_record("FACTION_SEA_REGION_MANAGER", Some(manager.build()?))?;
267        }
268
269        let mut record = layout.builder(SEA_REGION_PATH)?;
270        record.group()?
271            .set_u32("^+1", id)?
272            .set_record("FACTION_SEA_REGION_MANAGER_BLOCK", Some(managers.build()?))?;
273        record.build()
274    }
275
276    /// Writes an empty recruitment manager under a parent record, with the next recruitment manager id.
277    fn write_recruitment_manager(&mut self, parent: &str) -> Result<RecordNode> {
278        let mut record = child(self.inputs.layout, parent, "RECRUITMENT_MANAGER")?;
279        record.group()?.set_u32("^+1", next_id(&mut self.next_recruitment))?;
280        record.build()
281    }
282
283    /// Returns the runtime id of a faction, or the rebels' one.
284    fn faction_id(&self, faction: Option<&str>) -> u32 {
285        faction.and_then(|faction| self.plan.faction_id(faction)).unwrap_or(REBEL_FACTION_ID)
286    }
287
288    /// Returns the sum of an integer column of `start_pos_regions` over some regions.
289    fn sum(&self, regions: &[String], column: &str) -> Result<i64> {
290        let mut total = 0;
291        for region in regions {
292            if let Some(row) = self.regions.get(region) {
293                total += row.int(column)?;
294            }
295        }
296        Ok(total)
297    }
298}
299
300/// Returns the provinces of a campaign with their regions, in `region_to_province_junctions` order.
301///
302/// # Arguments
303///
304/// * `tables` - Tables with `region_to_province_junctions`.
305/// * `plan` - The campaign's regions. Regions of other campaigns are left out.
306///
307/// # Errors
308///
309/// Returns an error if the table or a column is missing.
310pub(crate) fn campaign_provinces(tables: &StartposTables, plan: &WorldPlan) -> Result<Vec<(String, Vec<String>)>> {
311    let mut provinces = Vec::<(String, Vec<String>)>::new();
312    for row in tables.table("region_to_province_junctions")?.rows() {
313        let region = row.str("region")?;
314        if plan.region_id(&region).is_none() {
315            continue;
316        }
317        let province = row.str("province")?;
318        match provinces.iter_mut().find(|(key, _)| *key == province) {
319            Some((_, province_regions)) => province_regions.push(region.into_owned()),
320            None => provinces.push((province.into_owned(), vec![region.into_owned()])),
321        }
322    }
323    Ok(provinces)
324}
325
326/// Writes a mercenary pool (`MERCENARY_POOL`) under a parent record: one entry per unit, numbered in order.
327pub(super) fn write_mercenary_pool(inputs: &WorldInputs, parent: &str, units: &[MercenaryUnit]) -> Result<RecordNode> {
328    let path = format!("{parent}/MERCENARY_POOL");
329    let mut array = child(inputs.layout, &path, "MERCENARY_POOL_UNIT_ARRAY")?;
330    for (index, unit) in units.iter().enumerate() {
331        array.push_group()?
332            .set_str("^+1", unit.unit())?
333            .set_u32("^+2", unit.initial_count())?
334            .set_f32("^+3", unit.replenish_chance())?
335            .set_u32("^+4", unit.max_count())?
336            .set_u32("^+5", unit.max_replenish_per_turn())?
337            .set_u32("^+6", unit.max_count())?
338            .set_u32("^+8", u32::try_from(index).unwrap_or(u32::MAX))?;
339    }
340
341    let mut record = inputs.layout.builder(&path)?;
342    record.group()?.set_record("MERCENARY_POOL_UNIT_ARRAY", Some(array.build()?))?;
343    record.build()
344}