Skip to main content

steel_utils/
direction.rs

1//! Direction enum representing the six cardinal directions in Minecraft.
2//!
3//! This is moved from `steel-registry::blocks::properties::Direction`.
4
5use std::io::{self, Cursor};
6
7use glam::IVec3;
8use steel_math::{DEGREE_90, DEGREE_180, DEGREE_270};
9
10use crate::{axis::Axis, codec::VarInt, serial::ReadFrom, types::BlockPos};
11
12/// The six cardinal directions in Minecraft.
13#[derive(Clone, Copy, Debug)]
14#[derive_const(PartialEq)]
15pub enum Direction {
16    /// Negative Y direction.
17    Down,
18    /// Positive Y direction.
19    Up,
20    /// Negative Z direction.
21    North,
22    /// Positive Z direction.
23    South,
24    /// Negative X direction.
25    West,
26    /// Positive X direction.
27    East,
28}
29
30impl ReadFrom for Direction {
31    fn read(data: &mut Cursor<&[u8]>) -> io::Result<Self> {
32        let id = VarInt::read(data)?.0;
33        if (0..6).contains(&id) {
34            Ok(Direction::from_3d_data_value(id))
35        } else {
36            Err(io::Error::other("Invalid Direction id"))
37        }
38    }
39}
40
41impl Direction {
42    /// Vanilla `Direction.get3DDataValue()` like Down=0,…,East=5.
43    #[must_use]
44    pub const fn get_3d_data_value(self) -> i32 {
45        match self {
46            Direction::Down => 0,
47            Direction::Up => 1,
48            Direction::North => 2,
49            Direction::South => 3,
50            Direction::West => 4,
51            Direction::East => 5,
52        }
53    }
54
55    /// Vanilla `Direction.from3DDataValue(int)`.
56    /// Uses Java remainder semantics (toward zero) then absolute value
57    /// `BY_3D_DATA`[Mth.abs(data % 6)].
58    #[must_use]
59    pub const fn from_3d_data_value(data: i32) -> Direction {
60        let rem = data % 6;
61        let idx = if rem < 0 {
62            (-rem) as usize
63        } else {
64            rem as usize
65        };
66        Direction::ALL[idx]
67    }
68
69    /// Returns the block position offset for this direction.
70    #[must_use]
71    pub const fn offset(self) -> (i32, i32, i32) {
72        match self {
73            Direction::Down => (0, -1, 0),
74            Direction::Up => (0, 1, 0),
75            Direction::North => (0, 0, -1),
76            Direction::South => (0, 0, 1),
77            Direction::West => (-1, 0, 0),
78            Direction::East => (1, 0, 0),
79        }
80    }
81
82    /// Returns the XZ offset for this direction, treating the vertical directions as no movement.
83    ///
84    /// For `Down` and `Up` the result is `(0, 0)`.
85    #[must_use]
86    pub const fn offset_xz(self) -> (i32, i32) {
87        match self {
88            Direction::Down | Direction::Up => (0, 0),
89            Direction::North => (0, -1),
90            Direction::South => (0, 1),
91            Direction::West => (-1, 0),
92            Direction::East => (1, 0),
93        }
94    }
95
96    /// Returns the block position offset for this direction as an [`IVec3`].
97    #[must_use]
98    pub const fn offset_vec(self) -> IVec3 {
99        let (x, y, z) = self.offset();
100        IVec3::new(x, y, z)
101    }
102
103    /// Returns the block position relative to the given position in this direction.
104    #[must_use]
105    pub fn relative(self, pos: BlockPos) -> BlockPos {
106        BlockPos(pos.0 + self.offset_vec())
107    }
108
109    /// Returns the axis this direction is on.
110    #[must_use]
111    pub const fn get_axis(self) -> Axis {
112        match self {
113            Direction::Down | Direction::Up => Axis::Y,
114            Direction::North | Direction::South => Axis::Z,
115            Direction::West | Direction::East => Axis::X,
116        }
117    }
118
119    /// Returns the opposite direction.
120    #[must_use]
121    pub const fn opposite(self) -> Direction {
122        match self {
123            Direction::Down => Direction::Up,
124            Direction::Up => Direction::Down,
125            Direction::North => Direction::South,
126            Direction::South => Direction::North,
127            Direction::West => Direction::East,
128            Direction::East => Direction::West,
129        }
130    }
131
132    /// Returns the horizontal direction from a yaw rotation.
133    #[must_use]
134    pub fn from_yaw(yaw: f32) -> Direction {
135        match ((f64::from(yaw) / f64::from(DEGREE_90) + 0.5).floor() as i32) & 3 {
136            0 => Direction::South,
137            1 => Direction::West,
138            2 => Direction::North,
139            _ => Direction::East,
140        }
141    }
142
143    /// Returns the yaw rotation for this direction.
144    ///
145    /// Only meaningful for horizontal directions.
146    /// Vertical directions return 0.
147    #[must_use]
148    pub const fn to_yaw(self) -> f32 {
149        match self {
150            Direction::North => DEGREE_180,
151            Direction::South | Direction::Up | Direction::Down => 0.0,
152            Direction::West => DEGREE_90,
153            Direction::East => DEGREE_270,
154        }
155    }
156
157    /// Returns the axis this direction is on.
158    #[must_use]
159    pub const fn axis(self) -> Axis {
160        self.get_axis()
161    }
162
163    /// Returns vanilla `Direction.get(AxisDirection.POSITIVE, axis)`.
164    #[must_use]
165    pub const fn positive_for_axis(axis: Axis) -> Direction {
166        match axis {
167            Axis::X => Direction::East,
168            Axis::Y => Direction::Up,
169            Axis::Z => Direction::South,
170        }
171    }
172
173    /// Returns whether this direction is horizontal (not up or down).
174    #[must_use]
175    pub const fn is_horizontal(self) -> bool {
176        matches!(
177            self,
178            Direction::North | Direction::South | Direction::East | Direction::West
179        )
180    }
181
182    /// Rotates this direction 90 degrees clockwise around the Y axis.
183    ///
184    /// Vertical directions are unchanged.
185    #[must_use]
186    pub const fn rotate_y_clockwise(self) -> Direction {
187        match self {
188            Direction::North => Direction::East,
189            Direction::East => Direction::South,
190            Direction::South => Direction::West,
191            Direction::West => Direction::North,
192            other => other,
193        }
194    }
195
196    /// Rotates this direction 90 degrees counter-clockwise around the Y axis.
197    ///
198    /// Vertical directions are unchanged.
199    #[must_use]
200    pub const fn rotate_y_counter_clockwise(self) -> Direction {
201        match self {
202            Direction::North => Direction::West,
203            Direction::West => Direction::South,
204            Direction::South => Direction::East,
205            Direction::East => Direction::North,
206            other => other,
207        }
208    }
209
210    /// The order in which neighbor shape updates are processed.
211    /// This matches vanilla's `BlockBehavior.UPDATE_SHAPE_ORDER`.
212    pub const UPDATE_SHAPE_ORDER: [Direction; 6] = [
213        Direction::West,
214        Direction::East,
215        Direction::North,
216        Direction::South,
217        Direction::Down,
218        Direction::Up,
219    ];
220
221    /// Vanilla: `LiquidBlock.POSSIBLE_FLOW_DIRECTIONS` mapped through `getOpposite()`.
222    /// Used by `LiquidBlock.shouldSpreadLiquid()` to check neighbors for lava-water interactions.
223    pub const FLOW_NEIGHBOR_CHECK: [Direction; 5] = [
224        Direction::Up,
225        Direction::North,
226        Direction::South,
227        Direction::West,
228        Direction::East,
229    ];
230
231    /// Vanilla `Direction.Plane.HORIZONTAL` order.
232    pub const HORIZONTAL: [Direction; 4] = [
233        Direction::North,
234        Direction::East,
235        Direction::South,
236        Direction::West,
237    ];
238
239    /// Vanilla `Direction.values()` order.
240    pub const ALL: [Direction; 6] = [
241        Direction::Down,
242        Direction::Up,
243        Direction::North,
244        Direction::South,
245        Direction::West,
246        Direction::East,
247    ];
248
249    /// Returns all directions ordered by how closely they match the player's look direction.
250    ///
251    /// This matches vanilla's `Direction.orderedByNearest(Entity)`.
252    /// - `yaw`: Player's yaw rotation in degrees (0 = South, 90 = West, 180 = North, 270 = East)
253    /// - `pitch`: Player's pitch rotation in degrees (negative = looking up, positive = looking down)
254    #[must_use]
255    pub fn ordered_by_nearest(yaw: f32, pitch: f32) -> [Direction; 6] {
256        // Convert to radians and negate yaw to match vanilla's coordinate system
257        let pitch_rad = pitch.to_radians();
258        let yaw_rad = (-yaw).to_radians();
259
260        let pitch_sin = pitch_rad.sin();
261        let pitch_cos = pitch_rad.cos();
262        let yaw_sin = yaw_rad.sin();
263        let yaw_cos = yaw_rad.cos();
264
265        // Determine which direction on each axis the player is looking
266        let x_pos = yaw_sin > 0.0;
267        let y_pos = pitch_sin < 0.0; // Negative pitch = looking up
268        let z_pos = yaw_cos > 0.0;
269
270        // Calculate magnitude of look direction on each axis
271        let x_yaw = if x_pos { yaw_sin } else { -yaw_sin };
272        let y_mag = if y_pos { -pitch_sin } else { pitch_sin };
273        let z_yaw = if z_pos { yaw_cos } else { -yaw_cos };
274        let x_mag = x_yaw * pitch_cos;
275        let z_mag = z_yaw * pitch_cos;
276
277        // Determine the primary direction on each axis
278        let axis_x = if x_pos {
279            Direction::East
280        } else {
281            Direction::West
282        };
283        let axis_y = if y_pos {
284            Direction::Up
285        } else {
286            Direction::Down
287        };
288        let axis_z = if z_pos {
289            Direction::South
290        } else {
291            Direction::North
292        };
293
294        // Sort axes by magnitude and build the direction array
295        if x_yaw > z_yaw {
296            if y_mag > x_mag {
297                Self::make_direction_array(axis_y, axis_x, axis_z)
298            } else if z_mag > y_mag {
299                Self::make_direction_array(axis_x, axis_z, axis_y)
300            } else {
301                Self::make_direction_array(axis_x, axis_y, axis_z)
302            }
303        } else if y_mag > z_mag {
304            Self::make_direction_array(axis_y, axis_z, axis_x)
305        } else if x_mag > y_mag {
306            Self::make_direction_array(axis_z, axis_x, axis_y)
307        } else {
308            Self::make_direction_array(axis_z, axis_y, axis_x)
309        }
310    }
311
312    /// Creates an array of all 6 directions ordered by magnitude.
313    ///
314    /// The order is: 3 primary directions by magnitude, then their opposites in reverse order.
315    /// This matches vanilla's `Direction.makeDirectionArray()`.
316    const fn make_direction_array(
317        axis1: Direction,
318        axis2: Direction,
319        axis3: Direction,
320    ) -> [Direction; 6] {
321        [
322            axis1,
323            axis2,
324            axis3,
325            axis3.opposite(),
326            axis2.opposite(),
327            axis1.opposite(),
328        ]
329    }
330
331    /// Returns the direction name as a string (for `PropertyEnum` compatibility).
332    #[must_use]
333    pub const fn as_str(&self) -> &str {
334        match self {
335            Direction::Down => "down",
336            Direction::Up => "up",
337            Direction::North => "north",
338            Direction::South => "south",
339            Direction::West => "west",
340            Direction::East => "east",
341        }
342    }
343
344    /// Returns whether this direction is facing the given yaw angle
345    #[must_use]
346    pub fn is_facing_yaw(self, yaw: f32) -> bool {
347        let radians = yaw.to_radians();
348        let dx = -radians.sin();
349        let dz = radians.cos();
350
351        let (nx, nz) = self.offset_xz();
352
353        (nx as f32) * dx + (nz as f32) * dz > 0.0
354    }
355
356    /// Returns a random direction
357    #[must_use]
358    pub fn random() -> Self {
359        match rand::random_range(0..6) {
360            1 => Self::Up,
361            2 => Self::North,
362            3 => Self::South,
363            4 => Self::West,
364            5 => Self::East,
365            _ => Self::Down,
366        }
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use crate::axis::Axis;
373
374    use super::Direction;
375
376    #[test]
377    fn horizontal_dirs_matches_vanilla_plane_horizontal_order() {
378        assert_eq!(
379            Direction::HORIZONTAL,
380            [
381                Direction::North,
382                Direction::East,
383                Direction::South,
384                Direction::West,
385            ]
386        );
387    }
388
389    #[test]
390    fn all_dirs_matches_vanilla_values_order() {
391        assert_eq!(
392            Direction::ALL,
393            [
394                Direction::Down,
395                Direction::Up,
396                Direction::North,
397                Direction::South,
398                Direction::West,
399                Direction::East,
400            ]
401        );
402    }
403
404    #[test]
405    fn three_d_data_value_round_trips_vanilla_ids() {
406        for direction in Direction::ALL {
407            let id = direction.get_3d_data_value();
408            assert_eq!(Direction::from_3d_data_value(id), direction);
409        }
410    }
411
412    #[test]
413    fn from_yaw_matches_vanilla_from_y_rot_boundaries() {
414        let cases = [
415            (0.0, Direction::South),
416            (44.999, Direction::South),
417            (45.0, Direction::West),
418            (134.999, Direction::West),
419            (135.0, Direction::North),
420            (224.999, Direction::North),
421            (225.0, Direction::East),
422            (314.999, Direction::East),
423            (315.0, Direction::South),
424            (-44.999, Direction::South),
425            (-45.0, Direction::South),
426            (-45.001, Direction::East),
427            (-135.0, Direction::East),
428            (-225.0, Direction::North),
429            (-315.0, Direction::West),
430            (405.0, Direction::West),
431        ];
432
433        for (yaw, direction) in cases {
434            assert_eq!(Direction::from_yaw(yaw), direction, "yaw {yaw}");
435        }
436    }
437
438    #[test]
439    fn from_3d_data_value_matches_java_abs_mod_wrap() {
440        // Vanilla is "BY_3D_DATA[Mth.abs(data % 6)]".
441        assert_eq!(Direction::from_3d_data_value(6), Direction::Down);
442        assert_eq!(Direction::from_3d_data_value(7), Direction::Up);
443        assert_eq!(Direction::from_3d_data_value(-1), Direction::Up);
444        assert_eq!(Direction::from_3d_data_value(-7), Direction::Up);
445        assert_eq!(Direction::from_3d_data_value(-6), Direction::Down);
446    }
447
448    #[test]
449    fn from_yaw_preserves_vanilla_double_precision_for_large_angles() {
450        // Vanilla widens yaw to double; preserving that precision selects East here.
451        assert_eq!(Direction::from_yaw(193_273_528_320.0), Direction::East);
452    }
453
454    #[test]
455    fn positive_for_axis_matches_vanilla_axis_direction_positive() {
456        assert_eq!(Direction::positive_for_axis(Axis::X), Direction::East);
457        assert_eq!(Direction::positive_for_axis(Axis::Y), Direction::Up);
458        assert_eq!(Direction::positive_for_axis(Axis::Z), Direction::South);
459    }
460}