Skip to main content

steel_core/chunk/
player_chunk_view.rs

1use std::cmp::{max, min};
2
3use steel_utils::ChunkPos;
4
5/// A view of chunks around a center chunk.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub struct PlayerChunkView {
8    /// The center of the view.
9    pub center: ChunkPos,
10    /// The view distance in chunks.
11    pub view_distance: u8,
12}
13
14impl PlayerChunkView {
15    /// Creates a new empty `ChunkTrackingView`.
16    #[must_use]
17    pub const fn empty() -> Self {
18        Self {
19            center: ChunkPos::new(0, 0),
20            view_distance: 0,
21        }
22    }
23
24    /// Creates a new `ChunkTrackingView` with the given center and view distance.
25    #[must_use]
26    pub const fn new(center: ChunkPos, view_distance: u8) -> Self {
27        Self {
28            center,
29            view_distance,
30        }
31    }
32
33    fn min_x(&self) -> i32 {
34        self.center.0.x - i32::from(self.view_distance) - 1
35    }
36
37    fn max_x(&self) -> i32 {
38        self.center.0.x + i32::from(self.view_distance) + 1
39    }
40
41    fn min_z(&self) -> i32 {
42        self.center.0.y - i32::from(self.view_distance) - 1
43    }
44
45    fn max_z(&self) -> i32 {
46        self.center.0.y + i32::from(self.view_distance) + 1
47    }
48
49    /// Checks if the given chunk position is within the view.
50    #[must_use]
51    pub fn contains(&self, pos: ChunkPos) -> bool {
52        Self::is_within_distance(
53            self.center.0.x,
54            self.center.0.y,
55            i32::from(self.view_distance),
56            pos.0.x,
57            pos.0.y,
58            true,
59        )
60    }
61
62    /// Checks if a chunk at `(chunk_x, chunk_z)` is within the view distance of `(center_x, center_z)`.
63    #[must_use]
64    pub fn is_within_distance(
65        center_x: i32,
66        center_z: i32,
67        view_distance: i32,
68        chunk_x: i32,
69        chunk_z: i32,
70        include_neighbors: bool,
71    ) -> bool {
72        let buffer_range = if include_neighbors { 2 } else { 1 };
73        let delta_x = i64::from(max(0, (chunk_x - center_x).abs() - buffer_range));
74        let delta_z = i64::from(max(0, (chunk_z - center_z).abs() - buffer_range));
75        let distance_squared = delta_x * delta_x + delta_z * delta_z;
76        let radius_squared = i64::from(view_distance) * i64::from(view_distance);
77        distance_squared < radius_squared
78    }
79
80    /// Iterates over all chunks in the view.
81    pub fn for_each<F>(&self, mut f: F)
82    where
83        F: FnMut(ChunkPos),
84    {
85        for x in self.min_x()..=self.max_x() {
86            for z in self.min_z()..=self.max_z() {
87                let pos = ChunkPos::new(x, z);
88                if self.contains(pos) {
89                    f(pos);
90                }
91            }
92        }
93    }
94
95    fn square_intersects(&self, other: &Self) -> bool {
96        self.min_x() <= other.max_x()
97            && self.max_x() >= other.min_x()
98            && self.min_z() <= other.max_z()
99            && self.max_z() >= other.min_z()
100    }
101
102    /// Calculates the difference between two views, calling `on_added` for chunks in the new view but not the old,
103    /// and `on_removed` for chunks in the old view but not the new.
104    pub fn difference<T>(
105        old: &Self,
106        new: &Self,
107        mut on_added: impl FnMut(ChunkPos, &mut T),
108        mut on_removed: impl FnMut(ChunkPos, &mut T),
109        data: &mut T,
110    ) {
111        if old == new {
112            return;
113        }
114
115        if old.square_intersects(new) {
116            let min_x = min(old.min_x(), new.min_x());
117            let min_z = min(old.min_z(), new.min_z());
118            let max_x = max(old.max_x(), new.max_x());
119            let max_z = max(old.max_z(), new.max_z());
120
121            for x in min_x..=max_x {
122                for z in min_z..=max_z {
123                    let pos = ChunkPos::new(x, z);
124                    let saw = old.contains(pos);
125                    let sees = new.contains(pos);
126                    if saw != sees {
127                        if sees {
128                            on_added(pos, data);
129                        } else {
130                            on_removed(pos, data);
131                        }
132                    }
133                }
134            }
135        } else {
136            old.for_each(|pos| on_removed(pos, data));
137            new.for_each(|pos| on_added(pos, data));
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn test_contains() {
148        let view = PlayerChunkView::new(ChunkPos::new(0, 0), 2);
149
150        // (0,0) is center, should be contained
151        assert!(view.contains(ChunkPos::new(0, 0)));
152
153        // view distance 2 + buffer 2 = 4 (actually logic is slightly different)
154        // radius_squared = 2*2 = 4
155        // delta = max(0, dist - 2)
156        // if dist = 3, delta = 1. 1*1 = 1 < 4. True.
157        // if dist = 4, delta = 2. 2*2 = 4 == 4. False.
158
159        // Check neighbors logic
160        assert!(view.contains(ChunkPos::new(3, 0))); // dist 3 -> delta 1 -> 1 < 4 -> ok
161        assert!(!view.contains(ChunkPos::new(4, 0))); // dist 4 -> delta 2 -> 4 < 4 -> false
162    }
163
164    #[test]
165    fn test_difference() {
166        let mut added = Vec::new();
167        let mut removed = Vec::new();
168
169        let old_view = PlayerChunkView::new(ChunkPos::new(0, 0), 2);
170        let new_view = PlayerChunkView::new(ChunkPos::new(1, 0), 2);
171
172        PlayerChunkView::difference(
173            &old_view,
174            &new_view,
175            |p, ()| added.push(p),
176            |p, ()| removed.push(p),
177            &mut (),
178        );
179
180        // Just verify something happened
181        assert!(!added.is_empty());
182        assert!(!removed.is_empty());
183
184        // Verify intersection logic worked by ensuring we have balanced adds/removes for shift
185        // (Rough check)
186    }
187
188    #[test]
189    fn test_disjoint_difference() {
190        let mut added = Vec::new();
191        let mut removed = Vec::new();
192
193        let old_view = PlayerChunkView::new(ChunkPos::new(0, 0), 2);
194        let new_view = PlayerChunkView::new(ChunkPos::new(100, 100), 2); // far away
195
196        PlayerChunkView::difference(
197            &old_view,
198            &new_view,
199            |p, ()| added.push(p),
200            |p, ()| removed.push(p),
201            &mut (),
202        );
203
204        // Should be full remove and full add
205        // Count for view distance 2:
206        // iterate -2-1..2+1 = -3..3 range (7x7 square roughly), filtered by circle
207        let mut count = 0;
208        old_view.for_each(|_| count += 1);
209
210        assert_eq!(removed.len(), count);
211        assert_eq!(added.len(), count);
212    }
213}