boon/position.rs
1//! World-coordinate helpers for Source 2's split position storage.
2//!
3//! Networked entities in Source 2 do not transmit a full world position
4//! every tick. Each position is split across two networked fields:
5//!
6//! - an integer **cell index** (`m_cellX`, `m_cellY`, `m_cellZ`) identifying
7//! which fixed-size cell of the world the entity is currently in, and
8//! - a quantized **offset** (`m_vecOrigin.m_vecX`, etc.) describing where
9//! inside that cell the entity sits, bounded to `[0, CELL_SIZE)`.
10//!
11//! The true world position (in Hammer units, the same coordinate space used
12//! by Valve's level editor and `.vmap` data) is reconstructed via
13//! [`cell_to_world`]. Reading the offset alone gives a sawtooth signal that
14//! resets every time the entity crosses a cell boundary, not a usable
15//! coordinate. See [`Entity::world_position`](crate::Entity::world_position)
16//! for the typical entity-side combine.
17
18/// Number of bits used by Source 2 to address a position within a cell.
19///
20/// The on-the-wire offset is quantized into a `2^CELL_BITS` window, so cells
21/// are `CELL_SIZE` Hammer units wide on each axis.
22pub const CELL_BITS: u32 = 9;
23
24/// Edge length of a single cell in Hammer units (`2^CELL_BITS`).
25pub const CELL_SIZE: f32 = (1u32 << CELL_BITS) as f32;
26
27/// Half the addressable world extent in Hammer units.
28///
29/// Source 2's cell grid is centred on the world origin, so cell 0 starts at
30/// `-WORLD_HALF` and cell indices run upward from there. This constant is the
31/// shift applied in [`cell_to_world`] to translate cell-relative addresses
32/// back to centred world coordinates.
33pub const WORLD_HALF: f32 = 16384.0;
34
35/// Combine a cell index and an in-cell offset into a world coordinate.
36///
37/// Applies the standard Source 2 transform `cell * CELL_SIZE - WORLD_HALF +
38/// offset` along a single axis. Operate on each axis independently to
39/// recover a full `[x, y, z]` world position; see
40/// [`Entity::world_position`](crate::Entity::world_position) for the typical
41/// entity-side combine that does this for all three axes at once.
42pub fn cell_to_world(cell: i32, offset: f32) -> f32 {
43 (cell as f32) * CELL_SIZE - WORLD_HALF + offset
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49
50 #[test]
51 fn constants_match_source2_layout() {
52 assert_eq!(CELL_BITS, 9);
53 assert_eq!(CELL_SIZE, 512.0);
54 assert_eq!(WORLD_HALF, 16384.0);
55 }
56
57 #[test]
58 fn world_origin_maps_to_centre_of_cell_32() {
59 // Cell 32 is the cell that contains the world origin (0): it starts
60 // at `32 * 512 - 16384 = 0` and ends just before `+512`.
61 assert_eq!(cell_to_world(32, 0.0), 0.0);
62 assert_eq!(cell_to_world(32, 256.0), 256.0);
63 }
64
65 #[test]
66 fn cell_zero_is_negative_world_half() {
67 assert_eq!(cell_to_world(0, 0.0), -WORLD_HALF);
68 assert_eq!(cell_to_world(0, 1.0), -WORLD_HALF + 1.0);
69 }
70
71 #[test]
72 fn adjacent_cells_differ_by_cell_size() {
73 let a = cell_to_world(32, 511.0);
74 let b = cell_to_world(33, 0.0);
75 // The sawtooth: a tiny offset step across a cell boundary jumps from
76 // (cell, ~CELL_SIZE) to (cell+1, ~0), but the *world* delta is 1.0.
77 assert!((b - a - 1.0).abs() < 1e-3);
78 }
79}