Skip to main content

banished_game/world/
placement.rs

1//! Building placement (`PlacementTool`): the footprint under the cursor, whether it may be built
2//! and the map stamp of a footprint string. See docs/BUILD-TOOLS.md.
3//!
4//! Original: `FUN_140102ed0`, `FUN_1400a2a50`, `FUN_1400a4aa0`, `FUN_1400a30f0`.
5// vtable 0x140168130
6
7use crate::nature::seeding::Cells;
8use crate::terrain::{CellType, HeightGrid};
9
10// DAT_140178904
11/// Degrees per rotation step.
12pub const ROTATION_STEP: f32 = 90.0;
13// DAT_140178948
14/// Height tolerance placement passes (effectively unlimited).
15pub const HEIGHT_TOLERANCE: f32 = 1000.0;
16
17/// A cell rectangle, inclusive.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
19pub struct Rect {
20    /// The first column.
21    pub x0: i32,
22    /// The first row.
23    pub y0: i32,
24    /// The last column.
25    pub x1: i32,
26    /// The last row.
27    pub y1: i32,
28}
29
30impl Rect {
31    /// The width in cells.
32    pub fn width(&self) -> i32 {
33        self.x1 - self.x0 + 1
34    }
35    /// The height in cells.
36    pub fn height(&self) -> i32 {
37        self.y1 - self.y0 + 1
38    }
39}
40
41/// The footprint of a `width × height` building centred on `cell` (even sizes extend one more cell
42/// towards +x/+y), turned by `rotation` steps (1 and 3 swap the sides). `None` when it leaves the
43/// map (the left column must also be at least 1).
44///
45/// Original: `FUN_140102ed0`.
46pub fn footprint(
47    map_size: i32,
48    cell: (i32, i32),
49    width: i32,
50    height: i32,
51    rotation: i32,
52) -> Option<Rect> {
53    let (w, h) = if rotation == 1 || rotation == 3 {
54        (height, width)
55    } else {
56        (width, height)
57    };
58    let span = |c: i32, s: i32| {
59        if s & 1 == 0 {
60            (c - (s >> 1) + 1, (s >> 1) + c)
61        } else {
62            let half = (s - 1) >> 1;
63            (c - half, half + c)
64        }
65    };
66    let (x0, x1) = span(cell.0, w);
67    let (y0, y1) = span(cell.1, h);
68    let n = map_size;
69    if x0 < 0 || n <= x0 || y0 < 0 || n <= y0 || x0 < 1 || n <= x1 || y1 < 0 || n <= y1 {
70        return None;
71    }
72    Some(Rect { x0, y0, x1, y1 })
73}
74
75/// Index into a footprint string (rows separated by one character, last row first) of cell `(i, j)`
76/// of a `w × h` rectangle placed with `degrees` rotation (`w`, `h` are the rotated sides).
77///
78/// Original: `FUN_1400a2a50`, `FUN_1400a30f0`.
79pub fn footprint_index(i: i32, j: i32, w: i32, h: i32, degrees: i32) -> usize {
80    (match degrees {
81        90 => i * (h + 1) + j,
82        180 => j * (w + 1) + (w - 1 - i),
83        270 => (w - 1 - i) * (h + 1) + (h - 1 - j),
84        _ => (h - 1 - j) * (w + 1) + i,
85    }) as usize
86}
87
88/// The `CreatePlacedDescription` and `MapDescription` values placement reads.
89#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
90pub struct PlacementRules {
91    // +0x20
92    /// Allowed cell types (bit per type) when no per-character table applies.
93    pub mask: u32,
94    // +0x30 / +0x28
95    /// Per-character masks: `'0' + k` selects `masks[k]`.
96    pub masks: Vec<u32>,
97    /// The string that chooses a mask for each cell.
98    pub mask_chars: String,
99    // MapDescription +0x28
100    /// `MapDescription`: `#` own type, `_` ground, `.` untouched.
101    pub footprint: String,
102    // +0x44; +0x48
103    /// Not identified yet.
104    pub width: i32,
105    /// Not identified yet.
106    pub height: i32,
107    // MapDescription +0x20
108    /// `MapDescription`.
109    pub cell_type: u8,
110}
111
112// owner_ok(occupant) stands for the occupant's template (+0x28) being the owner template.
113/// Whether a cell admits a building: its type is in `mask`, the ground is flat enough and level
114/// enough, and its occupant (if any) may be replaced.
115///
116/// Original: `FUN_1400a4aa0(map, x, y, mask, min_normal, tolerance, reference, override_occupant,
117/// override_flag, owner)`.
118#[allow(clippy::too_many_arguments)]
119pub fn cell_admits(
120    cells: &Cells,
121    terrain: &HeightGrid,
122    x: i32,
123    y: i32,
124    mask: u32,
125    min_normal: f32,
126    tolerance: f32,
127    reference: f32,
128    override_occupant: bool,
129    override_flag: bool,
130    owner_ok: impl Fn(usize) -> bool,
131) -> bool {
132    let n = cells.size as i32;
133    if x < 0 || n <= x || y < 0 || n <= y {
134        return false;
135    }
136    let i = (n * y + x) as usize;
137    if !mask & (1u32 << (cells.types[i].0 & 0x1f)) != 0 {
138        return false;
139    }
140    let occupant = cells.occupants[i];
141    if !(occupant.is_none_or(&owner_ok) || override_occupant) {
142        return false;
143    }
144    if !(override_flag || cells.flags[i] == 0) {
145        return false;
146    }
147    let half = (n >> 1) as f32 - 0.5;
148    let (px, py) = ((i as i32 % n) as f32 - half, (i as i32 / n) as f32 - half);
149    let mut normal = [0.0f32; 3];
150    let h = cells
151        .floors
152        .ground_normal(cells.size, terrain, px, py, &mut normal);
153    min_normal <= normal[2] && (h - reference).abs() <= tolerance
154}
155
156/// Whether a building can be placed: every cell of its footprint admits it. This is the test the
157/// placement tool makes.
158///
159/// Original: `FUN_1400a2a50`.
160pub fn can_place(
161    cells: &Cells,
162    terrain: &HeightGrid,
163    rules: &PlacementRules,
164    rect: Rect,
165    degrees: i32,
166    owner_ok: impl Fn(usize) -> bool,
167) -> bool {
168    (rect.y0..=rect.y1).all(|y| {
169        (rect.x0..=rect.x1)
170            .all(|x| footprint_cell(cells, terrain, rules, rect, degrees, x, y, &owner_ok).0)
171    })
172}
173
174// The mask is the rules' entry (+0x30) for the character, else +0x28 / +0x20.
175/// One cell of a footprint, as the placement test and the overlay test it: whether it admits the
176/// building (with the cell's mask), and whether its footprint character is `.` (the cell is not
177/// changed, and its zone count is not checked).
178///
179/// Original: `FUN_140106850`; the cell test is `FUN_1400a4aa0`, the placement test
180/// `FUN_1400a2a50`.
181#[allow(clippy::too_many_arguments)]
182pub fn footprint_cell(
183    cells: &Cells,
184    terrain: &HeightGrid,
185    rules: &PlacementRules,
186    rect: Rect,
187    degrees: i32,
188    x: i32,
189    y: i32,
190    owner_ok: impl Fn(usize) -> bool,
191) -> (bool, bool) {
192    let (w, h) = (rect.width(), rect.height());
193    let k = footprint_index(x - rect.x0, y - rect.y0, w, h, degrees);
194    let untouched = rules.footprint.as_bytes().get(k) == Some(&b'.');
195    let mut mask = rules.mask;
196    if !rules.masks.is_empty() {
197        if let Some(&c) = rules.mask_chars.as_bytes().get(k) {
198            if let Some(&m) = rules.masks.get((c as i32 - 0x30) as usize) {
199                mask = m;
200            }
201        }
202    }
203    let admits = cell_admits(
204        cells,
205        terrain,
206        x,
207        y,
208        mask,
209        0.0,
210        HEIGHT_TOLERANCE,
211        0.0,
212        false,
213        untouched,
214        owner_ok,
215    );
216    (admits, untouched)
217}
218
219// +0x08
220/// Stamp a footprint. `#` and an empty footprint set the building's type, `_` sets ground (2);
221/// every cell except `.` records the entity.
222///
223/// Original: `FUN_1400a30f0`.
224pub fn stamp(cells: &mut Cells, rules: &PlacementRules, rect: Rect, degrees: i32, entity: usize) {
225    let n = cells.size as i32;
226    let (w, h) = (rect.width(), rect.height());
227    let fp = rules.footprint.as_bytes();
228    for y in rect.y0..=rect.y1 {
229        for x in rect.x0..=rect.x1 {
230            let i = (n * y + x) as usize;
231            let (set, owned, ty) = if fp.is_empty() {
232                (true, true, rules.cell_type)
233            } else {
234                let c = fp
235                    .get(footprint_index(x - rect.x0, y - rect.y0, w, h, degrees))
236                    .copied()
237                    .unwrap_or(b'.');
238                (
239                    c == b'#' || c == b'_',
240                    c != b'.',
241                    if c == b'_' { 2 } else { rules.cell_type },
242                )
243            };
244            if set {
245                cells.set_type(i, CellType(ty));
246            }
247            cells.objects[i] = owned.then_some(entity);
248        }
249    }
250}
251
252// The occupant is cell +0x00, the zone count +0x28.
253/// Activate a zone: the entity becomes the occupant of every footprint cell that is not `.`, and
254/// each `.` cell counts one more zone. Not ported: the path grid's blocked bit (citizens' mode 0
255/// does not honour it).
256///
257/// Original: `FUN_1401356c0`; the zone component's activation `FUN_1400a2830`.
258pub fn zone(cells: &mut Cells, rules: &PlacementRules, rect: Rect, degrees: i32, entity: usize) {
259    let n = cells.size as i32;
260    let (w, h) = (rect.width(), rect.height());
261    let fp = rules.footprint.as_bytes();
262    for y in rect.y0..=rect.y1 {
263        for x in rect.x0..=rect.x1 {
264            let i = (n * y + x) as usize;
265            let untouched = !fp.is_empty()
266                && fp
267                    .get(footprint_index(x - rect.x0, y - rect.y0, w, h, degrees))
268                    .copied()
269                    .unwrap_or(b'.')
270                    == b'.';
271            if untouched {
272                cells.occupants[i] = None;
273                cells.flags[i] = cells.flags[i].wrapping_add(1);
274            } else {
275                cells.occupants[i] = Some(entity);
276            }
277        }
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn footprints_centre_on_the_cursor() {
287        // Barn: 8 × 5.
288        let r = footprint(256, (100, 100), 8, 5, 0).unwrap();
289        assert_eq!((r.x0, r.x1, r.y0, r.y1), (97, 104, 98, 102));
290        let r = footprint(256, (100, 100), 8, 5, 1).unwrap();
291        assert_eq!((r.width(), r.height()), (5, 8));
292        assert!(footprint(256, (0, 100), 3, 3, 0).is_none());
293    }
294
295    #[test]
296    fn rotated_indices_cover_the_string() {
297        // "ab cd" is 2 wide, 2 high (rows separated by one char, last row first).
298        let (w, h) = (2, 2);
299        let mut seen: Vec<usize> = Vec::new();
300        for deg in [0, 90, 180, 270] {
301            seen.clear();
302            for j in 0..h {
303                for i in 0..w {
304                    seen.push(footprint_index(i, j, w, h, deg));
305                }
306            }
307            seen.sort();
308            assert_eq!(seen, vec![0, 1, 3, 4], "{deg}");
309        }
310    }
311}