Skip to main content

steel_utils/types/
position.rs

1use std::{
2    collections::VecDeque,
3    hash::{Hash, Hasher},
4    io::{self, Cursor, Write},
5};
6
7use glam::{DVec3, IVec2, IVec3};
8use rustc_hash::FxHashSet;
9
10use crate::{
11    axis::Axis,
12    direction::Direction,
13    serial::{ReadFrom, WriteTo},
14};
15
16use super::{
17    identifier::Identifier,
18    packed_position::{PackedBlockPos, PackedChunkPos, PackedSectionBlockPos, PackedSectionPos},
19};
20
21/// A chunk position.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct ChunkPos(pub IVec2);
24
25impl Hash for ChunkPos {
26    fn hash<H: Hasher>(&self, state: &mut H) {
27        state.write_u64(PackedChunkPos::from(*self).as_raw() as u64);
28    }
29}
30
31impl ChunkPos {
32    const OFFSETS: [(i32, i32); 8] = [
33        (-1, -1),
34        (0, -1),
35        (1, -1),
36        (-1, 0),
37        (1, 0),
38        (-1, 1),
39        (0, 1),
40        (1, 1),
41    ];
42
43    /// Safety margin in chunks for world generation dependencies.
44    /// Calculated as `(32 + GENERATION_PYRAMID.getStepTo(FULL).accumulatedDependencies().size() + 1) * 2`.
45    /// The accumulated dependencies size for FULL is 9 (radius 8 + 1).
46    const SAFETY_MARGIN_CHUNKS: i32 = (32 + 12 + 1) * 2;
47
48    /// Maximum valid chunk coordinate value.
49    /// Calculated as `SectionPos.blockToSectionCoord(MAX_HORIZONTAL_COORDINATE) - SAFETY_MARGIN_CHUNKS`.
50    pub const MAX_COORDINATE_VALUE: i32 =
51        SectionPos::block_to_section_coord(BlockPos::MAX_HORIZONTAL_COORDINATE)
52            - Self::SAFETY_MARGIN_CHUNKS;
53
54    /// Returns all 8 neighbors of this chunk position.
55    #[must_use]
56    pub fn neighbors(self) -> [ChunkPos; 8] {
57        Self::OFFSETS.map(|(dx, dy)| ChunkPos::new(self.0.x + dx, self.0.y + dy))
58    }
59
60    #[must_use]
61    #[inline]
62    /// Creates a new `ChunkPos` with the given x and y coordinates.
63    pub const fn new(x: i32, y: i32) -> Self {
64        Self(IVec2::new(x, y))
65    }
66
67    /// Creates a `ChunkPos` from a world block position.
68    #[must_use]
69    pub const fn from_block_pos(pos: BlockPos) -> Self {
70        Self::new(
71            SectionPos::block_to_section_coord(pos.0.x),
72            SectionPos::block_to_section_coord(pos.0.z),
73        )
74    }
75
76    /// Creates a `ChunkPos` containing the given floating-point world position.
77    #[must_use]
78    pub fn from_entity_pos(pos: DVec3) -> Self {
79        Self::from_block_pos(BlockPos::from(pos))
80    }
81
82    /// Checks if the given chunk coordinates are within valid bounds.
83    /// Uses `Mth.absMax(x, z) <= MAX_COORDINATE_VALUE`.
84    #[must_use]
85    #[inline]
86    pub const fn is_valid(x: i32, z: i32) -> bool {
87        x.abs().max(z.abs()) <= Self::MAX_COORDINATE_VALUE
88    }
89}
90
91impl WriteTo for ChunkPos {
92    fn write(&self, writer: &mut impl Write) -> io::Result<()> {
93        self.0.write(writer)
94    }
95}
96
97impl ReadFrom for ChunkPos {
98    fn read(data: &mut Cursor<&[u8]>) -> io::Result<Self> {
99        Ok(Self(IVec2::read(data)?))
100    }
101}
102
103/// A block position.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
105pub struct BlockPos(pub IVec3);
106
107/// Result of processing a node during bfs
108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109pub enum TraversalNodeStatus {
110    /// Count the node and visit its neighbors if depth allows
111    Accept,
112    /// Do not count the node or visit its neighbors
113    Skip,
114    /// Stop traversal immediately
115    Stop,
116}
117
118/// Iterator returned by [`BlockPos::spiral_around`].
119#[derive(Clone, Debug)]
120pub struct SpiralAround {
121    directions: [Direction; 4],
122    cursor: BlockPos,
123    legs: i32,
124    leg: i32,
125    leg_size: i32,
126    leg_index: i32,
127    last: BlockPos,
128}
129
130impl Iterator for SpiralAround {
131    type Item = BlockPos;
132
133    fn next(&mut self) -> Option<Self::Item> {
134        let direction_index = (self.leg + 4).rem_euclid(4) as usize;
135        self.cursor = self.last.relative(self.directions[direction_index]);
136        self.last = self.cursor;
137
138        if self.leg_index >= self.leg_size {
139            if self.leg >= self.legs {
140                return None;
141            }
142
143            self.leg += 1;
144            self.leg_index = 0;
145            self.leg_size = self.leg / 2 + 1;
146        }
147
148        self.leg_index += 1;
149        Some(self.cursor)
150    }
151}
152
153impl From<DVec3> for BlockPos {
154    fn from(value: DVec3) -> Self {
155        BlockPos(IVec3 {
156            x: value.x.floor() as i32,
157            y: value.y.floor() as i32,
158            z: value.z.floor() as i32,
159        })
160    }
161}
162
163impl BlockPos {
164    pub const ZERO: BlockPos = BlockPos(IVec3::new(0, 0, 0));
165
166    /// Maximum horizontal coordinate value: `(1 << 26) / 2 - 1 = 33554431`
167    pub const MAX_HORIZONTAL_COORDINATE: i32 = (1 << PackedBlockPos::HORIZONTAL_BITS) / 2 - 1;
168
169    /// Creates a new `BlockPos` from coordinates.
170    #[must_use]
171    pub const fn new(x: i32, y: i32, z: i32) -> Self {
172        Self(IVec3::new(x, y, z))
173    }
174
175    /// Returns a new `BlockPos` offset by the given amounts.
176    #[must_use]
177    pub const fn offset(&self, dx: i32, dy: i32, dz: i32) -> Self {
178        Self(IVec3::new(self.0.x + dx, self.0.y + dy, self.0.z + dz))
179    }
180
181    /// Returns the x coordinate.
182    #[must_use]
183    pub const fn x(&self) -> i32 {
184        self.0.x
185    }
186
187    /// Returns the y coordinate.
188    #[must_use]
189    pub const fn y(&self) -> i32 {
190        self.0.y
191    }
192
193    /// Returns the z coordinate.
194    #[must_use]
195    pub const fn z(&self) -> i32 {
196        self.0.z
197    }
198
199    /// Returns the position one block above (Y + 1).
200    #[must_use]
201    pub const fn above(&self) -> Self {
202        self.offset(0, 1, 0)
203    }
204
205    /// Returns the position `n` blocks above (Y + n).
206    #[must_use]
207    pub const fn above_n(&self, n: i32) -> Self {
208        self.offset(0, n, 0)
209    }
210
211    /// Returns the position one block below (Y - 1).
212    #[must_use]
213    pub const fn below(&self) -> Self {
214        self.offset(0, -1, 0)
215    }
216
217    /// Returns the position `n` blocks below (Y - n).
218    #[must_use]
219    pub const fn below_n(&self, n: i32) -> Self {
220        self.offset(0, -n, 0)
221    }
222
223    /// Returns the position one block to the north (Z - 1).
224    #[must_use]
225    pub const fn north(&self) -> Self {
226        self.offset(0, 0, -1)
227    }
228
229    /// Returns the position one block to the south (Z + 1).
230    #[must_use]
231    pub const fn south(&self) -> Self {
232        self.offset(0, 0, 1)
233    }
234
235    /// Returns the position one block to the west (X - 1).
236    #[must_use]
237    pub const fn west(&self) -> Self {
238        self.offset(-1, 0, 0)
239    }
240
241    /// Returns the position one block to the east (X + 1).
242    #[must_use]
243    pub const fn east(&self) -> Self {
244        self.offset(1, 0, 0)
245    }
246
247    /// Returns the position offset by one block in the given direction.
248    #[must_use]
249    pub fn relative(self, direction: Direction) -> Self {
250        Self(self.0 + direction.offset_vec())
251    }
252
253    /// Does a breadth-first traversal of all block pos from `start_pos`
254    #[must_use]
255    pub fn breadth_first_traversal<NP, P>(
256        start_pos: Self,
257        max_depth: i32,
258        max_count: i32,
259        mut neighbor_provider: NP,
260        mut node_processor: P,
261    ) -> i32
262    where
263        NP: FnMut(Self, &mut dyn FnMut(Self)),
264        P: FnMut(Self) -> TraversalNodeStatus,
265    {
266        let mut nodes = VecDeque::from([(start_pos, 0)]);
267        let mut visited = FxHashSet::default();
268        let mut count = 0;
269
270        while let Some((current_pos, depth)) = nodes.pop_front() {
271            if !visited.insert(current_pos) {
272                continue;
273            }
274
275            let next = node_processor(current_pos);
276            if next == TraversalNodeStatus::Skip {
277                continue;
278            }
279
280            if next == TraversalNodeStatus::Stop {
281                break;
282            }
283
284            count += 1;
285            if count >= max_count {
286                return count;
287            }
288
289            if depth < max_depth {
290                let next_depth = depth + 1;
291                neighbor_provider(current_pos, &mut |pos| nodes.push_back((pos, next_depth)));
292            }
293        }
294
295        count
296    }
297
298    /// Returns the position offset by `n` blocks in the given direction.
299    #[must_use]
300    pub fn relative_n(&self, direction: Direction, n: i32) -> Self {
301        if n == 0 {
302            *self
303        } else {
304            Self(self.0 + direction.offset_vec() * n)
305        }
306    }
307
308    /// Returns vanilla `BlockPos.spiralAround`.
309    ///
310    /// # Panics
311    ///
312    /// Panics if `radius` is negative or if both directions are on the same axis.
313    #[must_use]
314    pub fn spiral_around(
315        center: Self,
316        radius: i32,
317        first_direction: Direction,
318        second_direction: Direction,
319    ) -> SpiralAround {
320        assert!(radius >= 0, "spiral radius must be non-negative");
321        assert!(
322            first_direction.get_axis() != second_direction.get_axis(),
323            "spiral directions cannot be on the same axis"
324        );
325
326        let cursor = center.relative(second_direction);
327        SpiralAround {
328            directions: [
329                first_direction,
330                second_direction,
331                first_direction.opposite(),
332                second_direction.opposite(),
333            ],
334            cursor,
335            legs: 4 * radius,
336            leg: -1,
337            leg_size: 0,
338            leg_index: 0,
339            last: cursor,
340        }
341    }
342
343    /// Returns the position offset by `n` blocks along the given axis.
344    #[must_use]
345    pub const fn relative_axis(&self, axis: Axis, n: i32) -> Self {
346        if n == 0 {
347            *self
348        } else {
349            match axis {
350                Axis::X => self.offset(n, 0, 0),
351                Axis::Y => self.offset(0, n, 0),
352                Axis::Z => self.offset(0, 0, n),
353            }
354        }
355    }
356
357    /// Returns a new position with the same X and Z but the given Y.
358    #[must_use]
359    pub const fn at_y(&self, y: i32) -> Self {
360        Self::new(self.0.x, y, self.0.z)
361    }
362
363    /// Returns a new position with all coordinates multiplied by the given factor.
364    #[must_use]
365    pub const fn multiply(&self, factor: i32) -> Self {
366        if factor == 1 {
367            *self
368        } else if factor == 0 {
369            Self::ZERO
370        } else {
371            Self::new(self.0.x * factor, self.0.y * factor, self.0.z * factor)
372        }
373    }
374
375    /// Returns the center of this block as a floating-point position.
376    #[must_use]
377    pub fn get_center(&self) -> (f64, f64, f64) {
378        (
379            f64::from(self.0.x) + 0.5,
380            f64::from(self.0.y) + 0.5,
381            f64::from(self.0.z) + 0.5,
382        )
383    }
384
385    /// Returns the bottom center of this block (center of the bottom face).
386    #[must_use]
387    pub fn get_bottom_center(&self) -> (f64, f64, f64) {
388        (
389            f64::from(self.0.x) + 0.5,
390            f64::from(self.0.y),
391            f64::from(self.0.z) + 0.5,
392        )
393    }
394
395    /// Creates a `BlockPos` containing the given floating-point coordinates.
396    #[must_use]
397    pub const fn containing(x: f64, y: f64, z: f64) -> Self {
398        Self::new(x.floor() as i32, y.floor() as i32, z.floor() as i32)
399    }
400
401    /// Returns the minimum coordinates of two positions.
402    #[must_use]
403    pub const fn min(a: BlockPos, b: BlockPos) -> Self {
404        Self::new(a.0.x.min(b.0.x), a.0.y.min(b.0.y), a.0.z.min(b.0.z))
405    }
406
407    /// Returns the maximum coordinates of two positions.
408    #[must_use]
409    pub const fn max(a: BlockPos, b: BlockPos) -> Self {
410        Self::new(a.0.x.max(b.0.x), a.0.y.max(b.0.y), a.0.z.max(b.0.z))
411    }
412
413    /// Returns positions in vanilla `BlockPos.withinManhattan` order.
414    #[must_use]
415    pub const fn within_manhattan(
416        self,
417        reach_x: i32,
418        reach_y: i32,
419        reach_z: i32,
420    ) -> BlockPosWithinManhattan {
421        BlockPosWithinManhattan {
422            origin: self,
423            reach_x,
424            reach_y,
425            reach_z,
426            max_depth: reach_x + reach_y + reach_z,
427            current_depth: 0,
428            max_x: 0,
429            max_y: 0,
430            x: 0,
431            y: 0,
432            pending_z_mirror: None,
433            done: false,
434        }
435    }
436
437    /// Returns vanilla `BlockPos.findClosestMatch`.
438    #[must_use]
439    pub fn find_closest_match(
440        self,
441        horizontal_search_radius: i32,
442        vertical_search_radius: i32,
443        mut predicate: impl FnMut(BlockPos) -> bool,
444    ) -> Option<BlockPos> {
445        self.within_manhattan(
446            horizontal_search_radius,
447            vertical_search_radius,
448            horizontal_search_radius,
449        )
450        .find(|pos| predicate(*pos))
451    }
452}
453
454/// Iterator returned by [`BlockPos::within_manhattan`].
455#[derive(Debug, Clone)]
456pub struct BlockPosWithinManhattan {
457    origin: BlockPos,
458    reach_x: i32,
459    reach_y: i32,
460    reach_z: i32,
461    max_depth: i32,
462    current_depth: i32,
463    max_x: i32,
464    max_y: i32,
465    x: i32,
466    y: i32,
467    pending_z_mirror: Option<BlockPos>,
468    done: bool,
469}
470
471impl Iterator for BlockPosWithinManhattan {
472    type Item = BlockPos;
473
474    fn next(&mut self) -> Option<Self::Item> {
475        if let Some(pos) = self.pending_z_mirror.take() {
476            return Some(pos);
477        }
478        if self.done {
479            return None;
480        }
481
482        loop {
483            if self.y > self.max_y {
484                self.x += 1;
485                if self.x > self.max_x {
486                    self.current_depth += 1;
487                    if self.current_depth > self.max_depth {
488                        self.done = true;
489                        return None;
490                    }
491
492                    self.max_x = self.reach_x.min(self.current_depth);
493                    self.x = -self.max_x;
494                }
495
496                self.max_y = self.reach_y.min(self.current_depth - self.x.abs());
497                self.y = -self.max_y;
498            }
499
500            let x = self.x;
501            let y = self.y;
502            let z = self.current_depth - x.abs() - y.abs();
503            self.y += 1;
504            if z > self.reach_z {
505                continue;
506            }
507
508            let pos = self.origin.offset(x, y, z);
509            if z != 0 {
510                self.pending_z_mirror = Some(self.origin.offset(x, y, -z));
511            }
512            return Some(pos);
513        }
514    }
515}
516
517impl ReadFrom for BlockPos {
518    fn read(data: &mut Cursor<&[u8]>) -> io::Result<Self> {
519        let packed = <i64 as ReadFrom>::read(data)?;
520        Ok(PackedBlockPos::from_raw(packed).into())
521    }
522}
523
524/// A position tied to a dimension key.
525#[derive(Debug, Clone, PartialEq, Eq, Hash)]
526pub struct GlobalPos {
527    /// Dimension containing the block position.
528    pub dimension: Identifier,
529    /// Block position within the dimension.
530    pub pos: BlockPos,
531}
532
533impl GlobalPos {
534    /// Creates a new global position.
535    #[must_use]
536    pub const fn new(dimension: Identifier, pos: BlockPos) -> Self {
537        Self { dimension, pos }
538    }
539}
540
541impl ReadFrom for GlobalPos {
542    fn read(data: &mut Cursor<&[u8]>) -> io::Result<Self> {
543        Ok(Self {
544            dimension: <Identifier as ReadFrom>::read(data)?,
545            pos: BlockPos::read(data)?,
546        })
547    }
548}
549
550impl WriteTo for GlobalPos {
551    fn write(&self, writer: &mut impl Write) -> io::Result<()> {
552        self.dimension.write(writer)?;
553        self.pos.write(writer)
554    }
555}
556
557/// A chunk section position (16x16x16 region).
558#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
559pub struct SectionPos(pub IVec3);
560
561impl SectionPos {
562    const SECTION_BITS: i32 = 4;
563    const SECTION_SIZE: i32 = 1 << Self::SECTION_BITS; // 16
564    pub(super) const SECTION_MASK: i32 = Self::SECTION_SIZE - 1; // 15
565
566    /// Creates a new `SectionPos` from section coordinates.
567    #[must_use]
568    pub const fn new(x: i32, y: i32, z: i32) -> Self {
569        Self(IVec3::new(x, y, z))
570    }
571
572    /// Converts a block coordinate to a section coordinate.
573    #[must_use]
574    #[inline]
575    pub const fn block_to_section_coord(block_coord: i32) -> i32 {
576        block_coord >> Self::SECTION_BITS
577    }
578
579    /// Creates a `SectionPos` from a `BlockPos`.
580    #[must_use]
581    pub const fn from_block_pos(pos: BlockPos) -> Self {
582        Self::new(
583            Self::block_to_section_coord(pos.0.x),
584            Self::block_to_section_coord(pos.0.y),
585            Self::block_to_section_coord(pos.0.z),
586        )
587    }
588
589    /// Creates a `SectionPos` containing the given floating-point world position.
590    #[must_use]
591    pub fn from_entity_pos(pos: DVec3) -> Self {
592        Self::from_block_pos(BlockPos::from(pos))
593    }
594
595    /// Gets the X coordinate.
596    #[must_use]
597    pub const fn x(&self) -> i32 {
598        self.0.x
599    }
600
601    /// Gets the Y coordinate.
602    #[must_use]
603    pub const fn y(&self) -> i32 {
604        self.0.y
605    }
606
607    /// Gets the Z coordinate.
608    #[must_use]
609    pub const fn z(&self) -> i32 {
610        self.0.z
611    }
612
613    /// Converts section-relative coordinates to an absolute block X coordinate.
614    #[must_use]
615    pub const fn relative_to_block_x(&self, relative: PackedSectionBlockPos) -> i32 {
616        (self.0.x << Self::SECTION_BITS) + relative.x() as i32
617    }
618
619    /// Converts section-relative coordinates to an absolute block Y coordinate.
620    #[must_use]
621    pub const fn relative_to_block_y(&self, relative: PackedSectionBlockPos) -> i32 {
622        (self.0.y << Self::SECTION_BITS) + relative.y() as i32
623    }
624
625    /// Converts section-relative coordinates to an absolute block Z coordinate.
626    #[must_use]
627    pub const fn relative_to_block_z(&self, relative: PackedSectionBlockPos) -> i32 {
628        (self.0.z << Self::SECTION_BITS) + relative.z() as i32
629    }
630
631    /// Packs a block position into a section-relative offset.
632    /// Format: (x << 8) | (z << 4) | y (each coordinate masked to 4 bits)
633    #[must_use]
634    #[inline]
635    pub const fn section_relative_pos(pos: BlockPos) -> PackedSectionBlockPos {
636        PackedSectionBlockPos::from_block_pos(pos)
637    }
638
639    /// Converts a section-relative packed position back to a block position.
640    #[must_use]
641    pub const fn relative_to_block_pos(&self, relative: PackedSectionBlockPos) -> BlockPos {
642        BlockPos(IVec3::new(
643            self.relative_to_block_x(relative),
644            self.relative_to_block_y(relative),
645            self.relative_to_block_z(relative),
646        ))
647    }
648}
649
650impl ReadFrom for SectionPos {
651    fn read(data: &mut Cursor<&[u8]>) -> io::Result<Self> {
652        Ok(<PackedSectionPos as ReadFrom>::read(data)?.into())
653    }
654}
655
656impl WriteTo for SectionPos {
657    fn write(&self, writer: &mut impl Write) -> io::Result<()> {
658        PackedSectionPos::from(*self).write(writer)
659    }
660}