Skip to main content

rpfm_extensions/startpos/world/writer/
regions.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 `REGION` records (`WORLD/REGION_MANAGER/REGIONS_ARRAY`), with their slots and settlements.
12
13use std::collections::{HashMap, HashSet};
14
15use rpfm_lib::error::{RLibError, Result};
16use rpfm_lib::files::esf::RecordNode;
17use rpfm_lib::games::supported_games::KEY_THRONES_OF_BRITANNIA;
18
19use super::factions::write_flag;
20use super::forces::{ForcePlan, PlannedForce};
21use super::sight::{write_line_of_sight, SightPlan};
22use super::{child, horde_details, next_id, HORDE_BUILDING_COLUMNS, WorldInputs, WorldPlan, REBEL_FACTION_ID, WORLD_PATH};
23use crate::startpos::coords::{EdgePiece, HexCell};
24use crate::startpos::settlements::{SettlementLayout, SlotLayout};
25use crate::startpos::tables::{Row, StartposTables};
26use crate::startpos::world::{CharacterEntry, RegionEntry};
27use crate::startpos::world::estates::EstateRules;
28use crate::startpos::world::government::faction_leader;
29use crate::startpos::world::legacy::ForceKind;
30use crate::startpos::world::rebels::rebel_setup;
31
32#[cfg(test)] #[path = "regions_test.rs"] mod regions_test;
33
34/// Games whose startposes store their regions' base GDP (`SETTLEMENT+2`): Attila's are 0 there, though its
35/// buildings have GDP effects too.
36const GDP_GAMES: [&str; 1] = [KEY_THRONES_OF_BRITANNIA];
37
38/// Prefix of the bonus values of a building's base GDP.
39const GDP_BONUS_PREFIX: &str = "gdp_";
40
41/// Prefix of the bonus values that modify GDP by a percentage.
42const GDP_MODIFIER_PREFIX: &str = "gdp_mod";
43
44/// Path of a region.
45const REGION_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/REGION_MANAGER/REGIONS_ARRAY/REGION";
46
47/// Path of a region slot.
48const SLOT_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/REGION_MANAGER/REGIONS_ARRAY/REGION/REGION_SLOT_MANAGER/REGION_SLOT_ARRAY/REGION_SLOT";
49
50/// Path of a settlement.
51const SETTLEMENT_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/REGION_MANAGER/REGIONS_ARRAY/REGION/SETTLEMENT";
52
53/// Path of a settlement's expansion data.
54const EXPANSION_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/REGION_MANAGER/REGIONS_ARRAY/REGION/SETTLEMENT/SETTLEMENT_EXPANSION_MANAGER/SETTLEMENT_EXPANSION_MAP_DATA";
55
56/// Localisation key prefix of the region names.
57const REGION_NAME_PREFIX: &str = "regions_onscreen_";
58
59/// Localisation key prefix of the settlement names (followed by `settlement:<region key><region row id>`).
60const SETTLEMENT_NAME_PREFIX: &str = "start_pos_settlements_onscreen_name_";
61
62/// Localisation key of the name of the rebels holding an abandoned region.
63const ABANDONED_REBELS_NAME: &str = "campaign_localised_strings_string_abandoned_region_rebel_faction_name";
64
65/// Value of `REBEL_SETUP^+5` for the rebels holding an abandoned region (meaning unknown).
66const ABANDONED_REBELS_VALUE: u32 = 12;
67
68/// Population classes of every region, in startpos order.
69const POPULATION_CLASSES: [&str; 3] = ["upper", "lower", "middle"];
70
71/// Value of the fifth field of every population class (meaning unknown).
72const POPULATION_CLASS_VALUE: u32 = 7;
73
74/// Coordinate the startpos uses for slots without position (the port slot of settlements without port).
75const NO_POSITION: u32 = 65535;
76
77/// Added to a building's level to get the lowest value of the two floats of its `BUILDING_BASE`.
78const BUILDING_BASE_VALUE_OFFSET: i64 = 3;
79
80//---------------------------------------------------------------------------//
81//                              Types
82//---------------------------------------------------------------------------//
83
84/// Writes the regions, numbering their slots, residences and buildings as it goes.
85struct RegionWriter<'a, 'b> {
86
87    /// Tables, startpos rows and layouts.
88    inputs: &'b WorldInputs<'a>,
89
90    /// Order and runtime ids of the world's objects.
91    plan: &'b WorldPlan<'a>,
92
93    /// Characters and forces of the world.
94    forces: &'b ForcePlan<'a>,
95
96    /// Vision of the world.
97    sight: &'b SightPlan,
98
99    /// Table rows the regions use, indexed once.
100    rows: RegionRows<'a>,
101
102    /// Next slot id.
103    next_slot: u32,
104
105    /// Next garrison residence id (slots and settlements share the counter).
106    next_residence: u32,
107
108    /// Next building id (horde and region buildings share the counter).
109    next_building: u32,
110
111    /// Id of the next estate.
112    next_estate: u32,
113
114    /// Estates written so far in each region, by estate type.
115    region_estates: HashMap<(String, String), u32>,
116
117    /// Base GDP of the buildings written so far in the current region.
118    region_gdp: i64,
119}
120
121/// Table rows the region writer looks up, indexed once.
122struct RegionRows<'a> {
123
124    /// `start_pos_regions` rows, by row id.
125    regions: HashMap<String, Row<'a>>,
126
127    /// `start_pos_settlements.settlement_id`, by row id.
128    settlement_keys: HashMap<String, String>,
129
130    /// Government types of the campaign's factions, by faction key.
131    governments: HashMap<String, String>,
132
133    /// Slot templates, by region key and slot type.
134    templates: HashMap<(String, String), String>,
135
136    /// `create_time` and `level` of the building levels, by key.
137    building_levels: HashMap<String, (i64, i64)>,
138
139    /// Religion keys, in `religions` order.
140    religions: Vec<String>,
141
142    /// Religion percentages, by region row id and religion key.
143    religion_shares: HashMap<(String, String), f32>,
144
145    /// Estates the buildings give, and the owners the table gives them.
146    estates: EstateRules,
147
148    /// Base GDP each building level gives (see [`building_gdp`]).
149    building_gdp: HashMap<String, i64>,
150}
151
152/// The kind of a region slot, with its building.
153#[derive(Debug, Clone, Copy)]
154struct SlotContent<'s> {
155
156    /// Slot type (`primary`, `port` or `secondary`), as in `start_pos_region_slot_templates`.
157    slot_type: &'s str,
158
159    /// Building level in the slot, if any.
160    building: Option<&'s str>,
161}
162
163//---------------------------------------------------------------------------//
164//                              Implementations
165//---------------------------------------------------------------------------//
166
167/// Writes the region manager.
168///
169/// # Errors
170///
171/// Returns an error if a table, column or layout is missing, or a region has no settlement layout.
172pub fn write_region_manager<'a>(inputs: &WorldInputs<'a>, plan: &WorldPlan<'a>, forces: &ForcePlan<'a>, sight: &SightPlan) -> Result<RecordNode> {
173    let mut writer = RegionWriter {
174        inputs,
175        plan,
176        forces,
177        sight,
178        rows: RegionRows::new(inputs)?,
179        next_slot: 1,
180        next_residence: 1,
181        next_building: horde_building_count(inputs)? + 1,
182        next_estate: 1,
183        region_estates: HashMap::new(),
184        region_gdp: 0,
185    };
186
187    let mut regions = child(inputs.layout, &format!("{WORLD_PATH}/REGION_MANAGER"), "REGIONS_ARRAY")?;
188    for region in plan.regions() {
189        regions.push_group()?.set_record("REGION", Some(writer.write_region(region)?))?;
190    }
191
192    let mut manager = child(inputs.layout, WORLD_PATH, "REGION_MANAGER")?;
193    manager.group()?.set_record("REGIONS_ARRAY", Some(regions.build()?))?;
194    manager.build()
195}
196
197impl<'a> RegionRows<'a> {
198
199    /// Indexes the rows the region writer needs.
200    fn new(inputs: &WorldInputs<'a>) -> Result<Self> {
201        let tables = inputs.tables;
202        let campaign = inputs.index.campaign();
203
204        let mut regions = HashMap::new();
205        for row in tables.table("start_pos_regions")?.rows() {
206            regions.insert(row.str("id")?.into_owned(), row);
207        }
208
209        let settlement_keys = settlement_keys(inputs)?;
210
211        let mut governments = HashMap::new();
212        for row in tables.table("start_pos_factions")?.rows() {
213            if row.str("campaign")? == *campaign {
214                governments.insert(row.str("faction")?.into_owned(), row.str("government_type")?.into_owned());
215            }
216        }
217
218        let mut templates = HashMap::new();
219        for row in tables.table("start_pos_region_slot_templates")?.rows() {
220            if row.str("campaign")? == *campaign {
221                templates.insert((row.str("region")?.into_owned(), row.str("slot_type")?.into_owned()), row.str("slot_template")?.into_owned());
222            }
223        }
224
225        let mut building_levels = HashMap::new();
226        for row in tables.table("building_levels")?.rows() {
227            building_levels.insert(row.str("level_name")?.into_owned(), (row.int("create_time")?, row.int("level")?));
228        }
229
230        let religions = tables.table("religions")?.rows()
231            .map(|row| Ok(row.str("religion_key")?.into_owned()))
232            .collect::<Result<Vec<_>>>()?;
233
234        let mut religion_shares = HashMap::new();
235        for row in tables.table("start_pos_region_religions")?.rows() {
236            religion_shares.insert((row.str("region")?.into_owned(), row.str("religion")?.into_owned()), row.f32("percentage")?);
237        }
238
239        Ok(Self { regions, settlement_keys, governments, templates, building_levels, religions, religion_shares, estates: EstateRules::new(tables)?, building_gdp: building_gdp(tables)? })
240    }
241}
242
243impl RegionWriter<'_, '_> {
244
245    /// Writes a region.
246    fn write_region(&mut self, region: &RegionEntry) -> Result<RecordNode> {
247        let layout = self.inputs.layout;
248        let key = region.key();
249        let row = *self.rows.regions.get(region.id())
250            .ok_or_else(|| RLibError::StartposInvalidValue("start_pos_regions".to_owned(), "id".to_owned(), region.id().to_owned()))?;
251        let wealth = u32::try_from(row.int("town_wealth")?).unwrap_or_default();
252        let owner = region.owner().as_deref().and_then(|owner| self.plan.faction_id(owner)).unwrap_or(REBEL_FACTION_ID);
253        let map_layout = settlement_layout(self.inputs, region)?;
254
255        let mut localisation = child(layout, REGION_PATH, "CAMPAIGN_LOCALISATION")?;
256        localisation.group()?.set_str("^+1", &format!("{REGION_NAME_PREFIX}{key}"))?;
257
258        self.region_gdp = 0;
259        let slots = self.write_slot_manager(region, map_layout)?;
260        let settlement = self.write_settlement(region, map_layout)?;
261
262        let mut record = layout.builder(REGION_PATH)?;
263        record.group()?
264            .set_u32("^+1", self.plan.region_id(key).unwrap_or_default())?
265            .set_str("^+2", key)?
266            .set_record("POPULATION", Some(self.write_population(region)?))?
267            .set_record("REGION_SLOT_MANAGER", Some(slots))?
268            .set_record("SETTLEMENT", Some(settlement))?
269            .set_u32("SETTLEMENT+2", if GDP_GAMES.contains(&self.inputs.game) { u32::try_from(self.region_gdp).unwrap_or_default() } else { 0 })?
270            .set_u32("SETTLEMENT+3", wealth)?
271            .set_u32("SETTLEMENT+4", wealth)?
272            .set_u32("SETTLEMENT+5", wealth)?
273            .set_u32("SETTLEMENT+9", owner)?
274            .set_str("LINE_OF_SIGHT+1", &row.str("rebel_faction")?)?
275            .set_str("LINE_OF_SIGHT+3", &row.str("alternative_rebel_faction")?)?
276            .set_str("LINE_OF_SIGHT+4", &row.str("cultural_originator")?)?
277            .set_record("CAMPAIGN_LOCALISATION", Some(localisation.build()?))?
278            .set_record("REGION_REBEL_MANAGER", Some(self.write_rebel_manager(region)?))?
279            .set_bool("REGION_REBEL_MANAGER+1", region.owner().is_none())?
280            .set_record("LINE_OF_SIGHT", Some(write_line_of_sight(layout, REGION_PATH, self.sight.grid(), self.sight.region(key))?))?;
281        record.build()
282    }
283
284    /// Writes the population of a region: its religion shares (see [`religion_shares`]), in `religions` order.
285    fn write_population(&self, region: &RegionEntry) -> Result<RecordNode> {
286        let percentages = self.rows.religions.iter()
287            .map(|religion| self.rows.religion_shares.get(&(region.id().to_owned(), religion.to_owned())).copied().unwrap_or_default())
288            .collect::<Vec<_>>();
289
290        let path = format!("{REGION_PATH}/POPULATION/REGION_FACTORS");
291        let mut breakdown = child(self.inputs.layout, &path, "RELIGION_BREAKDOWN")?;
292        for (religion, share) in self.rows.religions.iter().zip(religion_shares(&percentages)) {
293            breakdown.push_group()?
294                .set_str("^+1", religion)?
295                .set_f32("^+2", share)?;
296        }
297
298        let classes_path = format!("{path}/POPULATION CLASSES");
299        let mut classes = self.inputs.layout.builder(&classes_path)?;
300        for class in POPULATION_CLASSES {
301            let mut record = child(self.inputs.layout, &classes_path, "POPULATION_CLASS")?;
302            record.group()?
303                .set_str("^+1", class)?
304                .set_u32("^+5", POPULATION_CLASS_VALUE)?;
305            classes.push_group()?.set_record("POPULATION_CLASS", Some(record.build()?))?;
306        }
307
308        let mut factors = self.inputs.layout.builder(&path)?;
309        factors.group()?
310            .set_record("POPULATION CLASSES", Some(classes.build()?))?
311            .set_record("RELIGION_BREAKDOWN", Some(breakdown.build()?))?;
312
313        let mut population = child(self.inputs.layout, REGION_PATH, "POPULATION")?;
314        population.group()?.set_record("REGION_FACTORS", Some(factors.build()?))?;
315        population.build()
316    }
317
318    /// Writes the rebel manager of a region: the default setup, or the setup of the rebels holding the region.
319    fn write_rebel_manager(&self, region: &RegionEntry) -> Result<RecordNode> {
320        let layout = self.inputs.layout;
321        let path = format!("{REGION_PATH}/REGION_REBEL_MANAGER");
322        let mut manager = layout.builder(&path)?;
323        if let Some(setup) = rebel_setup(self.inputs.tables, region)? {
324            let setup_path = format!("{path}/REBEL_SETUP");
325            let mut name = child(layout, &setup_path, "CAMPAIGN_LOCALISATION")?;
326            name.group()?.set_str("^+1", ABANDONED_REBELS_NAME)?.unset("^+2")?;
327            let name = name.build()?;
328
329            let mut record = layout.builder(&setup_path)?;
330            record.group()?
331                .set_str("^+2", setup.faction())?
332                .set_str("^+3", setup.religion())?
333                .set_u32("^+5", ABANDONED_REBELS_VALUE)?
334                .set_record("CAMPAIGN_LOCALISATION", Some(name.clone()))?
335                .set_record("CAMPAIGN_LOCALISATION@2", Some(name))?
336                .set_record("FACTION_FLAG_AND_COLOURS", Some(write_flag(self.inputs, &setup_path, setup.flag())?))?;
337            manager.group()?
338                .set_record("REBEL_SETUP", Some(record.build()?))?
339                .set_bool("REBEL_SETUP+1", true)?;
340        }
341        manager.build()
342    }
343
344    /// Writes the slot manager of a region: the primary slot, the port slot if the settlement has a port, then the
345    /// secondary slots, with their buildings.
346    fn write_slot_manager(&mut self, region: &RegionEntry, map_layout: &SettlementLayout) -> Result<RecordNode> {
347        let inputs = self.inputs;
348        let settlement = inputs.index.settlement_of(region.key());
349        let primary = settlement.map(|settlement| settlement.primary_building().as_str()).filter(|building| !building.is_empty());
350        let port = settlement.map(|settlement| settlement.port_building().as_str()).filter(|building| !building.is_empty());
351        let others = settlement.map(|settlement| settlement.buildings().as_slice()).unwrap_or_default();
352
353        let mut contents = vec![SlotContent { slot_type: "primary", building: primary }];
354        if has_port(map_layout) {
355            contents.push(SlotContent { slot_type: "port", building: port });
356        }
357        for index in 0..map_layout.slots().len().saturating_sub(2) {
358            contents.push(SlotContent { slot_type: "secondary", building: others.get(index).map(String::as_str) });
359        }
360
361        let mut array = child(inputs.layout, &format!("{REGION_PATH}/REGION_SLOT_MANAGER"), "REGION_SLOT_ARRAY")?;
362        for (index, content) in contents.iter().enumerate() {
363            let slot = self.write_slot(region, index, content)?;
364            array.push_group()?.set_record("REGION_SLOT", Some(slot))?;
365        }
366
367        let mut manager = child(inputs.layout, REGION_PATH, "REGION_SLOT_MANAGER")?;
368        manager.group()?.set_record("REGION_SLOT_ARRAY", Some(array.build()?))?;
369        manager.build()
370    }
371
372    /// Writes a region slot and its building.
373    fn write_slot(&mut self, region: &RegionEntry, index: usize, content: &SlotContent) -> Result<RecordNode> {
374        let layout = self.inputs.layout;
375        let template = self.rows.templates.get(&(region.key().to_owned(), content.slot_type.to_owned())).cloned().unwrap_or_default();
376
377        let mut residence = child(layout, SLOT_PATH, "GARRISON_RESIDENCE")?;
378        residence.group()?.set_u32("^+1", next_id(&mut self.next_residence))?;
379
380        let building = content.building.map(|building| self.write_building(region, building, content.slot_type == "primary")).transpose()?;
381        let estates = self.write_estates(region, content.building.unwrap_or_default())?;
382        self.region_gdp += content.building.and_then(|building| self.rows.building_gdp.get(building)).copied().unwrap_or_default();
383        let mut manager = child(layout, SLOT_PATH, "REGION_BUILDING_MANAGER")?;
384        manager.group()?
385            .set_bool("^+2", building.is_some())?
386            .set_bool("^+5", building.is_some())?
387            .set_bool("^+6", region.owner().is_none())?
388            .set_record("BUILDING_BASE", building)?
389            .set_record("ESTATES", Some(estates))?;
390
391        let mut record = layout.builder(SLOT_PATH)?;
392        record.group()?
393            .set_u32("^+1", next_id(&mut self.next_slot))?
394            .set_record("GARRISON_RESIDENCE", Some(residence.build()?))?
395            .set_record("REGION_BUILDING_MANAGER", Some(manager.build()?))?
396            .set_str("REGION_BUILDING_MANAGER+1", &format!("{}:{index}", region.key()))?
397            .set_str("REGION_BUILDING_MANAGER+2", &template)?
398            .set_str("REGION_BUILDING_MANAGER+3", content.slot_type)?;
399        record.build()
400    }
401
402    /// Writes the estates a slot's building gives (see [`EstateRules`]), numbered in writing order.
403    fn write_estates(&mut self, region: &RegionEntry, building: &str) -> Result<RecordNode> {
404        let path = format!("{SLOT_PATH}/REGION_BUILDING_MANAGER");
405        let mut estates = child(self.inputs.layout, &path, "ESTATES")?;
406        let owner_faction = self.building_owner(region);
407        for estate in self.rows.estates.estates_of(building) {
408            let number = self.region_estates.entry((region.key().to_owned(), estate.to_owned())).or_default();
409            *number += 1;
410            let owner = match self.rows.estates.table_owner(region.key(), estate, *number) {
411                Some(id) => self.character_id(|entry| *entry.id() == id),
412                None => faction_leader(self.inputs.index, &owner_faction).and_then(|leader| self.character_id(|entry| entry.id() == leader.id())),
413            };
414
415            let mut record = child(self.inputs.layout, &format!("{path}/ESTATES"), "ESTATE")?;
416            record.push_group()?
417                .set_u32("^+1", next_id(&mut self.next_estate))?
418                .set_u32("^+2", owner.unwrap_or_default())?
419                .set_str("^+3", estate)?;
420            estates.push_group()?
421                .set_str("^+1", estate)?
422                .set_record("ESTATE", Some(record.build()?))?;
423        }
424        estates.build()
425    }
426
427    /// Returns the runtime id of the first planned character whose table row matches.
428    fn character_id(&self, matches: impl Fn(&CharacterEntry) -> bool) -> Option<u32> {
429        self.forces.characters().iter().find(|character| character.entry().is_some_and(&matches)).map(|character| character.id())
430    }
431
432    /// Returns the faction that owns a region's buildings: its owner, or the first faction of the world for regions
433    /// without one.
434    fn building_owner(&self, region: &RegionEntry) -> String {
435        region.owner().as_deref()
436            .or_else(|| self.plan.factions().first().map(|faction| faction.key().as_str()))
437            .unwrap_or_default()
438            .to_owned()
439    }
440
441    /// Writes a building of a region slot (`BUILDING_BASE`).
442    ///
443    /// Buildings of unowned regions belong to the first faction of the world. The two floats of the record (meaning
444    /// unknown) are the level's `create_time`, raised to its `level` plus 3, plus 1 in the primary slot.
445    fn write_building(&mut self, region: &RegionEntry, building: &str, in_primary_slot: bool) -> Result<RecordNode> {
446        let owner = self.building_owner(region);
447        let government = self.rows.governments.get(&owner).map(String::as_str).unwrap_or_default();
448        let (create_time, level) = self.rows.building_levels.get(building).copied()
449            .ok_or_else(|| RLibError::StartposInvalidValue("building_levels".to_owned(), "level_name".to_owned(), building.to_owned()))?;
450        let value = create_time.max(level + BUILDING_BASE_VALUE_OFFSET + i64::from(in_primary_slot)) as f32;
451
452        let mut record = child(self.inputs.layout, &format!("{SLOT_PATH}/REGION_BUILDING_MANAGER"), "BUILDING_BASE")?;
453        record.group()?
454            .set_u32("^+1", next_id(&mut self.next_building))?
455            .set_str("^+2", building)?
456            .set_f32("^+3", value)?
457            .set_f32("^+4", value)?
458            .set_str("^+5", &owner)?
459            .set_str("^+6", government)?;
460        record.build()
461    }
462
463    /// Writes the settlement of a region: its residence (id after the ones of the region's slots, the general's army
464    /// starting in it and its garrison forces), name and expansion data.
465    fn write_settlement(&mut self, region: &RegionEntry, map_layout: &SettlementLayout) -> Result<RecordNode> {
466        let layout = self.inputs.layout;
467        let key = region.key();
468        let settlement_key = self.inputs.index.settlement_of(key)
469            .and_then(|settlement| self.rows.settlement_keys.get(settlement.id()))
470            .cloned()
471            .unwrap_or_default();
472
473        let mut localisation = child(layout, SETTLEMENT_PATH, "CAMPAIGN_LOCALISATION")?;
474        localisation.group()?.set_str("^+1", &settlement_name_key(&settlement_key, region))?;
475
476        let zone = write_cells(self.inputs, &format!("{SETTLEMENT_PATH}/GARRISON_RESIDENCE/ZOE_BLOCK"), &garrison_zone(map_layout))?;
477        let mut residence = child(layout, SETTLEMENT_PATH, "GARRISON_RESIDENCE")?;
478        let force_id = |force: Option<&PlannedForce>| force.map(|force| force.id()).unwrap_or_default();
479        residence.group()?
480            .set_u32("^+1", next_id(&mut self.next_residence))?
481            .set_u32("^+2", force_id(self.forces.stationed_in(key)))?
482            .set_record("ZOE_BLOCK", Some(zone))?
483            .set_record("LINE_OF_SIGHT", Some(write_line_of_sight(layout, &format!("{SETTLEMENT_PATH}/GARRISON_RESIDENCE"), self.sight.grid(), self.sight.settlement(key))?))?
484            .set_u32("ZOE_BLOCK+3", force_id(self.forces.garrison_of(key, ForceKind::GarrisonArmy)))?
485            .set_u32("ZOE_BLOCK+4", force_id(self.forces.garrison_of(key, ForceKind::GarrisonNavy)))?;
486
487        let mut expansion = child(layout, SETTLEMENT_PATH, "SETTLEMENT_EXPANSION_MANAGER")?;
488        expansion.group()?.set_record("SETTLEMENT_EXPANSION_MAP_DATA", Some(write_expansion_map_data(self.inputs, map_layout)?))?;
489
490        let mut record = layout.builder(SETTLEMENT_PATH)?;
491        record.group()?
492            .set_record("GARRISON_RESIDENCE", Some(residence.build()?))?
493            .set_u32("GARRISON_RESIDENCE+1", self.plan.region_id(key).unwrap_or_default())?
494            .set_record("CAMPAIGN_LOCALISATION", Some(localisation.build()?))?
495            .set_str("CAMPAIGN_LOCALISATION+2", &settlement_key)?
496            .set_record("SETTLEMENT_EXPANSION_MANAGER", Some(expansion.build()?))?;
497        record.build()
498    }
499}
500
501/// Writes the expansion map data of a settlement: its slots and its sprawl cells.
502///
503/// Each sprawl cell gets the two edge masks of the map, its road pieces (the edges of both masks leading to other
504/// sprawl cells) and its coast pieces (the edges leading to water cells, the ones outside every campaign region).
505fn write_expansion_map_data(inputs: &WorldInputs, map_layout: &SettlementLayout) -> Result<RecordNode> {
506    let mut secondary = child(inputs.layout, EXPANSION_PATH, "SECONDARY_SLOT_ARRAY")?;
507    for slot in map_layout.slots().iter().skip(2) {
508        secondary.push_group()?.set_record("SLOT_MAP_DATA", Some(write_slot_map_data(inputs, &format!("{EXPANSION_PATH}/SECONDARY_SLOT_ARRAY/SLOT_MAP_DATA"), slot)?))?;
509    }
510
511    let sprawl_path = format!("{EXPANSION_PATH}/SPRAWL_BLOCK");
512    let mut sprawl = inputs.layout.builder(&sprawl_path)?;
513    let sprawl_cells = map_layout.sprawl().iter().map(|cell| cell.cell()).collect::<HashSet<_>>();
514    let is_water = |cell: HexCell| inputs.region_map.region_at(cell).is_some_and(|region| inputs.index.region(region).is_none());
515    for cell in map_layout.sprawl() {
516        let hex_cell = cell.cell();
517        let [first_edges, second_edges] = inputs.region_map.edge_masks(hex_cell);
518        let roads = (first_edges | second_edges) & hex_cell.neighbour_mask(|neighbour| sprawl_cells.contains(&neighbour));
519        let coast = hex_cell.neighbour_mask(is_water);
520        let road_piece = EdgePiece::from_mask(roads);
521        let coast_piece = EdgePiece::from_mask(coast);
522
523        let mut hex = child(inputs.layout, &sprawl_path, "SPRAWL_HEX_MAP_DATA")?;
524        hex.group()?
525            .set_u32("^+1", u32::from(hex_cell.x()))?
526            .set_u32("^+2", u32::from(hex_cell.y()))?
527            .set_bool("^+3", cell.in_footprint())?
528            .set_bool("^+4", cell.in_zone())?
529            .set_u32("^+5", u32::from(first_edges))?
530            .set_u32("^+6", u32::from(second_edges))?
531            .set_bool("^+7", is_water(hex_cell))?
532            .set_u32("^+8", u32::from(roads))?
533            .set_i32("^+9", i32::from(road_piece.shape()))?
534            .set_u32("^+10", u32::from(road_piece.rotation()))?
535            .set_u32("^+11", cell.zone_slots())?
536            .set_u32("^+12", u32::from(coast))?
537            .set_i32("^+13", i32::from(coast_piece.shape()))?
538            .set_u32("^+14", u32::from(coast_piece.rotation()))?;
539        sprawl.push_group()?
540            .set_u32("^+1", u32::from(hex_cell.x()))?
541            .set_u32("^+2", u32::from(hex_cell.y()))?
542            .set_record("SPRAWL_HEX_MAP_DATA", Some(hex.build()?))?;
543    }
544
545    let slot_path = format!("{EXPANSION_PATH}/SLOT_MAP_DATA");
546    let mut record = inputs.layout.builder(EXPANSION_PATH)?;
547    record.group()?
548        .set_str("^+1", map_layout.region())?
549        .set_record("SLOT_MAP_DATA", Some(write_slot_map_data(inputs, &slot_path, &map_layout.slots()[0])?))?
550        .set_record("SLOT_MAP_DATA@2", Some(write_slot_map_data(inputs, &slot_path, &map_layout.slots()[1])?))?
551        .set_record("SECONDARY_SLOT_ARRAY", Some(secondary.build()?))?
552        .set_record("SPRAWL_BLOCK", Some(sprawl.build()?))?;
553    record.build()
554}
555
556/// Writes the map data of a slot (`SLOT_MAP_DATA`): id, position, footprint, zone, walls and touching slots.
557fn write_slot_map_data(inputs: &WorldInputs, path: &str, slot: &SlotLayout) -> Result<RecordNode> {
558    let (x, y) = slot.position().map(|cell| (u32::from(cell.x()), u32::from(cell.y()))).unwrap_or((NO_POSITION, NO_POSITION));
559    let mut record = inputs.layout.builder(path)?;
560    record.group()?
561        .set_u32("^+1", slot.id())?
562        .set_u32("^+2", x)?
563        .set_u32("^+3", y)?
564        .set_record("SLOT_FOOTPRINT_ARRAY", Some(write_cells(inputs, &format!("{path}/SLOT_FOOTPRINT_ARRAY"), slot.footprint())?))?
565        .set_record("SLOT_ZOE_ARRAY", Some(write_cells(inputs, &format!("{path}/SLOT_ZOE_ARRAY"), slot.zone())?))?
566        .set_record("SLOT_WALL_ARRAY", Some(write_cells(inputs, &format!("{path}/SLOT_WALL_ARRAY"), slot.walls())?))?
567        .set_u32("SLOT_WALL_ARRAY+1", slot.touching_slots())?;
568    record.build()
569}
570
571/// Writes a cell array: one group per cell, with its column and row.
572fn write_cells(inputs: &WorldInputs, path: &str, cells: &[HexCell]) -> Result<RecordNode> {
573    let mut record = inputs.layout.builder(path)?;
574    for cell in cells {
575        record.push_group()?
576            .set_u32("^+1", u32::from(cell.x()))?
577            .set_u32("^+2", u32::from(cell.y()))?;
578    }
579    record.build()
580}
581
582/// Returns the zone of control of a settlement's garrison: the main slot's footprint and zone, and the port's zone.
583fn garrison_zone(map_layout: &SettlementLayout) -> Vec<HexCell> {
584    let mut seen = HashSet::new();
585    let main = &map_layout.slots()[0];
586    let port = &map_layout.slots()[1];
587    main.footprint().iter().chain(main.zone()).chain(port.zone())
588        .filter(|cell| seen.insert(**cell))
589        .copied()
590        .collect()
591}
592
593/// Returns the garrison residence id of each region's settlement, by region key. The residence counter numbers each
594/// region's slots, then its settlement, in region order.
595///
596/// # Errors
597///
598/// Returns an error if a region has no settlement layout.
599pub(crate) fn settlement_residences(inputs: &WorldInputs, plan: &WorldPlan) -> Result<HashMap<String, u32>> {
600    let mut residences = HashMap::new();
601    let mut next_residence = 1;
602    for region in plan.regions() {
603        let map_layout = settlement_layout(inputs, region)?;
604        let slots = 1 + usize::from(has_port(map_layout)) + map_layout.slots().len().saturating_sub(2);
605        next_residence += u32::try_from(slots).unwrap_or(u32::MAX);
606        residences.insert(region.key().to_owned(), next_id(&mut next_residence));
607    }
608    Ok(residences)
609}
610
611/// Returns the `start_pos_settlements.settlement_id` of every settlement, by row id.
612///
613/// # Errors
614///
615/// Returns an error if the table or a column is missing.
616pub(crate) fn settlement_keys(inputs: &WorldInputs) -> Result<HashMap<String, String>> {
617    let mut keys = HashMap::new();
618    for row in inputs.tables.table("start_pos_settlements")?.rows() {
619        keys.insert(row.str("id")?.into_owned(), row.str("settlement_id")?.into_owned());
620    }
621    Ok(keys)
622}
623
624/// Returns the localisation key of the name of a region's settlement, from its `settlement_id`.
625pub(crate) fn settlement_name_key(settlement_key: &str, region: &RegionEntry) -> String {
626    format!("{SETTLEMENT_NAME_PREFIX}{settlement_key}{}", region.id())
627}
628
629/// Returns the settlement layout of a region.
630fn settlement_layout<'a>(inputs: &WorldInputs<'a>, region: &RegionEntry) -> Result<&'a SettlementLayout> {
631    inputs.settlements.iter().find(|settlement| settlement.region() == region.key())
632        .ok_or_else(|| RLibError::StartposInvalidValue("map_data".to_owned(), "SETTLEMENT_INFO".to_owned(), region.key().to_owned()))
633}
634
635/// Returns if a settlement has a port (its second primary slot has a position).
636fn has_port(map_layout: &SettlementLayout) -> bool {
637    map_layout.slots().get(1).is_some_and(|slot| slot.position().is_some())
638}
639
640/// Returns the number of horde buildings of the campaign. The game numbers them before the region buildings, with
641/// the same counter.
642fn horde_building_count(inputs: &WorldInputs) -> Result<u32> {
643    let mut count = 0;
644    for row in horde_details(inputs)? {
645        for column in HORDE_BUILDING_COLUMNS {
646            if !row.str(column)?.is_empty() {
647                count += 1;
648            }
649        }
650    }
651    Ok(count)
652}
653
654/// Returns the religion shares of a region from the percentages of its religions.
655///
656/// Shares are normalised, so percentages that don't add up to 100 still give a total of 1. The game computes them
657/// in single precision as `(percentage * 0.01) / (total * 0.01)`, which isn't always the same float as
658/// `percentage / total`. Regions without percentages have every religion in the same share (verified on Thrones).
659fn religion_shares(percentages: &[f32]) -> Vec<f32> {
660    let total = percentages.iter().sum::<f32>() * 0.01;
661    let even = 1.0 / percentages.len().max(1) as f32;
662    percentages.iter().map(|percentage| if total > 0.0 { percentage * 0.01 / total } else { even }).collect()
663}
664
665/// Returns the base GDP each building level gives: the values of its `building_effects_junction` effects whose
666/// `effect_bonus_value_basic_junction` bonus value is a base GDP one (`gdp_*`, not the `gdp_mod_*` modifiers). A region
667/// stores the sum of its buildings' (`SETTLEMENT+2`). Verified on Thrones: 228 of 229 regions.
668///
669/// # Errors
670///
671/// Returns an error if a table or column is missing.
672fn building_gdp(tables: &StartposTables) -> Result<HashMap<String, i64>> {
673    let mut effects = HashSet::new();
674    for row in tables.table("effect_bonus_value_basic_junction")?.rows() {
675        let bonus = row.str("bonus_value_id")?;
676        if bonus.starts_with(GDP_BONUS_PREFIX) && !bonus.starts_with(GDP_MODIFIER_PREFIX) {
677            effects.insert(row.str("effect")?.into_owned());
678        }
679    }
680    let mut gdp = HashMap::new();
681    for row in tables.table("building_effects_junction")?.rows() {
682        if effects.contains(row.str("effect")?.as_ref()) {
683            *gdp.entry(row.str("building")?.into_owned()).or_default() += row.number("value")? as i64;
684        }
685    }
686    Ok(gdp)
687}