Skip to main content

rpfm_extensions/startpos/cai/
settlements.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 AI's settlements (`CAI_WORLD_SETTLEMENTS`) and building slots (`CAI_WORLD_BUILDING_SLOTS`: the
12//! settlements' slots, then the horde armies' slots).
13
14use std::collections::HashMap;
15
16use rpfm_lib::error::Result;
17use rpfm_lib::files::esf::RecordNode;
18
19use super::factions::{owned_direct, REBELS_WORLD_ID};
20use super::map::{AreaGroups, MapRegion};
21use super::mobiles::{situated, MobileInputs};
22use super::plan::CaiPlan;
23use super::world_view::{ForceView, WorldView};
24use super::{component, CAI_WORLD_PATH};
25use crate::startpos::coords::HexCell;
26use crate::startpos::layout::{child_path, StartposLayout};
27use crate::startpos::settlements::SettlementLayout;
28
29#[cfg(test)] #[path = "settlements_test.rs"] mod settlements_test;
30
31/// Slot type (`CAI_BUILDING_SLOT^+3`) of ports.
32const PORT_SLOT_TYPE: u32 = 3;
33
34/// Slot type of horde army slots.
35const HORDE_SLOT_TYPE: u32 = 9;
36
37/// Position of the port slot in a settlement's slots (the main slot comes first).
38const PORT_SLOT: usize = 1;
39
40//---------------------------------------------------------------------------//
41//                              Implementations
42//---------------------------------------------------------------------------//
43
44/// Returns the cell where the AI places the port of each garrison residence's settlement.
45///
46/// # Arguments
47///
48/// * `world` - The world's objects.
49/// * `map_regions` - Regions of the map, in map order.
50/// * `groups` - Cell lookups of the map.
51pub fn port_cells(world: &WorldView, map_regions: &[MapRegion], groups: &AreaGroups) -> HashMap<u32, HexCell> {
52    world.regions().iter()
53        .filter_map(|region| {
54            let port = map_regions.iter().find(|map_region| map_region.key() == region.key())?.port()?;
55            Some((region.residence(), groups.port_cell(port)?))
56        })
57        .collect()
58}
59
60/// Returns the component of the settlement of each garrison residence.
61///
62/// # Arguments
63///
64/// * `world` - The world's objects.
65/// * `plan` - Component ids.
66/// * `map_regions` - Regions of the map, in map order.
67pub fn settlement_components(world: &WorldView, plan: &CaiPlan, map_regions: &[MapRegion]) -> HashMap<u32, u32> {
68    map_regions.iter().enumerate()
69        .filter_map(|(index, map_region)| {
70            let region = world.regions().iter().find(|region| region.key() == map_region.key())?;
71            Some((region.residence(), plan.settlements().get(&index)?.settlement()))
72        })
73        .collect()
74}
75
76/// Writes the settlements list, in map order.
77///
78/// # Arguments
79///
80/// * `inputs` - The world, plan and map lookups.
81/// * `map_regions` - Regions of the map, in map order.
82/// * `layouts` - Settlement layouts of the map.
83/// * `positions` - Where the AI places each map region (see [`super::regions::region_positions`]).
84/// * `capitals` - Component of the faction whose capital each map region is (see [`super::factions::capitals`]).
85///
86/// # Errors
87///
88/// Returns an error if the layout lacks the records.
89pub fn write_settlements(inputs: &MobileInputs, map_regions: &[MapRegion], positions: &[(u16, u16)], capitals: &HashMap<usize, u32>) -> Result<RecordNode> {
90    let layout = inputs.layout;
91    let plan = inputs.plan;
92    let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_SETTLEMENTS");
93    let garrisonable_path = child_path(&path, "CAI_GARRISONABLE");
94    let threatened_path = child_path(&path, "CAI_THREATENED_GARRISONABLE");
95
96    // The mobile of each force, by world force id, and the faction component of each world faction id.
97    let mut mobiles = HashMap::new();
98    for (position, faction) in inputs.world.factions().iter().enumerate() {
99        for (character, ids) in faction.characters().iter().zip(&plan.characters()[position]) {
100            if let Some(mobile) = ids.mobile().filter(|_| character.force() != 0) {
101                mobiles.insert(character.force(), mobile);
102            }
103        }
104    }
105    let mut faction_components = inputs.world.factions().iter().zip(plan.factions()).map(|(faction, ids)| (faction.id(), ids.faction())).collect::<HashMap<_, _>>();
106    if let Some(rebels) = plan.factions().last() {
107        faction_components.insert(REBELS_WORLD_ID, rebels.faction());
108    }
109
110    let forces = inputs.world.factions().iter().flat_map(|faction| faction.forces()).collect::<Vec<_>>();
111    let mut list = layout.builder(&path)?;
112    for (index, map_region) in map_regions.iter().enumerate() {
113        let (Some(ids), Some(region)) = (plan.settlements().get(&index), inputs.world.regions().iter().find(|region| region.key() == map_region.key())) else { continue };
114        let mobile_of = |matches: fn(&ForceView, u32) -> bool| forces.iter()
115            .find(|force| matches(force, region.residence()))
116            .and_then(|force| mobiles.get(&force.id()))
117            .copied()
118            .unwrap_or_default();
119
120        let mut garrisonable = layout.builder(&garrisonable_path)?;
121        garrisonable.group()?
122            .set_u32("^+1", mobile_of(|force, residence| force.residence() == residence && force.garrison_army_of() == 0 && force.garrison_navy_of() == 0))?
123            .set_u32("^+3", mobile_of(|force, residence| force.garrison_army_of() == residence))?
124            .set_u32("^+4", mobile_of(|force, residence| force.garrison_navy_of() == residence))?
125            .set_u32s("^+10", vec![])?;
126
127        let port = inputs.ports.get(&region.residence()).copied();
128        let mut threatened = layout.builder(&threatened_path)?;
129        threatened.group()?
130            .set_bool("^+1", port.is_some())?
131            .set_record("CAI_SITUATED", port.map(|cell| situated(inputs, &threatened_path, cell)).transpose()?)?;
132
133        let mut settlement = layout.builder(&child_path(&path, "CAI_SETTLEMENT"))?;
134        settlement.group()?
135            .set_u32s("^+1", ids.slots().to_vec())?
136            .set_u32("^+2", capitals.get(&index).copied().unwrap_or_default())?
137            .set_u32("^+3", region.id())?;
138
139        let (x, y) = positions.get(index).copied().unwrap_or_default();
140        list.push_group()?
141            .set_record("OWNED_DIRECT", Some(owned_direct(layout, &path, faction_components.get(&region.owner()).copied().unwrap_or_default())?))?
142            .set_record("CAI_SITUATED", Some(situated(inputs, &path, HexCell::new(x, y))?))?
143            .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, ids.settlement(), vec![])?))?
144            .set_record("CAI_GARRISONABLE", Some(garrisonable.build()?))?
145            .set_record("CAI_THREATENED_GARRISONABLE", Some(threatened.build()?))?
146            .set_record("CAI_SETTLEMENT", Some(settlement.build()?))?;
147    }
148    list.build()
149}
150
151/// Writes the building slots list: each settlement's slots in map order, then each horde army's slots.
152///
153/// # Arguments
154///
155/// * `layout` - Record layouts.
156/// * `plan` - Component ids.
157/// * `world` - The world's objects.
158/// * `map_regions` - Regions of the map, in map order.
159/// * `layouts` - Settlement layouts of the map.
160///
161/// # Errors
162///
163/// Returns an error if the layout lacks the records.
164pub fn write_building_slots(layout: &StartposLayout, plan: &CaiPlan, world: &WorldView, map_regions: &[MapRegion], layouts: &[SettlementLayout]) -> Result<RecordNode> {
165    let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_BUILDING_SLOTS");
166    let slot_path = child_path(&path, "CAI_BUILDING_SLOT");
167    let mut list = layout.builder(&path)?;
168    // Settlement slots hold their world slot id first, horde slots in the second field.
169    let mut push = |id: u32, settlement_slot: u32, horde_slot: u32, kind: u32, settlement: u32, horde: u32| -> Result<()> {
170        let mut record = layout.builder(&slot_path)?;
171        record.group()?
172            .set_u32("^+1", settlement_slot)?
173            .set_u32("^+2", horde_slot)?
174            .set_u32("^+3", kind)?
175            .set_u32("^+4", settlement)?
176            .set_u32("^+5", horde)?;
177        list.push_group()?
178            .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, id, vec![])?))?
179            .set_record("CAI_BUILDING_SLOT", Some(record.build()?))?;
180        Ok(())
181    };
182
183    for (index, map_region) in map_regions.iter().enumerate() {
184        let (Some(ids), Some(region)) = (plan.settlements().get(&index), world.regions().iter().find(|region| region.key() == map_region.key())) else { continue };
185        let has_port = layouts.iter().find(|settlement| settlement.region() == map_region.key()).and_then(|settlement| settlement.slots().get(PORT_SLOT)).is_some_and(|slot| slot.position().is_some());
186        for (slot, (id, world_id)) in ids.slots().iter().zip(region.slots()).enumerate() {
187            let kind = if has_port && slot == PORT_SLOT { PORT_SLOT_TYPE } else { 0 };
188            push(*id, *world_id, 0, kind, ids.settlement(), 0)?;
189        }
190    }
191
192    for (position, faction) in world.factions().iter().enumerate() {
193        for (character, ids) in faction.characters().iter().zip(&plan.characters()[position]) {
194            let Some(force) = faction.forces().iter().find(|force| character.force() != 0 && force.id() == character.force()) else { continue };
195            for (id, world_id) in ids.horde_slots().iter().zip(force.horde_slots()) {
196                push(*id, 0, *world_id, HORDE_SLOT_TYPE, 0, ids.mobile().unwrap_or_default())?;
197            }
198        }
199    }
200    list.build()
201}