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