Skip to main content

rpfm_extensions/startpos/cai/
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 AI's theatre (`CAI_WORLD_THEATRES`), regions (`CAI_WORLD_REGIONS`) and their HLCIs
12//! (`CAI_WORLD_REGION_HLCIS`): one region per map region (land, then seas), each split into HLCIs, the connected groups
13//! of its areas.
14
15use getset::Setters;
16
17use std::collections::HashMap;
18
19use rpfm_lib::error::Result;
20use rpfm_lib::files::esf::RecordNode;
21
22use super::map::{MapRegion, PathfinderNode};
23use super::plan::CaiPlan;
24use super::world_view::WorldView;
25use crate::startpos::coords::HexCell;
26use crate::startpos::region_map::RegionMap;
27use super::{component, CAI_WORLD_PATH};
28use crate::startpos::layout::{child_path, StartposLayout};
29
30#[cfg(test)] #[path = "regions_test.rs"] mod regions_test;
31
32/// Theatre key of single-theatre campaigns, from `map_data.esf`'s theatre.
33pub const THEATRE_VALUE: u32 = 3;
34
35//---------------------------------------------------------------------------//
36//                              Types
37//---------------------------------------------------------------------------//
38
39/// What a region refers to beyond the map: AI objects of the world mirror.
40#[derive(Debug, Clone, Default, Setters)]
41#[getset(set = "pub")]
42pub struct RegionLinks {
43
44    /// Component of the region's settlement (land regions).
45    settlement: u32,
46
47    /// Components of the region's boundaries.
48    boundaries: Vec<u32>,
49
50    /// Components of the armies and fleets in the region.
51    mobiles: Vec<u32>,
52
53    /// World id of the region (land regions).
54    world_id: u32,
55
56    /// Component of the region's province (land regions).
57    province: u32,
58
59    /// Index of the sea region (seas).
60    sea_index: u32,
61
62    /// Components of the land regions with a port on this sea (seas).
63    port_regions: Vec<u32>,
64
65    /// Component of the sea this region's port opens to (land regions).
66    port_sea: u32,
67
68    /// Components of the factions' views of this sea (seas).
69    faction_seas: Vec<u32>,
70
71    /// Components of the factions with this region among their victory conditions.
72    victory_factions: Vec<u32>,
73}
74
75/// What the regions' links are built from.
76#[derive(Debug, Clone, Copy)]
77pub struct LinkInputs<'a> {
78
79    /// Component ids.
80    pub plan: &'a CaiPlan,
81
82    /// The world's objects.
83    pub world: &'a WorldView,
84
85    /// Regions of the map, in map order.
86    pub map_regions: &'a [MapRegion],
87
88    /// Region of each cell.
89    pub region_map: &'a RegionMap,
90
91    /// Each mobile's component and cell, in creation order.
92    pub mobiles: &'a [(u32, HexCell)],
93
94    /// Port cell of each garrison residence's settlement.
95    pub ports: &'a HashMap<u32, HexCell>,
96
97    /// Components of the factions with victory conditions on each region, by region key.
98    pub victory_factions: &'a HashMap<String, Vec<u32>>,
99}
100
101//---------------------------------------------------------------------------//
102//                              Implementations
103//---------------------------------------------------------------------------//
104
105/// Returns what each map region refers to beyond the map, in map order.
106pub fn region_links(inputs: &LinkInputs) -> Vec<RegionLinks> {
107    let plan = inputs.plan;
108    let world = inputs.world;
109    let mut links = vec![RegionLinks::default(); inputs.map_regions.len()];
110
111    for boundary in plan.boundaries() {
112        for region in [boundary.region(), boundary.other()] {
113            links[region].boundaries.push(boundary.id());
114        }
115    }
116    for (mobile, cell) in inputs.mobiles {
117        if let Some(region) = inputs.region_map.region_index_at(*cell).filter(|region| *region < links.len()) {
118            links[region].mobiles.push(*mobile);
119        }
120    }
121
122    let index_of = inputs.map_regions.iter().enumerate().map(|(index, region)| (region.key().as_str(), index)).collect::<HashMap<_, _>>();
123    for (index, region) in inputs.map_regions.iter().enumerate() {
124        let link = &mut links[index];
125        link.victory_factions = inputs.victory_factions.get(region.key()).cloned().unwrap_or_default();
126        if let Some(settlement) = plan.settlements().get(&index) {
127            link.settlement = settlement.settlement();
128        }
129        if let Some(world_region) = world.regions().iter().find(|world_region| world_region.key() == region.key()) {
130            link.world_id = world_region.id();
131        }
132        if let Some(province) = world.provinces().iter().zip(plan.provinces()).find_map(|(province, ids)| province.regions().contains(region.key()).then(|| ids.province())) {
133            link.province = province;
134        }
135        if let Some((sea, ids)) = world.seas().iter().zip(plan.faction_seas()).find(|(sea, _)| sea.key() == region.key()) {
136            link.sea_index = sea.id();
137            link.faction_seas = ids.to_vec();
138        }
139    }
140
141    // Each port opens to the sea of its cell. Seas list their ports by world region id.
142    let mut world_regions = world.regions().iter().collect::<Vec<_>>();
143    world_regions.sort_by_key(|world_region| world_region.id());
144    for world_region in world_regions {
145        let (Some(cell), Some(land)) = (inputs.ports.get(&world_region.residence()), index_of.get(world_region.key().as_str())) else { continue };
146        let Some(sea) = inputs.region_map.region_index_at(*cell).filter(|sea| *sea < links.len()) else { continue };
147        links[*land].port_sea = plan.regions()[sea].id();
148        links[sea].port_regions.push(plan.regions()[*land].id());
149    }
150    links
151}
152
153/// Writes the theatres list: one theatre holding every army, fleet and region.
154///
155/// # Arguments
156///
157/// * `layout` - Record layouts.
158/// * `plan` - Component ids.
159/// * `key` - Theatre key (`map_data.esf`'s theatre).
160/// * `mobiles` - Components of every army and fleet.
161///
162/// # Errors
163///
164/// Returns an error if the layout lacks the records.
165pub fn write_theatres(layout: &StartposLayout, plan: &CaiPlan, key: &str, mobiles: Vec<u32>) -> Result<RecordNode> {
166    let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_THEATRES");
167    let mut theatre = layout.builder(&child_path(&path, "CAI_THEATRE"))?;
168    theatre.group()?
169        .set_str("^+1", key)?
170        .set_u32s("^+2", mobiles)?
171        .set_u32s("^+4", plan.regions().iter().map(|region| region.id()).collect())?
172        .set_u32("^+6", THEATRE_VALUE)?;
173
174    let mut theatres = layout.builder(&path)?;
175    theatres.push_group()?
176        .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, plan.theatre(), vec![])?))?
177        .set_record("CAI_THEATRE", Some(theatre.build()?))?;
178    theatres.build()
179}
180
181/// Writes the regions list.
182///
183/// # Arguments
184///
185/// * `layout` - Record layouts.
186/// * `plan` - Component ids.
187/// * `map_regions` - Regions of the map, in map order.
188/// * `nodes` - Pathfinder nodes, by region index.
189/// * `links` - What each region refers to, in map order.
190///
191/// # Errors
192///
193/// Returns an error if the layout lacks the records.
194pub fn write_regions(layout: &StartposLayout, plan: &CaiPlan, map_regions: &[MapRegion], nodes: &HashMap<usize, Vec<PathfinderNode>>, links: &[RegionLinks]) -> Result<RecordNode> {
195    let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_REGIONS");
196    let region_path = child_path(&path, "CAI_REGION");
197    let owned_path = child_path(&path, "OWNED_INDIRECT");
198    let empty = RegionLinks::default();
199
200    let mut regions = layout.builder(&path)?;
201    for (index, (region, ids)) in map_regions.iter().zip(plan.regions()).enumerate() {
202        let links = links.get(index).unwrap_or(&empty);
203        let (x, y) = region_node(region, nodes.get(&index)).map(|node| (node.x(), node.y())).unwrap_or((0, 0));
204
205        let mut owned = layout.builder(&owned_path)?;
206        owned.group()?.set_u32("^+1", links.settlement)?;
207
208        let mut record = layout.builder(&region_path)?;
209        record.group()?
210            .set_u32s("^+1", vec![plan.theatre()])?
211            .set_u32s("^+2", ids.hlcis().to_vec())?
212            .set_u32("^+3", links.settlement)?
213            .set_u32("^+5", u32::from(x))?
214            .set_u32("^+6", u32::from(y))?
215            .set_u32s("^+8", links.boundaries.to_vec())?
216            .set_bool("^+9", region.connects_to_other_kind())?
217            .set_u32s("^+10", links.mobiles.to_vec())?
218            .set_str("^+11", region.key())?
219            .set_u32("^+12", links.world_id)?
220            .set_u32s("^+13", links.victory_factions.to_vec())?
221            .set_u32("^+14", links.province)?
222            .set_u32("^+15", links.sea_index)?
223            .set_u32s("^+16", links.port_regions.to_vec())?
224            .set_u32("^+17", links.port_sea)?
225            .set_u32s("^+18", links.faction_seas.to_vec())?;
226
227        regions.push_group()?
228            .set_record("OWNED_INDIRECT", Some(owned.build()?))?
229            .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, ids.id(), vec![])?))?
230            .set_record("CAI_REGION", Some(record.build()?))?;
231    }
232    regions.build()
233}
234
235/// Writes the HLCIs list: each region's HLCIs, region by region.
236///
237/// # Arguments
238///
239/// * `layout` - Record layouts.
240/// * `plan` - Component ids.
241/// * `map_regions` - Regions of the map, in map order.
242/// * `nodes` - Pathfinder nodes, by region index.
243///
244/// # Errors
245///
246/// Returns an error if the layout lacks the records.
247pub fn write_hlcis(layout: &StartposLayout, plan: &CaiPlan, map_regions: &[MapRegion], nodes: &HashMap<usize, Vec<PathfinderNode>>) -> Result<RecordNode> {
248    let path = child_path(CAI_WORLD_PATH, "CAI_WORLD_REGION_HLCIS");
249    let hlci_path = child_path(&path, "CAI_REGION_HLCI");
250
251    let mut hlcis = layout.builder(&path)?;
252    for (index, (region, ids)) in map_regions.iter().zip(plan.regions()).enumerate() {
253        let node = region_node(region, nodes.get(&index));
254        let main = node.and_then(|node| region.hlci_of_area(node.area() as usize));
255        for (hlci_index, ((group, areas), id)) in region.hlcis().into_iter().zip(ids.hlcis()).enumerate() {
256            let (x, y) = node.filter(|_| main == Some(hlci_index)).map(|node| (node.x(), node.y())).unwrap_or((0, 0));
257            let mut record = layout.builder(&hlci_path)?;
258            record.group()?
259                .set_u32("^+1", ids.id())?
260                .set_u32("^+2", group)?
261                .set_u32s("^+3", areas)?
262                .set_u32("^+4", u32::from(x))?
263                .set_u32("^+5", u32::from(y))?;
264
265            hlcis.push_group()?
266                .set_record("CAI_BDI_COMPONENT", Some(component(layout, &path, *id, vec![])?))?
267                .set_record("CAI_REGION_HLCI", Some(record.build()?))?;
268        }
269    }
270    hlcis.build()
271}
272
273/// Returns where the AI places each region (see [`region_node`]), or (0, 0) for regions without nodes.
274///
275/// # Arguments
276///
277/// * `map_regions` - Regions of the map, in map order.
278/// * `nodes` - Pathfinder nodes, by region index.
279pub fn region_positions(map_regions: &[MapRegion], nodes: &HashMap<usize, Vec<PathfinderNode>>) -> Vec<(u16, u16)> {
280    map_regions.iter().enumerate()
281        .map(|(index, region)| region_node(region, nodes.get(&index)).map(|node| (node.x(), node.y())).unwrap_or((0, 0)))
282        .collect()
283}
284
285/// Returns the pathfinder node where the AI places a region: the one in its largest area, else its first one.
286fn region_node(region: &MapRegion, nodes: Option<&Vec<PathfinderNode>>) -> Option<PathfinderNode> {
287    let nodes = nodes?;
288    let largest = region.largest_area();
289    nodes.iter().find(|node| Some(node.area() as usize) == largest).or_else(|| nodes.first()).copied()
290}