Skip to main content

basalt_types/
angle.rs

1use crate::{Decode, Encode, EncodedSize, Result};
2
3/// A rotation angle encoded as a single unsigned byte.
4///
5/// The Minecraft protocol represents rotations as a single byte where the
6/// full 0-255 range maps to 0-360 degrees. This is used for entity head
7/// rotation (`Entity Head Look` packet), entity look direction, and
8/// similar rotation fields. The conversion formula is:
9///
10/// - Byte to degrees: `value / 256.0 * 360.0`
11/// - Degrees to byte: `degrees / 360.0 * 256.0`
12///
13/// The mapping wraps naturally: 256 steps for a full rotation gives
14/// approximately 1.4° precision per step, which is sufficient for
15/// visual entity rotation in the game.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct Angle(pub u8);
18
19impl Angle {
20    /// Creates an Angle from a degree value.
21    ///
22    /// The degree value is normalized to the 0-255 byte range. Values
23    /// outside 0-360 wrap naturally (e.g., 720° wraps to the same byte
24    /// as 360°, which is 0).
25    pub fn from_degrees(degrees: f32) -> Self {
26        Self((degrees / 360.0 * 256.0) as u8)
27    }
28
29    /// Converts the angle to degrees in the range 0.0 to ~359.0.
30    ///
31    /// The result has approximately 1.4° precision due to the single-byte
32    /// encoding. A byte value of 0 maps to 0°, 64 to 90°, 128 to 180°,
33    /// and 192 to 270°.
34    pub fn to_degrees(self) -> f32 {
35        self.0 as f32 / 256.0 * 360.0
36    }
37}
38
39/// Encodes an Angle as a single unsigned byte.
40///
41/// The angle is written directly as one byte with no transformation.
42/// This is the simplest encoding in the Minecraft protocol.
43impl Encode for Angle {
44    /// Writes the angle as a single byte.
45    fn encode(&self, buf: &mut Vec<u8>) -> Result<()> {
46        self.0.encode(buf)
47    }
48}
49
50/// Decodes an Angle from a single unsigned byte.
51///
52/// Reads exactly one byte. Any byte value is valid — the full 0-255
53/// range maps to 0-360 degrees.
54impl Decode for Angle {
55    /// Reads one byte and wraps it as an Angle.
56    ///
57    /// Fails with `Error::BufferUnderflow` if the buffer is empty.
58    fn decode(buf: &mut &[u8]) -> Result<Self> {
59        Ok(Self(u8::decode(buf)?))
60    }
61}
62
63/// An Angle always occupies exactly 1 byte on the wire.
64impl EncodedSize for Angle {
65    fn encoded_size(&self) -> usize {
66        1
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    fn roundtrip(value: u8) {
75        let angle = Angle(value);
76        let mut buf = Vec::with_capacity(angle.encoded_size());
77        angle.encode(&mut buf).unwrap();
78        assert_eq!(buf.len(), 1);
79
80        let mut cursor = buf.as_slice();
81        let decoded = Angle::decode(&mut cursor).unwrap();
82        assert!(cursor.is_empty());
83        assert_eq!(decoded, angle);
84    }
85
86    #[test]
87    fn zero() {
88        roundtrip(0);
89    }
90
91    #[test]
92    fn max() {
93        roundtrip(255);
94    }
95
96    #[test]
97    fn midpoint() {
98        roundtrip(128);
99    }
100
101    #[test]
102    fn from_degrees_zero() {
103        let angle = Angle::from_degrees(0.0);
104        assert_eq!(angle.0, 0);
105    }
106
107    #[test]
108    fn from_degrees_90() {
109        let angle = Angle::from_degrees(90.0);
110        assert_eq!(angle.0, 64);
111    }
112
113    #[test]
114    fn from_degrees_180() {
115        let angle = Angle::from_degrees(180.0);
116        assert_eq!(angle.0, 128);
117    }
118
119    #[test]
120    fn from_degrees_270() {
121        let angle = Angle::from_degrees(270.0);
122        assert_eq!(angle.0, 192);
123    }
124
125    #[test]
126    fn from_degrees_360_saturates() {
127        // 360.0 / 360.0 * 256.0 = 256.0, which saturates to 255 as u8.
128        // True wrap-around happens at values > 360 via float truncation.
129        let angle = Angle::from_degrees(360.0);
130        assert_eq!(angle.0, 255);
131    }
132
133    #[test]
134    fn to_degrees_zero() {
135        assert!((Angle(0).to_degrees() - 0.0).abs() < f32::EPSILON);
136    }
137
138    #[test]
139    fn to_degrees_90() {
140        assert!((Angle(64).to_degrees() - 90.0).abs() < f32::EPSILON);
141    }
142
143    #[test]
144    fn to_degrees_180() {
145        assert!((Angle(128).to_degrees() - 180.0).abs() < f32::EPSILON);
146    }
147
148    #[test]
149    fn to_degrees_270() {
150        assert!((Angle(192).to_degrees() - 270.0).abs() < f32::EPSILON);
151    }
152
153    #[test]
154    fn to_degrees_255() {
155        // 255 / 256 * 360 ≈ 358.59°
156        let degrees = Angle(255).to_degrees();
157        assert!((degrees - 358.59375).abs() < 0.001);
158    }
159
160    #[test]
161    fn encoded_size_is_1() {
162        assert_eq!(Angle(0).encoded_size(), 1);
163        assert_eq!(Angle(255).encoded_size(), 1);
164    }
165
166    #[test]
167    fn underflow() {
168        let mut cursor: &[u8] = &[];
169        assert!(Angle::decode(&mut cursor).is_err());
170    }
171
172    mod proptests {
173        use super::*;
174        use proptest::prelude::*;
175
176        proptest! {
177            #[test]
178            fn angle_roundtrip(v: u8) {
179                roundtrip(v);
180            }
181        }
182    }
183}