Skip to main content

rpfm_extensions/startpos/cai/
map.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//! What the campaign AI reads from the map files: the regions of `map_data.esf` (land regions, then seas), their
12//! areas and connections, and the high-level pathfinder nodes of `hlp_data.esf`.
13
14use getset::{CopyGetters, Getters};
15
16use std::collections::{HashMap, HashSet};
17
18use rpfm_lib::error::{RLibError, Result};
19use rpfm_lib::files::esf::{NodeType, RecordNode, ESF};
20
21use crate::startpos::coords::HexCell;
22use crate::startpos::esf_nav::{as_record, as_str, as_u32, cell_groups, child_record, leaves};
23use crate::startpos::region_map::RegionMap;
24use crate::startpos::settlements::{region_data_records, region_key};
25
26#[cfg(test)] #[path = "map_test.rs"] mod map_test;
27
28/// Bits of a pathfinder node id holding its region index. The bits above hold the index of the area it lies in.
29const NODE_REGION_BITS: u32 = 9;
30
31/// Connection lists of a map region, in the order the AI walks them.
32const CONNECTIVITY_RECORDS: [&str; 2] = ["CONNECTIVITY_DIRECT_TRAVERSABLE_SAME", "CONNECTIVITY_DIRECT_TRAVERSABLE_OTHER"];
33
34//---------------------------------------------------------------------------//
35//                              Types
36//---------------------------------------------------------------------------//
37
38/// A region of the map (`REGION_DATA`), land or sea.
39#[derive(Debug, Clone, PartialEq, Getters, CopyGetters)]
40pub struct MapRegion {
41
42    /// Region key.
43    #[getset(get = "pub")]
44    key: String,
45
46    /// Connectivity group of each of the region's areas (`REGION_AREA_DATA`), in map order.
47    #[getset(get = "pub")]
48    area_groups: Vec<u32>,
49
50    /// Size of each of the region's areas, in map order.
51    #[getset(get = "pub")]
52    area_sizes: Vec<u32>,
53
54    /// Directly traversable connections to other regions (same kind of region first, then the other kind), as
55    /// (region index, value) pairs.
56    #[getset(get = "pub")]
57    connections: Vec<(usize, u32)>,
58
59    /// The region connects directly to a region of the other kind (land to sea or sea to land).
60    #[getset(get_copy = "pub")]
61    connects_to_other_kind: bool,
62
63    /// The region has a settlement (land regions do, seas don't).
64    #[getset(get_copy = "pub")]
65    has_settlement: bool,
66
67    /// Position of the settlement's port, if it has one (the first of `PORT_POSITIONS_BLOCK`).
68    #[getset(get_copy = "pub")]
69    port: Option<HexCell>,
70}
71
72/// The connectivity group of each cell (land masses and seas), from the group of the area it's in. Seas without areas
73/// take the most common group of the sea areas around them.
74#[derive(Debug, Clone)]
75pub struct AreaGroups<'a> {
76
77    /// Region and area of each cell.
78    map: &'a RegionMap,
79
80    /// Regions of the map, in map order.
81    regions: &'a [MapRegion],
82
83    /// Group of each sea without areas, by region index.
84    sea_groups: HashMap<usize, u32>,
85}
86
87/// Reads the integer leaves of a flat list one by one.
88struct Cursor<'a> {
89    values: &'a [&'a NodeType],
90    position: usize,
91}
92
93/// A high-level pathfinder node: a cell where the AI can place a region.
94#[derive(Debug, Clone, Copy, PartialEq, Eq, CopyGetters)]
95#[getset(get_copy = "pub")]
96pub struct PathfinderNode {
97
98    /// Index of the region's area the node lies in.
99    area: u32,
100
101    /// Cell column.
102    x: u16,
103
104    /// Cell row.
105    y: u16,
106}
107
108//---------------------------------------------------------------------------//
109//                              Implementations
110//---------------------------------------------------------------------------//
111
112impl MapRegion {
113
114    /// Reads every region of a `map_data.esf`, in map order.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if the map lacks its regions block or a region is malformed.
119    pub fn all_from_map_data(map_data: &ESF) -> Result<Vec<Self>> {
120        region_data_records(map_data)?.into_iter().map(Self::from_region_data).collect()
121    }
122
123    /// Reads a `REGION_DATA` record.
124    fn from_region_data(record: &RecordNode) -> Result<Self> {
125        let key = region_key(record)?.to_owned();
126        let mut area_groups = vec![];
127        let mut area_sizes = vec![];
128        for area in child_record(record, "REGION_AREAS").iter().flat_map(|areas| areas.children().iter().flatten().filter_map(as_record)) {
129            let values = area.children().iter().flat_map(|group| leaves(group)).collect::<Vec<_>>();
130
131            // After the type byte and the two corners: the group, four bounds, two lists, a cell, and the size.
132            let group = values.get(3).and_then(|node| as_u32(node));
133            let size = values.last().and_then(|node| as_u32(node));
134            let (Some(group), Some(size)) = (group, size) else {
135                return Err(RLibError::CampaignMapDataError(format!("region {key}: an area is malformed")));
136            };
137            area_groups.push(group);
138            area_sizes.push(size);
139        }
140
141        let mut connections = vec![];
142        for name in CONNECTIVITY_RECORDS {
143            for group in child_record(record, name).iter().flat_map(|list| list.children()) {
144                let mut values = leaves(group).filter_map(as_u32);
145                if let (Some(region), Some(value)) = (values.next(), values.next()) {
146                    connections.push((region as usize, value));
147                }
148            }
149        }
150        let connects_to_other_kind = child_record(record, CONNECTIVITY_RECORDS[1]).is_some_and(|list| !list.children().is_empty());
151        let settlement = child_record(record, "SETTLEMENT_INFO");
152        let has_settlement = settlement.is_some();
153        let port = settlement.and_then(|settlement| child_record(settlement, "PORT_POSITIONS_BLOCK")).and_then(|ports| cell_groups(ports).first().copied());
154
155        Ok(Self { key, area_groups, area_sizes, connections, connects_to_other_kind, has_settlement, port })
156    }
157
158    /// Returns the region's HLCIs: the distinct connectivity groups of its areas, in the order they first appear,
159    /// each with the indexes of its areas.
160    pub fn hlcis(&self) -> Vec<(u32, Vec<u32>)> {
161        let mut hlcis: Vec<(u32, Vec<u32>)> = vec![];
162        for (index, group) in self.area_groups.iter().enumerate() {
163            let index = index as u32;
164            match hlcis.iter_mut().find(|(hlci_group, _)| hlci_group == group) {
165                Some((_, areas)) => areas.push(index),
166                None => hlcis.push((*group, vec![index])),
167            }
168        }
169        hlcis
170    }
171
172    /// Returns the index of the region's largest area (the first one on ties), if it has areas.
173    pub fn largest_area(&self) -> Option<usize> {
174        self.area_sizes.iter().enumerate().max_by_key(|(index, size)| (**size, std::cmp::Reverse(*index))).map(|(index, _)| index)
175    }
176
177    /// Returns the index of the HLCI holding an area.
178    pub fn hlci_of_area(&self, area: usize) -> Option<usize> {
179        let group = self.area_groups.get(area)?;
180        self.hlcis().iter().position(|(hlci_group, _)| hlci_group == group)
181    }
182}
183
184impl<'a> AreaGroups<'a> {
185
186    /// Finds the groups of the seas without areas.
187    ///
188    /// # Arguments
189    ///
190    /// * `map` - Region and area of each cell.
191    /// * `regions` - Regions of the map, in map order.
192    pub fn new(map: &'a RegionMap, regions: &'a [MapRegion]) -> Self {
193        let mut groups = Self { map, regions, sea_groups: HashMap::new() };
194        let is_bare_sea = |region: usize| regions.get(region).is_some_and(|region| !region.has_settlement && region.area_groups.is_empty());
195
196        // Seas next to other bare seas only get a group once those have one, so repeat until nothing changes.
197        let (width, height) = map.size();
198        loop {
199            let mut counts: HashMap<usize, HashMap<u32, usize>> = HashMap::new();
200            for y in 0..height as u16 {
201                for x in 0..width as u16 {
202                    let cell = HexCell::new(x, y);
203                    let Some(region) = map.region_index_at(cell).filter(|region| is_bare_sea(*region) && !groups.sea_groups.contains_key(region)) else { continue };
204                    for neighbour in cell.neighbours() {
205                        let is_other_sea = map.region_index_at(neighbour).is_some_and(|other| other != region && regions.get(other).is_some_and(|other| !other.has_settlement));
206                        if let Some(group) = groups.group_at(neighbour).filter(|_| is_other_sea) {
207                            *counts.entry(region).or_default().entry(group).or_default() += 1;
208                        }
209                    }
210                }
211            }
212            if counts.is_empty() {
213                break;
214            }
215            for (region, counts) in counts {
216                let group = counts.into_iter().max_by_key(|(group, count)| (*count, std::cmp::Reverse(*group))).map(|(group, _)| group).unwrap_or_default();
217                groups.sea_groups.insert(region, group);
218            }
219        }
220        groups
221    }
222
223    /// Returns the sea cell where the AI places a port: the sea cell closest to the port's position, searching
224    /// neighbours ring by ring in the game's direction order. The game picks other cells for most ports (its rule
225    /// isn't known), always within a few cells.
226    pub fn port_cell(&self, port: HexCell) -> Option<HexCell> {
227        let is_sea = |cell: HexCell| self.map.region_index_at(cell).and_then(|region| self.regions.get(region)).is_some_and(|region| !region.has_settlement);
228        let mut seen = HashSet::from([port]);
229        let mut ring = vec![port];
230        while !ring.is_empty() {
231            let mut next = vec![];
232            for cell in ring {
233                for neighbour in cell.neighbours() {
234                    if self.map.region_index_at(neighbour).is_none() || !seen.insert(neighbour) {
235                        continue;
236                    }
237                    if is_sea(neighbour) {
238                        return Some(neighbour);
239                    }
240                    next.push(neighbour);
241                }
242            }
243            ring = next;
244        }
245        None
246    }
247
248    /// Returns the group of a cell, or `None` for cells outside the map or in seas without a known group.
249    pub fn group_at(&self, cell: HexCell) -> Option<u32> {
250        let region = self.map.region_index_at(cell)?;
251        let area = self.map.area_at(cell)?;
252        self.regions.get(region)?.area_groups.get(area).copied().or_else(|| self.sea_groups.get(&region).copied())
253    }
254}
255
256/// Returns the key of the map's theatre (`REGIONS_DATA/theatres/CAMPAIGN_THEATRE`'s string).
257///
258/// # Errors
259///
260/// Returns an error if the map lacks its theatre.
261pub fn theatre_key(map_data: &ESF) -> Result<String> {
262    as_record(map_data.root_node())
263        .and_then(|root| child_record(root, "REGIONS_DATA"))
264        .and_then(|regions| child_record(regions, "theatres"))
265        .and_then(|theatres| child_record(theatres, "CAMPAIGN_THEATRE"))
266        .and_then(|theatre| theatre.children().first())
267        .and_then(|group| leaves(group).find_map(as_str))
268        .map(str::to_owned)
269        .ok_or_else(|| RLibError::CampaignMapDataError("missing the key of REGIONS_DATA/theatres/CAMPAIGN_THEATRE".to_owned()))
270}
271
272/// Reads the nodes of the high-level pathfinder of a `hlp_data.esf`, by region index.
273///
274/// The pathfinder record is a flat list: the region count, then for each region its index, its node count and its
275/// nodes. Each node has its id, cell, a value, its edges (9 values each) and an edge-to-edge cost table.
276///
277/// # Errors
278///
279/// Returns an error if the file lacks the pathfinder or it ends early.
280pub fn pathfinder_nodes(hlp_data: &ESF) -> Result<HashMap<usize, Vec<PathfinderNode>>> {
281    let root = as_record(hlp_data.root_node()).ok_or_else(|| pathfinder_error("the root node is not a record"))?;
282    let pathfinder = child_record(root, "CAI_HIGH_LEVEL_PATHFINDER").ok_or_else(|| pathfinder_error("missing CAI_HIGH_LEVEL_PATHFINDER"))?;
283    let values = pathfinder.children().iter().flat_map(|group| leaves(group)).collect::<Vec<&NodeType>>();
284    let mut cursor = Cursor { values: &values, position: 0 };
285
286    let mut nodes = HashMap::new();
287    for _ in 0..cursor.next()? {
288        let region = cursor.next()? as usize;
289        let mut region_nodes = vec![];
290        for _ in 0..cursor.next()? {
291            let id = cursor.next()?;
292            let x = cursor.next()? as u16;
293            let y = cursor.next()? as u16;
294            cursor.next()?;
295            let edges = cursor.next()? as usize;
296
297            // Each edge has 9 values (cells, cost, target, index and two flags), then a cost per pair of edges.
298            cursor.position += edges * 9 + edges * edges.saturating_sub(1);
299            region_nodes.push(PathfinderNode { area: id >> NODE_REGION_BITS, x, y });
300        }
301        nodes.insert(region, region_nodes);
302    }
303    Ok(nodes)
304}
305
306impl Cursor<'_> {
307
308    /// Returns the next leaf as an unsigned integer.
309    fn next(&mut self) -> Result<u32> {
310        let value = self.values.get(self.position).and_then(|node| as_u32(node)).ok_or_else(|| pathfinder_error("the node list ends early"))?;
311        self.position += 1;
312        Ok(value)
313    }
314}
315
316/// Builds the error for a malformed pathfinder.
317fn pathfinder_error(message: &str) -> RLibError {
318    RLibError::CampaignMapDataError(format!("high-level pathfinder: {message}"))
319}