Skip to main content

banished_terrain/
query.rs

1//! Gameplay queries on the saved height grid: the vertical ray cast that gives the ground height
2//! and normal anywhere on the map, and the natural cell type derived from it.
3//!
4//! Positions are map-centred: cell `(i, j)` covers `[i - size/2, i - size/2 + 1)` and likewise for
5//! `y`. Float operations follow the disassembly so results are bit-exact.
6//!
7//! Original: `FUN_1400b6460`, `FUN_1400afef0`, `FUN_1400afce0`, `FUN_1400a3790`.
8
9use super::mesh::HeightGrid;
10
11type Vec3 = [f32; 3];
12
13fn sub(a: Vec3, b: Vec3) -> Vec3 {
14    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
15}
16
17/// `Math::Vector3::Cross`.
18fn cross(a: Vec3, b: Vec3) -> Vec3 {
19    [
20        a[1] * b[2] - a[2] * b[1],
21        b[0] * a[2] - a[0] * b[2],
22        a[0] * b[1] - b[0] * a[1],
23    ]
24}
25
26fn dot(a: Vec3, b: Vec3) -> f32 {
27    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
28}
29
30/// `Math::Vector3::Normalize` (by value): `v * (1 / |v|)`, unchanged when zero.
31fn normalize(v: Vec3) -> Vec3 {
32    let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
33    if len == 0.0 {
34        return v;
35    }
36    let inv = 1.0 / len;
37    [v[0] * inv, v[1] * inv, v[2] * inv]
38}
39
40const EPSILON: f32 = -1e-4;
41const RAY_TOP: f32 = 100.0;
42const RAY_LENGTH: f32 = 200.0;
43
44/// `FUN_1400afce0`: ray/triangle test. `t` is written once the barycentric tests pass, even when
45/// it is then rejected by the range test, as in the original.
46#[allow(clippy::too_many_arguments, clippy::neg_cmp_op_on_partial_ord)]
47fn ray_triangle(
48    origin: Vec3,
49    dir: Vec3,
50    tmax: f32,
51    v0: Vec3,
52    v1: Vec3,
53    v2: Vec3,
54    t: &mut f32,
55    normal: &mut Vec3,
56) -> bool {
57    let e1 = sub(v1, v0);
58    let e2 = sub(v2, v0);
59    let p = cross(dir, e2);
60    let det = dot(e1, p);
61    let s = sub(origin, v0);
62    let q = cross(s, e1);
63    let u = dot(s, p);
64    let v = dot(dir, q);
65    // comiss branches: each rejection is taken when the comparison holds (NaN passes through).
66    if !(det > 0.0) || EPSILON > u || u > det || EPSILON > v || v + u > det {
67        return false;
68    }
69    let hit = 1.0 / det * dot(e2, q);
70    *t = hit;
71    if hit < 0.0 || tmax < hit {
72        return false;
73    }
74    *normal = normalize(cross(e1, e2));
75    true
76}
77
78impl HeightGrid<'_> {
79    /// Where a ray first hits the ground, no farther than `maximum`: the distance and the point.
80    /// The game uses it to stop picking at the ground.
81    ///
82    /// Deviation: the port tests every cell in the rectangle the ray crosses, where the original
83    /// walks the cells along the ray.
84    ///
85    /// Original: `FUN_1400afef0`; its walk along the ray is `FUN_1400afa00`.
86    pub fn intersect(&self, origin: Vec3, direction: Vec3, maximum: f32) -> Option<(f32, Vec3)> {
87        let n = self.width as i32 - 1;
88        let half = n >> 1;
89        let end = [
90            origin[0] + direction[0] * maximum,
91            origin[1] + direction[1] * maximum,
92        ];
93        let lo = std::array::from_fn::<_, 2, _>(|k| {
94            ((origin[k].min(end[k]) + half as f32).floor() as i32 - 1).clamp(0, n - 1)
95        });
96        let hi = std::array::from_fn::<_, 2, _>(|k| {
97            ((origin[k].max(end[k]) + half as f32).floor() as i32 + 1).clamp(0, n - 1)
98        });
99        let mut limit = maximum;
100        let mut found = false;
101        let mut normal = [0.; 3];
102        for y in lo[1]..=hi[1] {
103            for x in lo[0]..=hi[0] {
104                let [a, b, c, d] = [
105                    self.corner(x, y, half),
106                    self.corner(x + 1, y, half),
107                    self.corner(x + 1, y + 1, half),
108                    self.corner(x, y + 1, half),
109                ];
110                let triangles = if (x + y) & 1 == 0 {
111                    [[a, b, d], [b, c, d]]
112                } else {
113                    [[a, b, c], [a, c, d]]
114                };
115                for [a, b, c] in triangles {
116                    let mut t = limit;
117                    if ray_triangle(origin, direction, limit, a, b, c, &mut t, &mut normal) {
118                        limit = t;
119                        found = true;
120                    }
121                }
122            }
123        }
124        found.then(|| {
125            (
126                limit,
127                [
128                    origin[0] + direction[0] * limit,
129                    origin[1] + direction[1] * limit,
130                    origin[2] + direction[2] * limit,
131                ],
132            )
133        })
134    }
135
136    fn corner(&self, x: i32, y: i32, half: i32) -> Vec3 {
137        let hi = self.width as i32 - 1;
138        let h = self.heights[y.clamp(0, hi) as usize * self.width + x.clamp(0, hi) as usize];
139        [
140            (x - half) as f32,
141            (y - half) as f32,
142            h as i32 as f32 * 0.00390625,
143        ]
144    }
145
146    /// `FUN_1400afef0`: cast a ray against the two triangles of cell `(x, y)`. Returns whether
147    /// either triangle was hit and the last `t` written; `normal` takes the last hit's normal.
148    fn raycast_cell(
149        &self,
150        x: i32,
151        y: i32,
152        dir: Vec3,
153        origin: Vec3,
154        tmax: f32,
155        normal: &mut Vec3,
156    ) -> (bool, f32) {
157        let half = (self.width as i32 - 1) >> 1;
158        let a = self.corner(x, y, half);
159        let b = self.corner(x + 1, y, half);
160        let c = self.corner(x + 1, y + 1, half);
161        let d = self.corner(x, y + 1, half);
162        let (first, second) = if (x + y) & 1 == 0 {
163            ([a, b, d], [b, c, d])
164        } else {
165            ([a, b, c], [a, c, d])
166        };
167        let mut limit = tmax;
168        let mut t = 0.0f32;
169        let mut hit = false;
170        if ray_triangle(
171            origin, dir, limit, first[0], first[1], first[2], &mut t, normal,
172        ) {
173            limit = t;
174            hit = true;
175        }
176        if ray_triangle(
177            origin, dir, limit, second[0], second[1], second[2], &mut t, normal,
178        ) {
179            hit = true;
180        }
181        (hit, t)
182    }
183
184    /// The normal of the ground under `(x, y)`: a ray straight down from height `top`, at most
185    /// `length` long. It is zero when the ray hits nothing.
186    ///
187    /// Original: `FUN_1400afef0`.
188    pub fn normal_below(&self, x: f32, y: f32, top: f32, length: f32) -> [f32; 3] {
189        let half = ((self.width as i32 - 1) >> 1) as f32;
190        let mut normal = [0.0; 3];
191        self.raycast_cell(
192            (x + half) as i32,
193            (y + half) as i32,
194            [0.0, 0.0, -1.0],
195            [x, y, top],
196            length,
197            &mut normal,
198        );
199        normal
200    }
201
202    /// Ground height at a map position (0 when no triangle is hit). `normal` receives the hit
203    /// triangle's normal and is left unchanged otherwise.
204    ///
205    /// Original: `FUN_1400b6460`.
206    pub fn ground(&self, x: f32, y: f32, normal: &mut [f32; 3]) -> f32 {
207        let half = ((self.width as i32 - 1) >> 1) as f32;
208        let ix = (x + half) as i32;
209        let iy = (y + half) as i32;
210        let dir = [0.0, 0.0, -1.0];
211        let origin = [x, y, RAY_TOP];
212        let (hit, t) = self.raycast_cell(ix, iy, dir, origin, RAY_LENGTH, normal);
213        if hit {
214            t * dir[2] + origin[2]
215        } else {
216            0.0
217        }
218    }
219
220    /// The natural type of gameplay map cell `index` (row-major over `map_size²`). The gameplay map
221    /// is `n × n` cells centred on the `2n`-cell terrain.
222    ///
223    /// Original: `FUN_1400a3790`.
224    pub fn natural_cell_type(&self, map_size: usize, index: usize) -> CellType {
225        let size = map_size as i32;
226        let index = index as i32;
227        let offset = (size >> 1) as f32 - 0.5;
228        let x = (index % size) as f32 - offset;
229        let y = (index / size) as f32 - offset;
230        let mut normal = [0.0; 3];
231        let h = self.ground(x, y, &mut normal);
232        CellType::classify(h, normal[2])
233    }
234}
235
236// Byte +0x29 of the 48-byte map cell.
237/// The type of a map cell. Types 5 to 8 block movement (the mask `0x1e0`); the others can be
238/// walked on (the mask `0xe1f`). Types whose meaning is not confirmed have no name.
239#[derive(Debug, Clone, Copy, PartialEq, Eq)]
240pub struct CellType(pub u8);
241
242impl CellType {
243    /// Flat ground where buildings can go.
244    pub const GROUND: CellType = CellType(2);
245    /// High ground. It blocks movement.
246    pub const HIGH: CellType = CellType(6);
247    /// Shallow water. It blocks movement.
248    pub const SHALLOW_WATER: CellType = CellType(7);
249    /// Deep water. It blocks movement.
250    pub const DEEP_WATER: CellType = CellType(8);
251    /// Rough ground: walkable, but not buildable.
252    pub const ROUGH: CellType = CellType(10);
253
254    /// The natural type of a cell from the ground height and the up component of its normal:
255    /// deep water below -1.4, shallow water below -0.25, ground from -0.25 to 0.75 when the
256    /// ground is flat (`up ≥ 0.9`), rough ground below 2.0, and high ground above.
257    ///
258    /// Original: `FUN_1400a3790`.
259    pub fn classify(height: f32, up: f32) -> CellType {
260        if height < -1.4 {
261            return CellType::DEEP_WATER;
262        }
263        if height < -0.25 {
264            return CellType::SHALLOW_WATER;
265        }
266        if (-0.25..=0.75).contains(&height) && 0.9 <= up {
267            return CellType::GROUND;
268        }
269        if height < 2.0 {
270            CellType::ROUGH
271        } else {
272            CellType::HIGH
273        }
274    }
275
276    /// Whether walkers cannot enter a cell of this type.
277    pub fn blocks_movement(self) -> bool {
278        (1u32 << self.0) & 0x1e0 != 0
279    }
280}