Skip to main content

banished_game/world/
terraform.rs

1//! Terraforming: a tunnel reshapes the terrain when it is built. See docs/TUNNELS.md.
2//!
3//! When the build is done (entity event 4; a multi-object passes it to its parts), the vertices
4//! of the ceiling mesh raise the terrain where it is below them, keeping what was there, and the
5//! vertices of the floor mesh lower it where it is above them. Then the rough cells (10) under
6//! the building become high ground (6). When the tunnel is removed, the raised heights are put
7//! back and those cells become rough again; the dug floor stays.
8//!
9//! Original: `TerraformComponent` (vtable `0x1401609a0`): `FUN_140057c70` (built, slot `+0x60`),
10//! `FUN_1400574b0` (ceiling and floor), `FUN_140057a80` (cell types), `FUN_140057cb0` (removed,
11//! slot `+0x38`).
12//!
13//! Deviation: the heights belong to the terrain outside the simulation, so the town queues the
14//! dig and the game applies it after the iteration (`StartingTown::apply_terraforms`), not
15//! during it.
16
17use std::sync::Arc;
18
19use crate::nature::seeding::Cells;
20use crate::terrain::{CellType, HeightGrid};
21
22/// The description's meshes as the first submesh's vertices (`PickingMesh::Vertices(0)`).
23#[derive(Debug, Clone, Default)]
24pub struct TerraformTemplate {
25    /// The floor mesh, if any.
26    pub floor: Option<Arc<Vec<[f32; 3]>>>,
27    /// The cell types the floor mesh may lower.
28    pub floor_mask: u32,
29    /// The ceiling mesh, if any.
30    pub ceiling: Option<Arc<Vec<[f32; 3]>>>,
31    /// The cell types the ceiling mesh may raise.
32    pub ceiling_mask: u32,
33}
34
35// +0x28; +0x38
36/// The component's raised points (where the ground was) and cells turned high.
37#[derive(Debug, Clone, Default, PartialEq)]
38pub struct TerraformState {
39    /// The raised points, with the ground height they had.
40    pub raised: Vec<[f32; 3]>,
41    /// The cells turned high.
42    pub cells: Vec<i32>,
43}
44
45/// A request for the terrain owner: dig (event 4) or undo (removal) an entity's terraform.
46#[derive(Debug, Clone, Copy, PartialEq)]
47pub enum Terraform {
48    /// Dig the entity's terraform.
49    Dig(usize),
50    /// Undo the entity's terraform.
51    Undo(usize),
52    /// A quarry's dig to `floor` (`crate::production::quarry`), claiming its steep cells with `claim`.
53    Quarry {
54        /// The quarry.
55        entity: usize,
56        /// The height to dig to.
57        floor: f32,
58        /// Whether it claims its steep cells.
59        claim: bool,
60    },
61}
62
63/// `FUN_140178930`: heights are stored × 256.
64const HEIGHT_SCALE: f32 = 256.0;
65
66fn cell_type(cells: &Cells, x: f32, y: f32) -> u8 {
67    let n = cells.size as i32;
68    let half = (n >> 1) as f32;
69    let cx = ((x + half) as i32).clamp(0, n - 1);
70    let cy = ((y + half) as i32).clamp(0, n - 1);
71    cells.types[(cy * n + cx) as usize].0
72}
73
74/// Move the ground to a mesh: each vertex goes through the model's transform (yaw, position) and
75/// a quarter cell further. Where the cells at it, one to the west, one to the south-west and
76/// (again) one to the west are all of the mask's types, and the vertex is above (`ceiling`) or
77/// below (floor) the ground, the height sample under it takes the vertex's height. A ceiling keeps
78/// the ground it raised.
79///
80/// Original: `FUN_1400574b0(ceiling)`.
81#[allow(clippy::too_many_arguments)]
82pub fn reshape(
83    cells: &Cells,
84    floors: &crate::world::floor::Floors,
85    heights: &mut [i16],
86    width: usize,
87    vertices: &[[f32; 3]],
88    mask: u32,
89    yaw: f32,
90    position: [f32; 3],
91    ceiling: bool,
92    raised: &mut Vec<[f32; 3]>,
93) -> bool {
94    let model = crate::math::Transform::of_model(yaw, position);
95    let mut changed = false;
96    for &v in vertices {
97        // The entity transform (`+0x148`) times the vertex, a quarter cell on.
98        let t = model.apply(v);
99        let q = [t[0] + 0.25, t[1] + 0.25, t[2] + 0.0];
100        let ground = {
101            let grid = HeightGrid { width, heights };
102            floors.ground(cells.size, &grid, q[0], q[1])
103        };
104        let types = [
105            cell_type(cells, q[0], q[1]),
106            cell_type(cells, q[0] - 1.0, q[1]),
107            cell_type(cells, q[0] - 1.0, q[1] - 1.0),
108            cell_type(cells, q[0] - 1.0, q[1]),
109        ];
110        let bits = types.iter().fold(0u32, |b, &t| b | 1 << (t & 0x1f));
111        if !mask & bits != 0 {
112            continue;
113        }
114        let moves = if ceiling {
115            ground < q[2]
116        } else {
117            q[2] < ground
118        };
119        if !moves {
120            continue;
121        }
122        let half = (width as i32 >> 1) as f32;
123        let (hx, hy) = ((q[0] + half) as i32, (q[1] + half) as i32);
124        if hx < 0 || hy < 0 || width as i32 <= hx || width as i32 <= hy {
125            continue;
126        }
127        heights[hy as usize * width + hx as usize] = (q[2] * HEIGHT_SCALE) as i32 as i16;
128        changed = true;
129        if ceiling {
130            raised.push([q[0], q[1], ground]);
131        }
132    }
133    changed
134}
135
136/// Rough cells (10) of the rectangle become high ground (6), kept.
137///
138/// Original: `FUN_140057a80`.
139pub fn harden(cells: &mut Cells, rect: (i32, i32, i32, i32), kept: &mut Vec<i32>) {
140    let n = cells.size as i32;
141    let (x0, y0, w, h) = rect;
142    for y in y0..y0 + h {
143        for x in x0..x0 + w {
144            let i = n * y + x;
145            if cells.types[i as usize] == CellType::ROUGH {
146                cells.set_type(i as usize, CellType::HIGH);
147                kept.push(i);
148            }
149        }
150    }
151}
152
153/// The raised samples get their ground back and the kept cells become rough.
154///
155/// Original: `FUN_140057cb0`.
156pub fn undo(cells: &mut Cells, heights: &mut [i16], width: usize, state: &TerraformState) -> bool {
157    let half = (width as i32 >> 1) as f32;
158    for p in &state.raised {
159        let (hx, hy) = ((p[0] + half) as i32, (p[1] + half) as i32);
160        if 0 <= hx && 0 <= hy && hx < width as i32 && hy < width as i32 {
161            heights[hy as usize * width + hx as usize] = (p[2] * HEIGHT_SCALE) as i32 as i16;
162        }
163    }
164    for &i in &state.cells {
165        cells.set_type(i as usize, CellType::ROUGH);
166    }
167    !state.raised.is_empty()
168}
169
170/// Each cell of the inclusive rectangle `x0, y0, x1, y1` sets its low corner's height to 0 (the
171/// ground point of the cell's middle, in the height grid). `cells` is the map's cells per side; the
172/// height grid is `width − 1` cells, the map in its middle.
173///
174/// Original: `FUN_1400a35b0`.
175pub fn flatten(heights: &mut [i16], width: usize, cells: usize, rect: [i32; 4]) -> bool {
176    let off = ((width as i32 - 1) - cells as i32) >> 1;
177    let [x0, y0, x1, y1] = rect;
178    let mut changed = false;
179    for y in y0..=y1 {
180        for x in x0..=x1 {
181            let (gx, gy) = (x + off, y + off);
182            if gx < 0 || gy < 0 || width as i32 <= gx || width as i32 <= gy {
183                continue;
184            }
185            let h = &mut heights[gy as usize * width + gx as usize];
186            changed |= *h != 0;
187            *h = 0;
188        }
189    }
190    changed
191}
192
193#[cfg(test)]
194mod tests {
195    use super::flatten;
196
197    #[test]
198    fn flattening_zeroes_the_cells_low_corners() {
199        // 4 map cells in a grid of 8 cells (9 points a side): the map starts at point 2.
200        let width = 9;
201        let mut heights = vec![100i16; width * width];
202        assert!(flatten(&mut heights, width, 4, [1, 1, 2, 2]));
203        for y in 0..width {
204            for x in 0..width {
205                let zero = (3..=4).contains(&x) && (3..=4).contains(&y);
206                assert_eq!(heights[y * width + x] == 0, zero, "({x}, {y})");
207            }
208        }
209        assert!(!flatten(&mut heights, width, 4, [1, 1, 2, 2]));
210    }
211}