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