1use 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
34const GDP_GAMES: [&str; 1] = [KEY_THRONES_OF_BRITANNIA];
37
38const GDP_BONUS_PREFIX: &str = "gdp_";
40
41const GDP_MODIFIER_PREFIX: &str = "gdp_mod";
43
44const REGION_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/REGION_MANAGER/REGIONS_ARRAY/REGION";
46
47const SLOT_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/REGION_MANAGER/REGIONS_ARRAY/REGION/REGION_SLOT_MANAGER/REGION_SLOT_ARRAY/REGION_SLOT";
49
50const SETTLEMENT_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/REGION_MANAGER/REGIONS_ARRAY/REGION/SETTLEMENT";
52
53const EXPANSION_PATH: &str = "CAMPAIGN_STARTPOS/CAMPAIGN_ENV/CAMPAIGN_MODEL/WORLD/REGION_MANAGER/REGIONS_ARRAY/REGION/SETTLEMENT/SETTLEMENT_EXPANSION_MANAGER/SETTLEMENT_EXPANSION_MAP_DATA";
55
56const REGION_NAME_PREFIX: &str = "regions_onscreen_";
58
59const SETTLEMENT_NAME_PREFIX: &str = "start_pos_settlements_onscreen_name_";
61
62const ABANDONED_REBELS_NAME: &str = "campaign_localised_strings_string_abandoned_region_rebel_faction_name";
64
65const ABANDONED_REBELS_VALUE: u32 = 12;
67
68const POPULATION_CLASSES: [&str; 3] = ["upper", "lower", "middle"];
70
71const POPULATION_CLASS_VALUE: u32 = 7;
73
74const NO_POSITION: u32 = 65535;
76
77const BUILDING_BASE_VALUE_OFFSET: i64 = 3;
79
80struct RegionWriter<'a, 'b> {
86
87 inputs: &'b WorldInputs<'a>,
89
90 plan: &'b WorldPlan<'a>,
92
93 forces: &'b ForcePlan<'a>,
95
96 sight: &'b SightPlan,
98
99 rows: RegionRows<'a>,
101
102 next_slot: u32,
104
105 next_residence: u32,
107
108 next_building: u32,
110
111 next_estate: u32,
113
114 region_estates: HashMap<(String, String), u32>,
116
117 region_gdp: i64,
119}
120
121struct RegionRows<'a> {
123
124 regions: HashMap<String, Row<'a>>,
126
127 settlement_keys: HashMap<String, String>,
129
130 governments: HashMap<String, String>,
132
133 templates: HashMap<(String, String), String>,
135
136 building_levels: HashMap<String, (i64, i64)>,
138
139 religions: Vec<String>,
141
142 religion_shares: HashMap<(String, String), f32>,
144
145 estates: EstateRules,
147
148 building_gdp: HashMap<String, i64>,
150}
151
152#[derive(Debug, Clone, Copy)]
154struct SlotContent<'s> {
155
156 slot_type: &'s str,
158
159 building: Option<&'s str>,
161}
162
163pub 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 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 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 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 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 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 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 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 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 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 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 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
501fn 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
556fn 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
571fn 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
582fn 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
593pub(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
611pub(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
624pub(crate) fn settlement_name_key(settlement_key: &str, region: &RegionEntry) -> String {
626 format!("{SETTLEMENT_NAME_PREFIX}{settlement_key}{}", region.id())
627}
628
629fn 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
635fn has_port(map_layout: &SettlementLayout) -> bool {
637 map_layout.slots().get(1).is_some_and(|slot| slot.position().is_some())
638}
639
640fn 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
654fn 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
665fn 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}