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