Skip to main content

roxlap_core/
world_query.rs

1//! Per-voxel world queries against a `.vxl` column-slab world.
2//!
3//! Port of voxlap's `getcube` (voxlap5.c:937-962) — the engine's
4//! random-access voxel lookup. Used by sphere/region edits
5//! (`meltsphere`, `setrect`, etc.) and any logic that needs to ask
6//! "what's at (x, y, z)?" without going through a ray cast.
7//!
8//! Voxlap's slab format (mirroring `roxlap-formats::vxl`):
9//!
10//! ```text
11//! slab record:
12//!   byte 0  nextptr  — advance to next slab in dwords (== 0 last slab)
13//!   byte 1  z1       — top z of floor-colour list
14//!   byte 2  z1c      — bottom z of floor-colour list MINUS 1
15//!   byte 3  z0       — ceiling z (additional slabs); dummy in first
16//!   <floor colours>  (z1c - z1 + 1) × 4 bytes (BGRA each)
17//!   <ceil colours>   n_ceil × 4 bytes — ceiling colours of the *next*
18//!                    slab (i.e. visible bottom of *this* slab's
19//!                    solid mass, observed from inside the air pocket
20//!                    between this slab and the next).
21//! ```
22//!
23//! The ceiling colours sit between this slab's floor colours and the
24//! next slab's header — so they're addressed at *negative* offsets
25//! from the next slab's header, which is what voxlap's getcube
26//! exploits via `&v[(z - v[3]) * 4]` with `z < v[3]`.
27//!
28//! `n_ceil` is derived as `n_ceil = nextptr - 2 - (z1c - z1)`. Voxlap
29//! stores the *negative* of this as `ceilnum = z1c - z1 - nextptr + 2`
30//! and uses it as a signed comparison threshold.
31
32// The slab walker is a pointer-arithmetic port; the casts mirror C's
33// implicit narrowing/sign-loss (e.g. voxlap's `(uint32_t)(x|y) >= VSID`
34// out-of-bounds test). Calls inside this module's loop are guarded by
35// the precondition checks above.
36#![allow(
37    clippy::cast_possible_truncation,
38    clippy::cast_possible_wrap,
39    clippy::cast_sign_loss
40)]
41
42/// Result of a [`getcube`] query.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum Cube {
45    /// Empty space (sky, air pocket, or out-of-bounds (x, y)).
46    Air,
47    /// Solid material whose colour isn't stored in the slab list —
48    /// either deep below the deepest slab, or in the hidden interior
49    /// between a slab's floor and the next slab's ceiling colours.
50    UnexposedSolid,
51    /// Visible voxel; `0xAARRGGBB` in voxlap's storage convention
52    /// (the slab bytes are little-endian B,G,R,intensity → reading
53    /// as `u32_le` lands intensity in the high byte).
54    Color(u32),
55}
56
57/// Look up the voxel at `(x, y, z)` in a column-slab world.
58///
59/// `slab_buf` + `column_offsets` + `vsid` describe the world the same
60/// way the rasterizer takes them: `slab_buf` is the concatenated raw
61/// slab bytes, `column_offsets[i]..column_offsets[i + 1]` is column
62/// `i = y * vsid + x`'s slab range, and `vsid` is the square map
63/// dimension. `column_offsets.len() == vsid² + 1`.
64///
65/// Out-of-bounds `(x, y)` returns [`Cube::Air`] (matches voxlap C's
66/// `(uint32_t)(x|y) >= VSID` early return). `z` is *not* range-checked
67/// — the caller is expected to clamp to `[0, MAXZDIM)` if needed.
68///
69/// # Panics
70///
71/// Panics on a malformed column whose slab walker would step past the
72/// column's data range. Voxlap's loader (`roxlap-formats::vxl::parse`)
73/// validates slab structure on parse, so any `Vxl` that round-trips
74/// the parser is safe to query.
75#[must_use]
76pub fn getcube(slab_buf: &[u8], column_offsets: &[u32], vsid: u32, x: i32, y: i32, z: i32) -> Cube {
77    // Voxlap's `(uint32_t)(x|y) >= VSID` test: rejects negatives and
78    // anything past the map edge in one bitwise comparison.
79    if x < 0 || y < 0 || (x as u32) >= vsid || (y as u32) >= vsid {
80        return Cube::Air;
81    }
82    let col_idx = (y as u32 * vsid + x as u32) as usize;
83    let col_start = column_offsets[col_idx] as usize;
84    // The slab walker self-terminates on `nextptr == 0`, so we don't
85    // need an explicit end bound. Using `column_offsets[idx + 1]` as
86    // the end was wrong post-edit: voxalloc scatters columns across
87    // vbuf, so adjacent indices in the offset table are no longer
88    // adjacent in the buffer (and may even overlap or appear in
89    // reverse order). The slab walker reads only what each column's
90    // own nextptr chain says, so slicing to end-of-buffer is safe.
91    let col = &slab_buf[col_start..];
92
93    // Walk the slab chain. `pos` is the byte offset within `col` of
94    // the current slab's header.
95    let mut pos: usize = 0;
96    loop {
97        let nextptr = i32::from(col[pos]);
98        let z1 = i32::from(col[pos + 1]);
99        let z1c = i32::from(col[pos + 2]);
100        // col[pos + 3] is z0 (additional slab) or dummy (first slab);
101        // only consulted after the `pos += nextptr*4` advance below.
102
103        if z <= z1c {
104            if z < z1 {
105                // Above the visible top of this slab — air pocket.
106                return Cube::Air;
107            }
108            // Floor colour at byte offset (z - z1)*4 + 4 from header.
109            let off = pos + (z - z1) as usize * 4 + 4;
110            return Cube::Color(read_color(col, off));
111        }
112
113        // Voxlap's signed-stride trick: ceilnum is the *negative* of
114        // the actual ceiling-colour count of the next slab. Always
115        // ≤ 0; equal to 0 when the next slab has no ceiling colours.
116        let ceilnum = z1c - z1 - nextptr + 2;
117
118        if nextptr == 0 {
119            // Last slab and z > z1c: deepest unexposed material.
120            return Cube::UnexposedSolid;
121        }
122
123        pos += nextptr as usize * 4;
124        let next_z0 = i32::from(col[pos + 3]);
125        if z < next_z0 {
126            let dz = z - next_z0; // dz < 0 here.
127            if dz < ceilnum {
128                // Above the ceiling colour list — hidden interior.
129                return Cube::UnexposedSolid;
130            }
131            // Ceiling colour at *negative* byte offset dz*4 from the
132            // next slab's header. dz ∈ [ceilnum, -1].
133            let off = (pos as isize + (dz * 4) as isize) as usize;
134            return Cube::Color(read_color(col, off));
135        }
136        // z >= next_z0: still in the air pocket above next_z0's air
137        // gap, or below it. Fall through and re-test against the next
138        // slab as the new "current" slab on the next iteration.
139    }
140}
141
142#[inline]
143fn read_color(col: &[u8], off: usize) -> u32 {
144    u32::from_le_bytes([col[off], col[off + 1], col[off + 2], col[off + 3]])
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    /// Single solid slab covering z = 5..=14 (10 visible floor voxels).
152    /// Header [nextptr=0, z1=5, z1c=14, dummy=0]. Each colour record
153    /// encodes its z value in B so we can verify the lookup picks the
154    /// right offset.
155    fn single_slab_5_to_14() -> Vec<u8> {
156        let mut col = vec![0u8, 5, 14, 0];
157        for z in 5..=14u8 {
158            col.extend_from_slice(&[z, 0xaa, 0xbb, 0x80]);
159        }
160        col
161    }
162
163    /// Two slabs separated by a one-voxel air gap.
164    ///
165    /// Slab 0 = solid mass at z=10..28 (floor at z=10..14, hidden
166    /// interior z=15..25, ceiling at z=26..28). Air pocket at z=29.
167    /// Slab 1 = solid mass at z=30..39 (floor at z=30..39).
168    ///
169    /// Layout:
170    ///   slab 0 header  [nextptr=9, z1=10, z1c=14, dummy=0]    4 bytes
171    ///   slab 0 floor   z=10..14 (5 colours, B byte = z)      20 bytes
172    ///   slab 1 ceil    z=26..28 (3 colours, B byte = z)      12 bytes
173    ///   slab 1 header  [nextptr=0, z1=30, z1c=39, z0=29]      4 bytes
174    ///   slab 1 floor   z=30..39 (10 colours, B byte = z)     40 bytes
175    ///
176    /// `nextptr_0 = (4 + 20 + 12) / 4 = 9` dwords.
177    /// `ceilnum_0 = z1c - z1 - nextptr + 2 = 14 - 10 - 9 + 2 = -3`,
178    /// so `n_ceil = 3` ✓.
179    /// `z0_1 = 29` puts the ceiling colours at `z = z0 - 3 .. z0 - 1`
180    ///        `= 26 .. 28` (matching the ceil-fill loop above).
181    fn two_slabs_with_ceiling() -> Vec<u8> {
182        let mut col = vec![9u8, 10, 14, 0];
183        for z in 10..=14u8 {
184            col.extend_from_slice(&[z, 0x10, 0x20, 0x80]);
185        }
186        for z in 26..=28u8 {
187            col.extend_from_slice(&[z, 0x30, 0x40, 0x80]);
188        }
189        col.extend_from_slice(&[0u8, 30, 39, 29]);
190        for z in 30..=39u8 {
191            col.extend_from_slice(&[z, 0x50, 0x60, 0x80]);
192        }
193        col
194    }
195
196    fn world_with(col: Vec<u8>) -> (Vec<u8>, Vec<u32>) {
197        // 1×1 world: column 0 = `col`, column_offset = [0, col.len()].
198        let len = col.len() as u32;
199        (col, vec![0, len])
200    }
201
202    #[test]
203    fn out_of_bounds_xy_returns_air() {
204        let (buf, off) = world_with(single_slab_5_to_14());
205        assert_eq!(getcube(&buf, &off, 1, -1, 0, 7), Cube::Air);
206        assert_eq!(getcube(&buf, &off, 1, 0, -1, 7), Cube::Air);
207        assert_eq!(getcube(&buf, &off, 1, 1, 0, 7), Cube::Air);
208        assert_eq!(getcube(&buf, &off, 1, 0, 1, 7), Cube::Air);
209    }
210
211    #[test]
212    fn above_first_slab_returns_air() {
213        let (buf, off) = world_with(single_slab_5_to_14());
214        // z = 0..4 is above z1=5 → air.
215        for z in 0..5 {
216            assert_eq!(getcube(&buf, &off, 1, 0, 0, z), Cube::Air, "z={z}");
217        }
218    }
219
220    #[test]
221    fn floor_color_returned_for_visible_voxels() {
222        let (buf, off) = world_with(single_slab_5_to_14());
223        // z=5..=14 are floor colours; B = z, G = 0xaa, R = 0xbb, alpha = 0x80.
224        for z in 5..=14 {
225            let want = u32::from_le_bytes([z as u8, 0xaa, 0xbb, 0x80]);
226            assert_eq!(getcube(&buf, &off, 1, 0, 0, z), Cube::Color(want), "z={z}");
227        }
228    }
229
230    #[test]
231    fn below_last_slab_returns_unexposed_solid() {
232        let (buf, off) = world_with(single_slab_5_to_14());
233        // z > z1c = 14 with nextptr = 0 → unexposed.
234        for z in 15..40 {
235            assert_eq!(
236                getcube(&buf, &off, 1, 0, 0, z),
237                Cube::UnexposedSolid,
238                "z={z}"
239            );
240        }
241    }
242
243    #[test]
244    fn second_slab_floor_resolved() {
245        let (buf, off) = world_with(two_slabs_with_ceiling());
246        // z=30..=39 are second slab's floor colours.
247        for z in 30..=39 {
248            let want = u32::from_le_bytes([z as u8, 0x50, 0x60, 0x80]);
249            assert_eq!(getcube(&buf, &off, 1, 0, 0, z), Cube::Color(want), "z={z}");
250        }
251    }
252
253    #[test]
254    fn slab0_ceiling_colors_resolved() {
255        let (buf, off) = world_with(two_slabs_with_ceiling());
256        // Slab 0's ceiling at z=26..=28 (visible bottom of slab 0,
257        // looking up from inside the air pocket below it).
258        for z in 26..=28 {
259            let want = u32::from_le_bytes([z as u8, 0x30, 0x40, 0x80]);
260            assert_eq!(getcube(&buf, &off, 1, 0, 0, z), Cube::Color(want), "z={z}");
261        }
262    }
263
264    #[test]
265    fn slab0_hidden_interior_returns_unexposed() {
266        let (buf, off) = world_with(two_slabs_with_ceiling());
267        // z=15..=25 is slab 0's hidden interior (between floor at
268        // z=14 and ceiling at z=26). Walker falls through slab 0's
269        // top, advances to slab 1, sees z < z0_next=29, dz < ceilnum
270        // = -3 so dz=z-29 ∈ [-14, -4] all satisfy dz < -3.
271        for z in 15..=25 {
272            assert_eq!(
273                getcube(&buf, &off, 1, 0, 0, z),
274                Cube::UnexposedSolid,
275                "z={z}"
276            );
277        }
278    }
279
280    #[test]
281    fn air_pocket_between_slabs_returns_air() {
282        let (buf, off) = world_with(two_slabs_with_ceiling());
283        // z=29 is the one-voxel air pocket between slab 0's bottom
284        // (z=28 ceiling) and slab 1's top (z=30 floor). z0_next=29
285        // so `z < z0_next` is false; loop continues with slab 1 as
286        // the new "current" slab. z <= z1c_1 = 39 and z < z1_1 = 30
287        // → Cube::Air.
288        assert_eq!(getcube(&buf, &off, 1, 0, 0, 29), Cube::Air);
289    }
290
291    #[test]
292    fn deep_below_last_slab_unexposed() {
293        let (buf, off) = world_with(two_slabs_with_ceiling());
294        // z >= 40 is below slab 1's floor (z1c=39) with nextptr=0.
295        for z in 40..50 {
296            assert_eq!(
297                getcube(&buf, &off, 1, 0, 0, z),
298                Cube::UnexposedSolid,
299                "z={z}"
300            );
301        }
302    }
303}