Skip to main content

ghast_protocol/types/
position.rs

1//! Block positions packed into 64 bits.
2//!
3//! The packing changed in 1.14: before it the layout (high to low bits) is
4//! `x:26 | y:12 | z:26`, from 1.14 on it is `x:26 | z:26 | y:12`. The generated packet code
5//! picks the right function for its version; [`BlockPos`] itself is version-agnostic.
6
7use bytes::{BufMut, BytesMut};
8
9use crate::codec::{DecodeResult, EncodeResult, Reader};
10use crate::error::EncodeError;
11
12/// A block position. `x`/`z` must fit 26 signed bits, `y` 12 signed bits to be encodable.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
14pub struct BlockPos {
15    /// X coordinate.
16    pub x: i32,
17    /// Y coordinate.
18    pub y: i32,
19    /// Z coordinate.
20    pub z: i32,
21}
22
23/// Bit layout of a packed [`BlockPos`].
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum PositionLayout {
26    /// `x:26 | y:12 | z:26` (1.8 – 1.13.2).
27    Xyz,
28    /// `x:26 | z:26 | y:12` (1.14+).
29    Xzy,
30}
31
32#[inline]
33const fn sign_extend(v: u64, bits: u32) -> i32 {
34    let shift = 64 - bits;
35    (((v << shift) as i64) >> shift) as i32
36}
37
38#[inline]
39const fn fits(v: i32, bits: u32) -> bool {
40    let min = -(1i64 << (bits - 1));
41    let max = (1i64 << (bits - 1)) - 1;
42    (v as i64) >= min && (v as i64) <= max
43}
44
45impl BlockPos {
46    /// Creates a position.
47    #[inline]
48    pub const fn new(x: i32, y: i32, z: i32) -> Self {
49        BlockPos { x, y, z }
50    }
51
52    /// Unpacks a 64-bit value with the given layout.
53    #[inline]
54    pub const fn unpack(v: u64, layout: PositionLayout) -> Self {
55        match layout {
56            PositionLayout::Xyz => BlockPos {
57                x: sign_extend(v >> 38, 26),
58                y: sign_extend((v >> 26) & 0xfff, 12),
59                z: sign_extend(v & 0x3ff_ffff, 26),
60            },
61            PositionLayout::Xzy => BlockPos {
62                x: sign_extend(v >> 38, 26),
63                z: sign_extend((v >> 12) & 0x3ff_ffff, 26),
64                y: sign_extend(v & 0xfff, 12),
65            },
66        }
67    }
68
69    /// Whether the coordinates fit the packed representation.
70    #[inline]
71    pub const fn is_packable(&self) -> bool {
72        fits(self.x, 26) && fits(self.y, 12) && fits(self.z, 26)
73    }
74
75    /// Packs into 64 bits; fails if a coordinate does not fit.
76    #[inline]
77    pub fn pack(&self, layout: PositionLayout) -> Result<u64, EncodeError> {
78        if !self.is_packable() {
79            return Err(EncodeError::OutOfRange("position"));
80        }
81        let x = (self.x as u64) & 0x3ff_ffff;
82        let y = (self.y as u64) & 0xfff;
83        let z = (self.z as u64) & 0x3ff_ffff;
84        Ok(match layout {
85            PositionLayout::Xyz => (x << 38) | (y << 26) | z,
86            PositionLayout::Xzy => (x << 38) | (z << 12) | y,
87        })
88    }
89
90    /// Reads a packed position with the given layout.
91    #[inline]
92    pub fn read(r: &mut Reader<'_>, layout: PositionLayout) -> DecodeResult<Self> {
93        Ok(Self::unpack(r.read_u64()?, layout))
94    }
95
96    /// Writes a packed position with the given layout.
97    #[inline]
98    pub fn write(&self, w: &mut BytesMut, layout: PositionLayout) -> EncodeResult {
99        w.put_u64(self.pack(layout)?);
100        Ok(())
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use bytes::Bytes;
108
109    #[test]
110    fn known_answers() {
111        // Hand-computed: x = 18357644 (0x118_1d8c), y = 831 (0x33f), z = -20882616.
112        let p = BlockPos::new(18357644, 831, -20882616);
113        let z = (-20882616i64 as u64) & 0x3ff_ffff;
114        let xyz = (0x118_1d8cu64 << 38) | (0x33fu64 << 26) | z;
115        let xzy = (0x118_1d8cu64 << 38) | (z << 12) | 0x33f;
116        assert_eq!(p.pack(PositionLayout::Xyz), Ok(xyz));
117        assert_eq!(p.pack(PositionLayout::Xzy), Ok(xzy));
118        assert_eq!(BlockPos::unpack(xyz, PositionLayout::Xyz), p);
119        assert_eq!(BlockPos::unpack(xzy, PositionLayout::Xzy), p);
120
121        // (-1, -1, -1) is all ones in both layouts.
122        let m = BlockPos::new(-1, -1, -1);
123        assert_eq!(m.pack(PositionLayout::Xyz), Ok(u64::MAX));
124        assert_eq!(m.pack(PositionLayout::Xzy), Ok(u64::MAX));
125
126        // (1, 2, 3) bytes in 1.8 layout: x<<38 | y<<26 | z.
127        let mut w = BytesMut::new();
128        BlockPos::new(1, 2, 3)
129            .write(&mut w, PositionLayout::Xyz)
130            .unwrap();
131        assert_eq!(&w[..], &[0x00, 0x00, 0x00, 0x40, 0x08, 0x00, 0x00, 0x03]);
132        let b = Bytes::from(w.to_vec());
133        assert_eq!(
134            BlockPos::read(&mut Reader::new(&b), PositionLayout::Xyz),
135            Ok(BlockPos::new(1, 2, 3))
136        );
137        // Same position in the 1.14+ layout: x<<38 | z<<12 | y.
138        let mut w = BytesMut::new();
139        BlockPos::new(1, 2, 3)
140            .write(&mut w, PositionLayout::Xzy)
141            .unwrap();
142        assert_eq!(&w[..], &[0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x30, 0x02]);
143    }
144
145    #[test]
146    fn range_checks() {
147        assert!(BlockPos::new(33554431, 2047, -33554432).is_packable());
148        assert_eq!(
149            BlockPos::new(33554432, 0, 0).pack(PositionLayout::Xyz),
150            Err(EncodeError::OutOfRange("position"))
151        );
152        assert!(BlockPos::new(0, 2048, 0).pack(PositionLayout::Xzy).is_err());
153        assert!(
154            BlockPos::new(0, -2049, 0)
155                .pack(PositionLayout::Xzy)
156                .is_err()
157        );
158    }
159}