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/// Iterator returned by [`BlockPos::between_closed`].
153#[derive(Debug, Clone)]
154pub struct BetweenClosed {
155    min_x: i32,
156    min_y: i32,
157    min_z: i32,
158    width: i32,
159    height: i32,
160    index: i32,
161    end: i32,
162}
163
164impl Iterator for BetweenClosed {
165    type Item = BlockPos;
166
167    fn next(&mut self) -> Option<Self::Item> {
168        if self.index == self.end {
169            return None;
170        }
171
172        let x = self.index % self.width;
173        let slice = self.index / self.width;
174        let y = slice % self.height;
175        let z = slice / self.height;
176        self.index += 1;
177
178        Some(BlockPos::new(
179            self.min_x + x,
180            self.min_y + y,
181            self.min_z + z,
182        ))
183    }
184
185    fn size_hint(&self) -> (usize, Option<usize>) {
186        let remaining = (self.end - self.index).max(0) as usize;
187        (remaining, Some(remaining))
188    }
189}
190impl From<DVec3> for BlockPos {
191    fn from(value: DVec3) -> Self {
192        BlockPos(IVec3 {
193            x: value.x.floor() as i32,
194            y: value.y.floor() as i32,
195            z: value.z.floor() as i32,
196        })
197    }
198}
199
200impl BlockPos {
201    pub const ZERO: BlockPos = BlockPos(IVec3::new(0, 0, 0));
202
203    /// Maximum horizontal coordinate value: `(1 << 26) / 2 - 1 = 33554431`
204    pub const MAX_HORIZONTAL_COORDINATE: i32 = (1 << PackedBlockPos::HORIZONTAL_BITS) / 2 - 1;
205
206    /// Creates a new `BlockPos` from coordinates.
207    #[must_use]
208    pub const fn new(x: i32, y: i32, z: i32) -> Self {
209        Self(IVec3::new(x, y, z))
210    }
211
212    /// Returns a new `BlockPos` offset by the given amounts.
213    #[must_use]
214    pub const fn offset(&self, dx: i32, dy: i32, dz: i32) -> Self {
215        Self(IVec3::new(self.0.x + dx, self.0.y + dy, self.0.z + dz))
216    }
217
218    /// Returns the x coordinate.
219    #[must_use]
220    pub const fn x(&self) -> i32 {
221        self.0.x
222    }
223
224    /// Returns the y coordinate.
225    #[must_use]
226    pub const fn y(&self) -> i32 {
227        self.0.y
228    }
229
230    /// Returns the z coordinate.
231    #[must_use]
232    pub const fn z(&self) -> i32 {
233        self.0.z
234    }
235
236    /// Returns the position one block above (Y + 1).
237    #[must_use]
238    pub const fn above(&self) -> Self {
239        self.offset(0, 1, 0)
240    }
241
242    /// Returns the position `n` blocks above (Y + n).
243    #[must_use]
244    pub const fn above_n(&self, n: i32) -> Self {
245        self.offset(0, n, 0)
246    }
247
248    /// Returns the position one block below (Y - 1).
249    #[must_use]
250    pub const fn below(&self) -> Self {
251        self.offset(0, -1, 0)
252    }
253
254    /// Returns the position `n` blocks below (Y - n).
255    #[must_use]
256    pub const fn below_n(&self, n: i32) -> Self {
257        self.offset(0, -n, 0)
258    }
259
260    /// Returns the position one block to the north (Z - 1).
261    #[must_use]
262    pub const fn north(&self) -> Self {
263        self.offset(0, 0, -1)
264    }
265
266    /// Returns the position one block to the south (Z + 1).
267    #[must_use]
268    pub const fn south(&self) -> Self {
269        self.offset(0, 0, 1)
270    }
271
272    /// Returns the position one block to the west (X - 1).
273    #[must_use]
274    pub const fn west(&self) -> Self {
275        self.offset(-1, 0, 0)
276    }
277
278    /// Returns the position one block to the east (X + 1).
279    #[must_use]
280    pub const fn east(&self) -> Self {
281        self.offset(1, 0, 0)
282    }
283
284    /// Returns the position offset by one block in the given direction.
285    #[must_use]
286    pub fn relative(self, direction: Direction) -> Self {
287        Self(self.0 + direction.offset_vec())
288    }
289
290    /// Does a breadth-first traversal of all block pos from `start_pos`
291    #[must_use]
292    pub fn breadth_first_traversal<NP, P>(
293        start_pos: Self,
294        max_depth: i32,
295        max_count: i32,
296        mut neighbor_provider: NP,
297        mut node_processor: P,
298    ) -> i32
299    where
300        NP: FnMut(Self, &mut dyn FnMut(Self)),
301        P: FnMut(Self) -> TraversalNodeStatus,
302    {
303        let mut nodes = VecDeque::from([(start_pos, 0)]);
304        let mut visited = FxHashSet::default();
305        let mut count = 0;
306
307        while let Some((current_pos, depth)) = nodes.pop_front() {
308            if !visited.insert(current_pos) {
309                continue;
310            }
311
312            let next = node_processor(current_pos);
313            if next == TraversalNodeStatus::Skip {
314                continue;
315            }
316
317            if next == TraversalNodeStatus::Stop {
318                break;
319            }
320
321            count += 1;
322            if count >= max_count {
323                return count;
324            }
325
326            if depth < max_depth {
327                let next_depth = depth + 1;
328                neighbor_provider(current_pos, &mut |pos| nodes.push_back((pos, next_depth)));
329            }
330        }
331
332        count
333    }
334
335    /// Returns the position offset by `n` blocks in the given direction.
336    #[must_use]
337    pub fn relative_n(&self, direction: Direction, n: i32) -> Self {
338        if n == 0 {
339            *self
340        } else {
341            Self(self.0 + direction.offset_vec() * n)
342        }
343    }
344
345    /// Returns vanilla `BlockPos.spiralAround`.
346    ///
347    /// # Panics
348    ///
349    /// Panics if `radius` is negative or if both directions are on the same axis.
350    #[must_use]
351    pub fn spiral_around(
352        center: Self,
353        radius: i32,
354        first_direction: Direction,
355        second_direction: Direction,
356    ) -> SpiralAround {
357        assert!(radius >= 0, "spiral radius must be non-negative");
358        assert!(
359            first_direction.get_axis() != second_direction.get_axis(),
360            "spiral directions cannot be on the same axis"
361        );
362
363        let cursor = center.relative(second_direction);
364        SpiralAround {
365            directions: [
366                first_direction,
367                second_direction,
368                first_direction.opposite(),
369                second_direction.opposite(),
370            ],
371            cursor,
372            legs: 4 * radius,
373            leg: -1,
374            leg_size: 0,
375            leg_index: 0,
376            last: cursor,
377        }
378    }
379
380    /// Returns the position offset by `n` blocks along the given axis.
381    #[must_use]
382    pub const fn relative_axis(&self, axis: Axis, n: i32) -> Self {
383        if n == 0 {
384            *self
385        } else {
386            match axis {
387                Axis::X => self.offset(n, 0, 0),
388                Axis::Y => self.offset(0, n, 0),
389                Axis::Z => self.offset(0, 0, n),
390            }
391        }
392    }
393
394    /// Returns a new position with the same X and Z but the given Y.
395    #[must_use]
396    pub const fn at_y(&self, y: i32) -> Self {
397        Self::new(self.0.x, y, self.0.z)
398    }
399
400    /// Returns a new position with all coordinates multiplied by the given factor.
401    #[must_use]
402    pub const fn multiply(&self, factor: i32) -> Self {
403        if factor == 1 {
404            *self
405        } else if factor == 0 {
406            Self::ZERO
407        } else {
408            Self::new(self.0.x * factor, self.0.y * factor, self.0.z * factor)
409        }
410    }
411
412    /// Returns the center of this block as a floating-point position.
413    #[must_use]
414    pub fn get_center(&self) -> (f64, f64, f64) {
415        (
416            f64::from(self.0.x) + 0.5,
417            f64::from(self.0.y) + 0.5,
418            f64::from(self.0.z) + 0.5,
419        )
420    }
421
422    /// Returns the bottom center of this block (center of the bottom face).
423    #[must_use]
424    pub fn get_bottom_center(&self) -> (f64, f64, f64) {
425        (
426            f64::from(self.0.x) + 0.5,
427            f64::from(self.0.y),
428            f64::from(self.0.z) + 0.5,
429        )
430    }
431
432    /// Creates a `BlockPos` containing the given floating-point coordinates.
433    #[must_use]
434    pub const fn containing(x: f64, y: f64, z: f64) -> Self {
435        Self::new(x.floor() as i32, y.floor() as i32, z.floor() as i32)
436    }
437
438    /// Returns the minimum coordinates of two positions.
439    #[must_use]
440    pub const fn min(a: BlockPos, b: BlockPos) -> Self {
441        Self::new(a.0.x.min(b.0.x), a.0.y.min(b.0.y), a.0.z.min(b.0.z))
442    }
443
444    /// Returns the maximum coordinates of two positions.
445    #[must_use]
446    pub const fn max(a: BlockPos, b: BlockPos) -> Self {
447        Self::new(a.0.x.max(b.0.x), a.0.y.max(b.0.y), a.0.z.max(b.0.z))
448    }
449
450    /// Returns positions in vanilla `BlockPos.withinManhattan` order.
451    #[must_use]
452    pub const fn within_manhattan(
453        self,
454        reach_x: i32,
455        reach_y: i32,
456        reach_z: i32,
457    ) -> BlockPosWithinManhattan {
458        BlockPosWithinManhattan {
459            origin: self,
460            reach_x,
461            reach_y,
462            reach_z,
463            max_depth: reach_x + reach_y + reach_z,
464            current_depth: 0,
465            max_x: 0,
466            max_y: 0,
467            x: 0,
468            y: 0,
469            pending_z_mirror: None,
470            done: false,
471        }
472    }
473    /// Returns vanilla `BlockPos.betweenClosed(BlockPos, BlockPos)`.
474    ///
475    /// Iterates all positions in the closed box spanned by `a` and `b`,
476    /// regardless of their relative min/max ordering.
477    #[must_use]
478    pub const fn between_closed(a: Self, b: Self) -> BetweenClosed {
479        Self::between_closed_coords(
480            a.0.x.min(b.0.x),
481            a.0.y.min(b.0.y),
482            a.0.z.min(b.0.z),
483            a.0.x.max(b.0.x),
484            a.0.y.max(b.0.y),
485            a.0.z.max(b.0.z),
486        )
487    }
488
489    /// Returns vanilla `BlockPos.betweenClosed(int, int, int, int, int, int)`.
490    ///
491    /// Iterates all positions in `[min_x, max_x] x [min_y, max_y] x [min_z, max_z]`.
492    #[must_use]
493    pub const fn between_closed_coords(
494        min_x: i32,
495        min_y: i32,
496        min_z: i32,
497        max_x: i32,
498        max_y: i32,
499        max_z: i32,
500    ) -> BetweenClosed {
501        let width = max_x - min_x + 1;
502        let height = max_y - min_y + 1;
503        let depth = max_z - min_z + 1;
504
505        BetweenClosed {
506            min_x,
507            min_y,
508            min_z,
509            width,
510            height,
511            index: 0,
512            end: width * height * depth,
513        }
514    }
515
516    /// Returns vanilla `BlockPos.findClosestMatch`.
517    #[must_use]
518    pub fn find_closest_match(
519        self,
520        horizontal_search_radius: i32,
521        vertical_search_radius: i32,
522        mut predicate: impl FnMut(BlockPos) -> bool,
523    ) -> Option<BlockPos> {
524        self.within_manhattan(
525            horizontal_search_radius,
526            vertical_search_radius,
527            horizontal_search_radius,
528        )
529        .find(|pos| predicate(*pos))
530    }
531}
532
533/// Iterator returned by [`BlockPos::within_manhattan`].
534#[derive(Debug, Clone)]
535pub struct BlockPosWithinManhattan {
536    origin: BlockPos,
537    reach_x: i32,
538    reach_y: i32,
539    reach_z: i32,
540    max_depth: i32,
541    current_depth: i32,
542    max_x: i32,
543    max_y: i32,
544    x: i32,
545    y: i32,
546    pending_z_mirror: Option<BlockPos>,
547    done: bool,
548}
549
550impl Iterator for BlockPosWithinManhattan {
551    type Item = BlockPos;
552
553    fn next(&mut self) -> Option<Self::Item> {
554        if let Some(pos) = self.pending_z_mirror.take() {
555            return Some(pos);
556        }
557        if self.done {
558            return None;
559        }
560
561        loop {
562            if self.y > self.max_y {
563                self.x += 1;
564                if self.x > self.max_x {
565                    self.current_depth += 1;
566                    if self.current_depth > self.max_depth {
567                        self.done = true;
568                        return None;
569                    }
570
571                    self.max_x = self.reach_x.min(self.current_depth);
572                    self.x = -self.max_x;
573                }
574
575                self.max_y = self.reach_y.min(self.current_depth - self.x.abs());
576                self.y = -self.max_y;
577            }
578
579            let x = self.x;
580            let y = self.y;
581            let z = self.current_depth - x.abs() - y.abs();
582            self.y += 1;
583            if z > self.reach_z {
584                continue;
585            }
586
587            let pos = self.origin.offset(x, y, z);
588            if z != 0 {
589                self.pending_z_mirror = Some(self.origin.offset(x, y, -z));
590            }
591            return Some(pos);
592        }
593    }
594}
595
596impl ReadFrom for BlockPos {
597    fn read(data: &mut Cursor<&[u8]>) -> io::Result<Self> {
598        let packed = <i64 as ReadFrom>::read(data)?;
599        Ok(PackedBlockPos::from_raw(packed).into())
600    }
601}
602
603/// A position tied to a dimension key.
604#[derive(Debug, Clone, PartialEq, Eq, Hash)]
605pub struct GlobalPos {
606    /// Dimension containing the block position.
607    pub dimension: Identifier,
608    /// Block position within the dimension.
609    pub pos: BlockPos,
610}
611
612impl GlobalPos {
613    /// Creates a new global position.
614    #[must_use]
615    pub const fn new(dimension: Identifier, pos: BlockPos) -> Self {
616        Self { dimension, pos }
617    }
618}
619
620impl ReadFrom for GlobalPos {
621    fn read(data: &mut Cursor<&[u8]>) -> io::Result<Self> {
622        Ok(Self {
623            dimension: <Identifier as ReadFrom>::read(data)?,
624            pos: BlockPos::read(data)?,
625        })
626    }
627}
628
629impl WriteTo for GlobalPos {
630    fn write(&self, writer: &mut impl Write) -> io::Result<()> {
631        self.dimension.write(writer)?;
632        self.pos.write(writer)
633    }
634}
635
636/// A chunk section position (16x16x16 region).
637#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
638pub struct SectionPos(pub IVec3);
639
640impl SectionPos {
641    const SECTION_BITS: i32 = 4;
642    const SECTION_SIZE: i32 = 1 << Self::SECTION_BITS; // 16
643    pub(super) const SECTION_MASK: i32 = Self::SECTION_SIZE - 1; // 15
644
645    /// Creates a new `SectionPos` from section coordinates.
646    #[must_use]
647    pub const fn new(x: i32, y: i32, z: i32) -> Self {
648        Self(IVec3::new(x, y, z))
649    }
650
651    /// Converts a block coordinate to a section coordinate.
652    #[must_use]
653    #[inline]
654    pub const fn block_to_section_coord(block_coord: i32) -> i32 {
655        block_coord >> Self::SECTION_BITS
656    }
657
658    /// Creates a `SectionPos` from a `BlockPos`.
659    #[must_use]
660    pub const fn from_block_pos(pos: BlockPos) -> Self {
661        Self::new(
662            Self::block_to_section_coord(pos.0.x),
663            Self::block_to_section_coord(pos.0.y),
664            Self::block_to_section_coord(pos.0.z),
665        )
666    }
667
668    /// Creates a `SectionPos` containing the given floating-point world position.
669    #[must_use]
670    pub fn from_entity_pos(pos: DVec3) -> Self {
671        Self::from_block_pos(BlockPos::from(pos))
672    }
673
674    /// Gets the X coordinate.
675    #[must_use]
676    pub const fn x(&self) -> i32 {
677        self.0.x
678    }
679
680    /// Gets the Y coordinate.
681    #[must_use]
682    pub const fn y(&self) -> i32 {
683        self.0.y
684    }
685
686    /// Gets the Z coordinate.
687    #[must_use]
688    pub const fn z(&self) -> i32 {
689        self.0.z
690    }
691
692    /// Converts section-relative coordinates to an absolute block X coordinate.
693    #[must_use]
694    pub const fn relative_to_block_x(&self, relative: PackedSectionBlockPos) -> i32 {
695        (self.0.x << Self::SECTION_BITS) + relative.x() as i32
696    }
697
698    /// Converts section-relative coordinates to an absolute block Y coordinate.
699    #[must_use]
700    pub const fn relative_to_block_y(&self, relative: PackedSectionBlockPos) -> i32 {
701        (self.0.y << Self::SECTION_BITS) + relative.y() as i32
702    }
703
704    /// Converts section-relative coordinates to an absolute block Z coordinate.
705    #[must_use]
706    pub const fn relative_to_block_z(&self, relative: PackedSectionBlockPos) -> i32 {
707        (self.0.z << Self::SECTION_BITS) + relative.z() as i32
708    }
709
710    /// Packs a block position into a section-relative offset.
711    /// Format: (x << 8) | (z << 4) | y (each coordinate masked to 4 bits)
712    #[must_use]
713    #[inline]
714    pub const fn section_relative_pos(pos: BlockPos) -> PackedSectionBlockPos {
715        PackedSectionBlockPos::from_block_pos(pos)
716    }
717
718    /// Converts a section-relative packed position back to a block position.
719    #[must_use]
720    pub const fn relative_to_block_pos(&self, relative: PackedSectionBlockPos) -> BlockPos {
721        BlockPos(IVec3::new(
722            self.relative_to_block_x(relative),
723            self.relative_to_block_y(relative),
724            self.relative_to_block_z(relative),
725        ))
726    }
727}
728
729impl ReadFrom for SectionPos {
730    fn read(data: &mut Cursor<&[u8]>) -> io::Result<Self> {
731        Ok(<PackedSectionPos as ReadFrom>::read(data)?.into())
732    }
733}
734
735impl WriteTo for SectionPos {
736    fn write(&self, writer: &mut impl Write) -> io::Result<()> {
737        PackedSectionPos::from(*self).write(writer)
738    }
739}