Skip to main content

rpfm_extensions/startpos/
region_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//! Region of each cell of the campaign map, from `map_data.esf`
12//! (`REGIONS_DATA/theatres/CAMPAIGN_THEATRE/HEX_MAP_DATA`).
13//!
14//! The data has 7 bytes per cell, row by row; the first byte is the index of the cell's region in
15//! `REGIONS_DATA/REGIONS_BLOCK` (land regions, then seas and rivers), and the last two are hex-edge masks (the
16//! startpos builds the road pieces of settlement sprawls from them). The meaning of the other bytes isn't known.
17//! Verified on Attila's main campaign: every garrison stands on a cell of its own region. Prefer it over the map's
18//! lookup image, which also paints seas the map data leaves empty.
19
20use std::collections::{HashMap, HashSet};
21
22use rpfm_lib::error::{RLibError, Result};
23use rpfm_lib::files::esf::{ESF, NodeType};
24
25use super::coords::{HexCell, MapScale};
26use super::esf_nav::{as_record, as_u32, child_record, leaves};
27use super::settlements::{region_data_records, region_key};
28
29#[cfg(test)] #[path = "region_map_test.rs"] pub(crate) mod region_map_test;
30
31/// Bytes stored per cell.
32const BYTES_PER_CELL: usize = 7;
33
34/// Position of the first hex-edge mask in a cell's bytes (the second one follows it).
35const EDGE_MASKS_OFFSET: usize = 5;
36
37/// Position of a cell's area byte: the index of the region's area the cell is in, shifted one bit left.
38const AREA_OFFSET: usize = 1;
39
40//---------------------------------------------------------------------------//
41//                              Types
42//---------------------------------------------------------------------------//
43
44/// Region of each cell of the campaign map.
45#[derive(Debug, Clone, PartialEq, Default)]
46pub struct RegionMap {
47
48    /// Region keys, in `REGIONS_BLOCK` order.
49    keys: Vec<String>,
50
51    /// Map width, in cells.
52    width: u32,
53
54    /// Map height, in cells.
55    height: u32,
56
57    /// Raw cell data, row by row.
58    cells: Vec<u8>,
59
60    /// Size of the map in world units.
61    world_size: (f32, f32),
62}
63
64//---------------------------------------------------------------------------//
65//                              Implementations
66//---------------------------------------------------------------------------//
67
68impl RegionMap {
69
70    /// Reads the region map of a campaign map.
71    ///
72    /// # Arguments
73    ///
74    /// * `map_data` - Decoded `map_data.esf` of the campaign map.
75    ///
76    /// # Errors
77    ///
78    /// Returns an error if the map data lacks the region list or the hex map, or the hex map is too short.
79    pub fn from_map_data(map_data: &ESF) -> Result<Self> {
80        let keys = region_data_records(map_data)?.into_iter()
81            .map(|region| region_key(region).map(str::to_owned))
82            .collect::<Result<Vec<_>>>()?;
83
84        let missing = || RLibError::CampaignMapDataError("missing REGIONS_DATA/theatres/CAMPAIGN_THEATRE/HEX_MAP_DATA".to_owned());
85        let theatre = as_record(map_data.root_node())
86            .and_then(|root| child_record(root, "REGIONS_DATA"))
87            .and_then(|regions| child_record(regions, "theatres"))
88            .and_then(|theatres| child_record(theatres, "CAMPAIGN_THEATRE"))
89            .ok_or_else(missing)?;
90        let hex_map = child_record(theatre, "HEX_MAP_DATA").ok_or_else(missing)?;
91
92        // The theatre's second position is its far corner: the size of the map in world units.
93        let world_size = theatre.children().first()
94            .and_then(|group| group.iter().filter_map(|node| match node { NodeType::Coord2d(corner) => Some((*corner.x(), *corner.y())), _ => None }).nth(1))
95            .ok_or_else(|| RLibError::CampaignMapDataError("missing the size of REGIONS_DATA/theatres/CAMPAIGN_THEATRE".to_owned()))?;
96
97        let group = hex_map.children().first().ok_or_else(missing)?;
98        let mut values = leaves(group);
99        let width = values.next().and_then(as_u32).ok_or_else(missing)?;
100        let height = values.next().and_then(as_u32).ok_or_else(missing)?;
101        let cells = match values.next() {
102            Some(NodeType::U8Array(cells)) => cells.to_owned(),
103            _ => return Err(missing()),
104        };
105
106        if cells.len() < width as usize * height as usize * BYTES_PER_CELL {
107            return Err(RLibError::CampaignMapDataError(format!("the hex map has {} bytes, expected {width}x{height} cells", cells.len())));
108        }
109
110        Ok(Self { keys, width, height, cells, world_size })
111    }
112
113    /// Returns the size of the map in world units.
114    pub fn world_size(&self) -> (f32, f32) {
115        self.world_size
116    }
117
118    /// Returns the scale between the map's world and raw units.
119    pub fn scale(&self) -> MapScale {
120        MapScale::new((self.width, self.height), self.world_size)
121    }
122
123    /// Returns the region keys of the map, in `REGIONS_BLOCK` order.
124    pub fn keys(&self) -> &[String] {
125        &self.keys
126    }
127
128    /// Returns the key of the region a cell belongs to.
129    ///
130    /// # Returns
131    ///
132    /// `None` for cells outside the map or with an unknown region index.
133    pub fn region_at(&self, cell: HexCell) -> Option<&str> {
134        let index = self.cell_offset(cell)?;
135        self.keys.get(usize::from(self.cells[index])).map(String::as_str)
136    }
137
138    /// Returns the index (in `REGIONS_BLOCK` order) of the region a cell belongs to, or `None` for cells outside the map.
139    pub fn region_index_at(&self, cell: HexCell) -> Option<usize> {
140        self.cell_offset(cell).map(|index| usize::from(self.cells[index]))
141    }
142
143    /// Returns the index of the area (in the region's `REGION_AREAS` order) a cell is in, or `None` for cells outside
144    /// the map.
145    pub fn area_at(&self, cell: HexCell) -> Option<usize> {
146        self.cell_offset(cell).map(|index| usize::from(self.cells[index + AREA_OFFSET] >> 1))
147    }
148
149    /// Returns the size of the map in cells.
150    pub fn size(&self) -> (u32, u32) {
151        (self.width, self.height)
152    }
153
154    /// Returns the two hex-edge masks of a cell (bit `n` stands for the `n`-th neighbour in the game's direction
155    /// order), or zeros for cells outside the map.
156    pub fn edge_masks(&self, cell: HexCell) -> [u8; 2] {
157        self.cell_offset(cell)
158            .map(|index| [self.cells[index + EDGE_MASKS_OFFSET], self.cells[index + EDGE_MASKS_OFFSET + 1]])
159            .unwrap_or_default()
160    }
161
162    /// Returns the cells of every region, by region key.
163    pub fn cells_by_region(&self) -> HashMap<&str, Vec<HexCell>> {
164        let mut cells = HashMap::<&str, Vec<HexCell>>::new();
165        for y in 0..self.height {
166            for x in 0..self.width {
167                let cell = HexCell::new(x as u16, y as u16);
168                if let Some(region) = self.region_at(cell) {
169                    cells.entry(region).or_default().push(cell);
170                }
171            }
172        }
173        cells
174    }
175
176    /// Returns every pair of different regions with neighbouring cells, each pair once with the keys in order.
177    pub fn neighbour_pairs(&self) -> HashSet<(&str, &str)> {
178        let mut pairs = HashSet::new();
179        for y in 0..self.height {
180            for x in 0..self.width {
181                let cell = HexCell::new(x as u16, y as u16);
182                let Some(region) = self.region_at(cell) else { continue };
183                for neighbour in cell.neighbours() {
184                    if let Some(other) = self.region_at(neighbour).filter(|other| *other != region) {
185                        pairs.insert(if region < other { (region, other) } else { (other, region) });
186                    }
187                }
188            }
189        }
190        pairs
191    }
192
193    /// Returns the position of a cell's first byte, or `None` for cells outside the map.
194    fn cell_offset(&self, cell: HexCell) -> Option<usize> {
195        if u32::from(cell.x()) >= self.width || u32::from(cell.y()) >= self.height {
196            return None;
197        }
198        Some((u32::from(cell.y()) * self.width + u32::from(cell.x())) as usize * BYTES_PER_CELL)
199    }
200}