Skip to main content

rpfm_extensions/startpos/
coords.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//! Coordinate systems of the campaign map.
12//!
13//! The campaign map uses three related coordinate systems:
14//!
15//! - **World**: the `f32` positions stored in startpos `Coord2d` nodes.
16//! - **Raw**: the `u16` units stored in the map files (`trade_routes.ptd`). `raw = world * scale + 4`.
17//! - **Cell**: the logical hex grid of the map (`HEX_MAP_DATA`, 1016x720 cells in Attila's main map). A cell is
18//!   6 raw units wide and 8 raw units high, and odd columns are shifted half a cell down.
19//!
20//! The scales of a map ([`MapScale`]) come from its hex grid and its size in world units: the last column starts
21//! at the map's right edge, and the last row of odd columns ends at its bottom edge. Characters placed on cells land
22//! exactly where the current game build (the one BOB uses) puts them: 518 of 518 in Attila's main campaign. CA's
23//! older build of the shipped startposes rounds the horizontal position differently, one ulp away at most.
24
25use getset::CopyGetters;
26use serde_derive::{Deserialize, Serialize};
27
28#[cfg(test)] #[path = "coords_test.rs"] pub(crate) mod coords_test;
29
30/// Offset added to raw coordinates.
31pub const RAW_OFFSET: f32 = 4.0;
32
33/// Raw units per cell, horizontally.
34pub const CELL_RAW_WIDTH: f32 = 6.0;
35
36/// Raw units per cell, vertically.
37pub const CELL_RAW_HEIGHT: f32 = 8.0;
38
39/// Neighbour offsets of a cell in an odd column, in the order the game uses (south first, then clockwise).
40const ODD_COLUMN_NEIGHBOURS: [(i32, i32); 6] = [(0, 1), (1, 1), (1, 0), (0, -1), (-1, 0), (-1, 1)];
41
42/// Neighbour offsets of a cell in an even column, in the order the game uses (south first, then clockwise).
43const EVEN_COLUMN_NEIGHBOURS: [(i32, i32); 6] = [(0, 1), (1, 0), (1, -1), (0, -1), (-1, -1), (-1, 0)];
44
45/// Edges of a hex cell.
46pub const HEX_EDGES: u8 = 6;
47
48/// Edge mask with every edge set.
49const ALL_EDGES: u8 = 0b11_1111;
50
51/// Edge mask of the first two edges.
52const FIRST_TWO_EDGES: u8 = 0b11;
53
54//---------------------------------------------------------------------------//
55//                              Types
56//---------------------------------------------------------------------------//
57
58/// A cell of the logical hex grid of the campaign map.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default, CopyGetters, Serialize, Deserialize)]
60#[getset(get_copy = "pub")]
61pub struct HexCell {
62
63    /// Column.
64    x: u16,
65
66    /// Row.
67    y: u16,
68}
69
70/// World units per raw unit of a campaign map, horizontally and vertically.
71#[derive(Debug, Clone, Copy, PartialEq, CopyGetters)]
72#[getset(get_copy = "pub")]
73pub struct MapScale {
74
75    /// World units per raw unit, horizontally.
76    x: f32,
77
78    /// World units per raw unit, vertically.
79    y: f32,
80
81    /// World units per cell, horizontally.
82    cell_width: f32,
83}
84
85/// A piece covering some edges of a hex cell (a road or a coast), as a shape and a rotation.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, CopyGetters)]
87#[getset(get_copy = "pub")]
88pub struct EdgePiece {
89
90    /// Edge mask of the piece before rotating it.
91    shape: u8,
92
93    /// Edges the shape is rotated by, in the game's direction order.
94    rotation: u8,
95}
96
97//---------------------------------------------------------------------------//
98//                              Implementations
99//---------------------------------------------------------------------------//
100
101impl HexCell {
102
103    /// Creates a cell from its column and row.
104    pub fn new(x: u16, y: u16) -> Self {
105        Self { x, y }
106    }
107
108    /// Returns the existing neighbours of the cell, in the game's direction order.
109    ///
110    /// # Returns
111    ///
112    /// Up to six cells; neighbours that would fall outside the `u16` range are skipped.
113    pub fn neighbours(&self) -> impl Iterator<Item = HexCell> + '_ {
114        (0..HEX_EDGES).filter_map(|direction| self.neighbour(direction))
115    }
116
117    /// Returns the neighbour in a direction (0 to 5, in the game's direction order).
118    ///
119    /// # Returns
120    ///
121    /// `None` if the neighbour would fall outside the `u16` range, or the direction is over 5.
122    pub fn neighbour(&self, direction: u8) -> Option<HexCell> {
123        let offsets = if self.x % 2 == 1 { &ODD_COLUMN_NEIGHBOURS } else { &EVEN_COLUMN_NEIGHBOURS };
124        let (dx, dy) = offsets.get(usize::from(direction))?;
125        let x = u16::try_from(i32::from(self.x) + dx).ok()?;
126        let y = u16::try_from(i32::from(self.y) + dy).ok()?;
127        Some(HexCell::new(x, y))
128    }
129
130    /// Returns the edge mask of the neighbours matching a predicate: bit `n` stands for the `n`-th neighbour in the
131    /// game's direction order. Neighbours outside the `u16` range never match.
132    pub fn neighbour_mask(&self, mut predicate: impl FnMut(HexCell) -> bool) -> u8 {
133        (0..HEX_EDGES)
134            .filter(|&direction| self.neighbour(direction).is_some_and(&mut predicate))
135            .fold(0, |mask, direction| mask | 1 << direction)
136    }
137
138    /// Returns the cell followed by its neighbours, in the order the game stores slot footprints.
139    pub fn footprint(&self) -> Vec<HexCell> {
140        std::iter::once(*self).chain(self.neighbours()).collect()
141    }
142}
143
144impl MapScale {
145
146    /// Returns the scale of a map.
147    ///
148    /// # Arguments
149    ///
150    /// * `cells` - Columns and rows of the map's hex grid.
151    /// * `world_size` - Size of the map in world units.
152    pub fn new(cells: (u32, u32), world_size: (f32, f32)) -> Self {
153        let width = f64::from(cells.0.saturating_sub(1)) * f64::from(CELL_RAW_WIDTH);
154        let height = f64::from(cells.1) * f64::from(CELL_RAW_HEIGHT) + f64::from(CELL_RAW_HEIGHT / 2.0);
155        let raw_per_world = (width / f64::from(world_size.0)) as f32;
156        Self {
157            x: (f64::from(world_size.0) / width) as f32,
158            y: (f64::from(world_size.1) / height) as f32,
159            cell_width: CELL_RAW_WIDTH / raw_per_world,
160        }
161    }
162
163    /// Returns the cell containing a world position, as the game finds it.
164    ///
165    /// Odd columns are shifted half a cell down, so their rows start half a cell height later. Cells are hexagons
166    /// wider than their column: the first third of a column holds the slanted edges shared with the previous column,
167    /// where positions above or below the edge belong to the previous column's cells.
168    pub fn cell_at(&self, x: f32, y: f32) -> HexCell {
169        let (raw_x, raw_y) = self.world_to_raw(x, y);
170        let columns = raw_x / CELL_RAW_WIDTH;
171        let mut column = columns.floor() as i32;
172        let row_offset = if column % 2 == 1 { CELL_RAW_HEIGHT / 2.0 } else { 0.0 };
173        let rows = (raw_y - row_offset) / CELL_RAW_HEIGHT;
174        let mut row = rows.floor() as i32;
175
176        // Distance into the column in thirds, and height from the middle of the row in rows.
177        let into_column = (columns - column as f32) * 3.0;
178        let from_middle = rows - row as f32 - 0.5;
179        if into_column < 1.0 {
180            let odd = column & 1;
181            if into_column <= from_middle * 2.0 {
182                (column, row) = (column - 1, row + odd);
183            } else if into_column <= from_middle * -2.0 {
184                (column, row) = (column - 1, row + odd - 1);
185            }
186        }
187        HexCell::new(column.max(0) as u16, row.max(0) as u16)
188    }
189
190    /// Returns the world position of a cell's top-left corner, where the game places characters given by cell.
191    ///
192    /// The game multiplies in double precision by single precision scales, and adds the half-cell shift of odd
193    /// columns as a separate single precision term, so the position is computed the same way.
194    pub fn world_position(&self, cell: HexCell) -> (f32, f32) {
195        let x = (f64::from(cell.x) * f64::from(self.cell_width)) as f32;
196        let y = (f64::from(cell.y) * f64::from(CELL_RAW_HEIGHT) * f64::from(self.y)) as f32;
197        let row_offset = if cell.x % 2 == 1 { (f64::from(CELL_RAW_HEIGHT / 2.0) * f64::from(self.y)) as f32 } else { 0.0 };
198        (x, y + row_offset)
199    }
200
201    /// Converts a world position to raw map units.
202    pub fn world_to_raw(&self, x: f32, y: f32) -> (f32, f32) {
203        (x / self.x + RAW_OFFSET, y / self.y + RAW_OFFSET)
204    }
205
206    /// Converts raw map units to a world position.
207    pub fn raw_to_world(&self, x: f32, y: f32) -> (f32, f32) {
208        ((x - RAW_OFFSET) * self.x, (y - RAW_OFFSET) * self.y)
209    }
210
211    /// Converts a raw offset (a vector, no translation) to world units.
212    ///
213    /// Map files store offsets at 100 times the raw scale.
214    pub fn raw_offset_to_world(&self, x: i16, y: i16) -> (f32, f32) {
215        (f32::from(x) * self.x / 100.0, f32::from(y) * self.y / 100.0)
216    }
217}
218
219impl EdgePiece {
220
221    /// Splits an edge mask into a piece shape and a rotation.
222    ///
223    /// The shape is the smallest rotation of the mask with its first two edges set, or the smallest rotation if
224    /// none has them (so a straight road is `0b1001` and a bend is `0b11`). The rotation is the smallest one turning
225    /// the shape into the mask.
226    pub fn from_mask(mask: u8) -> Self {
227        let mask = mask & ALL_EDGES;
228        let rotations = (0..HEX_EDGES).map(|rotation| rotate_edges(mask, rotation));
229        let shape = rotations.clone().filter(|rotated| rotated & FIRST_TWO_EDGES == FIRST_TWO_EDGES).min()
230            .or_else(|| rotations.min())
231            .unwrap_or_default();
232        let rotation = (0..HEX_EDGES).find(|&rotation| rotate_edges(shape, rotation) == mask).unwrap_or_default();
233        Self { shape, rotation }
234    }
235}
236
237/// Rotates an edge mask by some edges, in the game's direction order.
238fn rotate_edges(mask: u8, edges: u8) -> u8 {
239    ((mask << edges) | (mask >> ((HEX_EDGES - edges) % HEX_EDGES))) & ALL_EDGES
240}
241