Skip to main content

basalt_types/
position.rs

1use crate::{Decode, Encode, EncodedSize, Result};
2
3/// A packed block position encoded as a single 64-bit integer.
4///
5/// The Minecraft protocol packs three spatial coordinates into one `i64`
6/// to minimize bandwidth for the most common spatial reference in the game.
7/// Used in packets like block changes, player digging, block placement,
8/// sign updates, and many more.
9///
10/// Bit layout (MSB to LSB):
11/// - x: 26 bits (signed, range -33554432 to 33554431)
12/// - z: 26 bits (signed, range -33554432 to 33554431)
13/// - y: 12 bits (signed, range -2048 to 2047)
14///
15/// Encoding: `(x & 0x3FFFFFF) << 38 | (z & 0x3FFFFFF) << 12 | (y & 0xFFF)`
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
17pub struct Position {
18    pub x: i32,
19    pub y: i32,
20    pub z: i32,
21}
22
23impl Position {
24    /// Creates a new position from block coordinates.
25    pub fn new(x: i32, y: i32, z: i32) -> Self {
26        Self { x, y, z }
27    }
28
29    /// Packs the position into a single 64-bit integer.
30    ///
31    /// The x and z coordinates are masked to 26 bits, y to 12 bits.
32    /// Bits beyond the valid range are silently truncated.
33    fn pack(&self) -> i64 {
34        ((self.x as i64 & 0x3FFFFFF) << 38)
35            | ((self.z as i64 & 0x3FFFFFF) << 12)
36            | (self.y as i64 & 0xFFF)
37    }
38
39    /// Unpacks a 64-bit integer into x, y, z coordinates.
40    ///
41    /// Performs sign extension for each field: x and z from 26 bits,
42    /// y from 12 bits. This correctly handles negative coordinates.
43    fn unpack(val: i64) -> Self {
44        let mut x = (val >> 38) as i32;
45        let mut z = ((val >> 12) & 0x3FFFFFF) as i32;
46        let mut y = (val & 0xFFF) as i32;
47
48        // Sign-extend from 26 bits for x and z
49        if x >= 1 << 25 {
50            x -= 1 << 26;
51        }
52        if z >= 1 << 25 {
53            z -= 1 << 26;
54        }
55        // Sign-extend from 12 bits for y
56        if y >= 1 << 11 {
57            y -= 1 << 12;
58        }
59
60        Self { x, y, z }
61    }
62}
63
64/// Encodes a Position as a packed 64-bit big-endian integer.
65///
66/// The three coordinates are packed into a single `i64` using the bit
67/// layout described on the type. The packed value is then written as
68/// 8 big-endian bytes, matching the Minecraft protocol wire format.
69impl Encode for Position {
70    /// Packs x, y, z into a 64-bit integer and writes it as 8 big-endian bytes.
71    fn encode(&self, buf: &mut Vec<u8>) -> Result<()> {
72        self.pack().encode(buf)
73    }
74}
75
76/// Decodes a Position from a packed 64-bit big-endian integer.
77///
78/// Reads 8 bytes as a big-endian `i64`, then unpacks x (26 bits),
79/// z (26 bits), and y (12 bits) with proper sign extension for
80/// negative coordinates.
81impl Decode for Position {
82    /// Reads 8 big-endian bytes, unpacks into x, y, z with sign extension.
83    ///
84    /// Fails with `Error::BufferUnderflow` if fewer than 8 bytes remain.
85    fn decode(buf: &mut &[u8]) -> Result<Self> {
86        let val = i64::decode(buf)?;
87        Ok(Self::unpack(val))
88    }
89}
90
91/// A Position always occupies exactly 8 bytes on the wire (one packed i64).
92impl EncodedSize for Position {
93    fn encoded_size(&self) -> usize {
94        8
95    }
96}
97
98/// A block position using full 32-bit coordinates.
99///
100/// BlockPosition represents a block's location in the world without the bit
101/// packing constraints of [`Position`]. It is used internally for world
102/// logic and converts to/from `Position` for protocol serialization.
103///
104/// Unlike `Position`, BlockPosition is not directly serialized on the wire —
105/// it converts through `Position` for encoding. This type exists to
106/// provide a more ergonomic API for working with block coordinates
107/// without worrying about bit packing limitations.
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109pub struct BlockPosition {
110    pub x: i32,
111    pub y: i32,
112    pub z: i32,
113}
114
115impl BlockPosition {
116    /// Creates a new block position.
117    pub fn new(x: i32, y: i32, z: i32) -> Self {
118        Self { x, y, z }
119    }
120
121    /// Returns the chunk position that contains this block.
122    ///
123    /// Chunk coordinates are derived by dividing block coordinates by 16
124    /// (arithmetic right shift by 4). This matches the Minecraft convention
125    /// where each chunk is a 16x16 column of blocks.
126    pub fn chunk_pos(&self) -> ChunkPosition {
127        ChunkPosition {
128            x: self.x >> 4,
129            z: self.z >> 4,
130        }
131    }
132}
133
134/// Converts a [`Position`] (packed wire format) into a [`BlockPosition`] (full i32 coordinates).
135impl From<Position> for BlockPosition {
136    fn from(pos: Position) -> Self {
137        Self {
138            x: pos.x,
139            y: pos.y,
140            z: pos.z,
141        }
142    }
143}
144
145/// Converts a [`BlockPosition`] into a [`Position`] for wire serialization.
146///
147/// Coordinates outside the Position range (x/z: 26-bit signed, y: 12-bit
148/// signed) will be silently truncated during packing.
149impl From<BlockPosition> for Position {
150    fn from(pos: BlockPosition) -> Self {
151        Position {
152            x: pos.x,
153            y: pos.y,
154            z: pos.z,
155        }
156    }
157}
158
159/// A chunk position in the world, identified by chunk-level x and z coordinates.
160///
161/// Each chunk is a 16x16 column of blocks. ChunkPosition represents the chunk's
162/// location in the world grid. It is derived from block coordinates by
163/// dividing by 16. Used for chunk loading, unloading, and spatial indexing.
164///
165/// ChunkPosition is not directly serialized on the wire — chunk packets use
166/// their own field encoding. This type exists for spatial logic.
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
168pub struct ChunkPosition {
169    pub x: i32,
170    pub z: i32,
171}
172
173impl ChunkPosition {
174    /// Creates a new chunk position.
175    pub fn new(x: i32, z: i32) -> Self {
176        Self { x, z }
177    }
178}
179
180/// Converts a [`BlockPosition`] to the [`ChunkPosition`] that contains it.
181impl From<BlockPosition> for ChunkPosition {
182    fn from(pos: BlockPosition) -> Self {
183        pos.chunk_pos()
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use crate::Error;
191
192    fn roundtrip(x: i32, y: i32, z: i32) {
193        let pos = Position::new(x, y, z);
194        let mut buf = Vec::with_capacity(pos.encoded_size());
195        pos.encode(&mut buf).unwrap();
196        assert_eq!(buf.len(), 8);
197
198        let mut cursor = buf.as_slice();
199        let decoded = Position::decode(&mut cursor).unwrap();
200        assert!(cursor.is_empty());
201        assert_eq!(decoded, pos);
202    }
203
204    // -- Position encode/decode --
205
206    #[test]
207    fn origin() {
208        roundtrip(0, 0, 0);
209    }
210
211    #[test]
212    fn positive_coords() {
213        roundtrip(100, 64, 200);
214    }
215
216    #[test]
217    fn negative_coords() {
218        roundtrip(-100, -32, -200);
219    }
220
221    #[test]
222    fn max_values() {
223        // x/z max: 2^25 - 1 = 33554431, y max: 2^11 - 1 = 2047
224        roundtrip(33554431, 2047, 33554431);
225    }
226
227    #[test]
228    fn min_values() {
229        // x/z min: -2^25 = -33554432, y min: -2^11 = -2048
230        roundtrip(-33554432, -2048, -33554432);
231    }
232
233    #[test]
234    fn typical_overworld() {
235        // Typical overworld spawn area
236        roundtrip(256, 72, -128);
237    }
238
239    #[test]
240    fn position_underflow() {
241        let mut cursor: &[u8] = &[0x01; 7];
242        assert!(matches!(
243            Position::decode(&mut cursor),
244            Err(Error::BufferUnderflow { .. })
245        ));
246    }
247
248    #[test]
249    fn encoded_size_is_8() {
250        assert_eq!(Position::new(0, 0, 0).encoded_size(), 8);
251        assert_eq!(Position::new(100, 200, 300).encoded_size(), 8);
252    }
253
254    // -- Pack/unpack --
255
256    #[test]
257    fn pack_known_value() {
258        // From wiki.vg: position (18357644, 831, -20882616)
259        let pos = Position::new(18357644, 831, -20882616);
260        let packed = pos.pack();
261        let unpacked = Position::unpack(packed);
262        assert_eq!(unpacked, pos);
263    }
264
265    // -- BlockPosition --
266
267    #[test]
268    fn blockpos_to_position() {
269        let bp = BlockPosition::new(100, 64, -200);
270        let pos: Position = bp.into();
271        assert_eq!(pos, Position::new(100, 64, -200));
272    }
273
274    #[test]
275    fn position_to_blockpos() {
276        let pos = Position::new(-50, 128, 300);
277        let bp: BlockPosition = pos.into();
278        assert_eq!(bp, BlockPosition::new(-50, 128, 300));
279    }
280
281    // -- ChunkPosition --
282
283    #[test]
284    fn blockpos_to_chunkpos() {
285        let bp = BlockPosition::new(100, 64, -200);
286        let cp = bp.chunk_pos();
287        assert_eq!(cp, ChunkPosition::new(6, -13));
288    }
289
290    #[test]
291    fn blockpos_to_chunkpos_negative() {
292        // Block (-1, 0, -1) should be in chunk (-1, -1)
293        let bp = BlockPosition::new(-1, 0, -1);
294        let cp = bp.chunk_pos();
295        assert_eq!(cp, ChunkPosition::new(-1, -1));
296    }
297
298    #[test]
299    fn blockpos_to_chunkpos_origin() {
300        let bp = BlockPosition::new(0, 0, 0);
301        let cp = bp.chunk_pos();
302        assert_eq!(cp, ChunkPosition::new(0, 0));
303    }
304
305    #[test]
306    fn chunkpos_from_blockpos() {
307        let bp = BlockPosition::new(32, 0, 48);
308        let cp: ChunkPosition = bp.into();
309        assert_eq!(cp, ChunkPosition::new(2, 3));
310    }
311
312    // -- proptest --
313
314    mod proptests {
315        use super::*;
316        use proptest::prelude::*;
317
318        proptest! {
319            #[test]
320            fn position_roundtrip(
321                x in -33554432i32..=33554431,
322                y in -2048i32..=2047,
323                z in -33554432i32..=33554431,
324            ) {
325                roundtrip(x, y, z);
326            }
327        }
328    }
329}