1use std::{
4 mem,
5 sync::{Arc, Weak},
6};
7
8use rustc_hash::{FxHashMap, FxHashSet};
9use steel_utils::{ChunkPos, locks::SyncMutex};
10
11use super::chunk_holder::{ChunkHolder, ChunkSaveDependency};
12
13const BLOCK_TICKING_FULL_COUNT: u8 = 9;
14const ENTITY_TICKING_FULL_COUNT: u8 = 25;
15
16#[derive(Default)]
21pub(crate) struct FullPublicationQueue {
22 pending: SyncMutex<Vec<FullPublication>>,
23}
24
25impl FullPublicationQueue {
26 pub(crate) fn publish(&self, holder: &Arc<ChunkHolder>) {
27 self.pending.lock().push(FullPublication {
28 pos: holder.get_pos(),
29 holder: Arc::downgrade(holder),
30 });
31 }
32
33 pub(crate) fn drain(&self) -> Vec<FullPublication> {
34 mem::take(&mut *self.pending.lock())
35 }
36}
37
38pub(crate) struct FullPublication {
39 pub(crate) pos: ChunkPos,
40 pub(crate) holder: Weak<ChunkHolder>,
41}
42
43#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
44pub(crate) struct FullNeighborhoodCounts {
45 pub(crate) block_ticking: u8,
46 pub(crate) entity_ticking: u8,
47}
48
49impl FullNeighborhoodCounts {
50 #[must_use]
51 pub(crate) const fn block_ticking_ready(self) -> bool {
52 self.block_ticking == BLOCK_TICKING_FULL_COUNT
53 }
54
55 #[must_use]
56 pub(crate) const fn entity_ticking_ready(self) -> bool {
57 self.entity_ticking == ENTITY_TICKING_FULL_COUNT
58 }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub(crate) enum FullNeighborhoodError {
63 CounterOverflow {
64 center: ChunkPos,
65 radius: u8,
66 count: u8,
67 },
68 CounterUnderflow {
69 center: ChunkPos,
70 radius: u8,
71 },
72 ContributorIdentityMismatch {
73 pos: ChunkPos,
74 },
75}
76
77struct PendingReadiness {
78 holder: Weak<ChunkHolder>,
79 _save_dependency: ChunkSaveDependency,
80}
81
82#[derive(Default)]
83pub(crate) struct FullNeighborhoodIndex {
84 contributors: FxHashMap<ChunkPos, Weak<ChunkHolder>>,
85 counts: FxHashMap<ChunkPos, FullNeighborhoodCounts>,
86 dirty_centers: FxHashSet<ChunkPos>,
87 pending_readiness: FxHashMap<ChunkPos, PendingReadiness>,
88}
89
90impl FullNeighborhoodIndex {
91 pub(crate) fn reconcile_contributor(
92 &mut self,
93 pos: ChunkPos,
94 holder: Option<&Arc<ChunkHolder>>,
95 ) -> Result<(), FullNeighborhoodError> {
96 let unchanged = match (self.contributors.get(&pos), holder) {
97 (Some(current), Some(holder)) => current.ptr_eq(&Arc::downgrade(holder)),
98 (None, None) => true,
99 (Some(_), None) | (None, Some(_)) => false,
100 };
101 if unchanged {
102 return Ok(());
103 }
104
105 if self.contributors.contains_key(&pos) {
106 self.remove_contributor(pos)?;
107 }
108 if let Some(holder) = holder {
109 self.add_contributor(pos, Arc::downgrade(holder))?;
110 }
111 Ok(())
112 }
113
114 pub(crate) fn remove_contributor_if_matches(
115 &mut self,
116 pos: ChunkPos,
117 holder: &Arc<ChunkHolder>,
118 ) -> Result<(), FullNeighborhoodError> {
119 let expected = Arc::downgrade(holder);
120 match self.contributors.get(&pos) {
121 None => Ok(()),
122 Some(current) if current.ptr_eq(&expected) => self.remove_contributor(pos),
123 Some(_) => Err(FullNeighborhoodError::ContributorIdentityMismatch { pos }),
124 }
125 }
126
127 fn add_contributor(
128 &mut self,
129 pos: ChunkPos,
130 holder: Weak<ChunkHolder>,
131 ) -> Result<(), FullNeighborhoodError> {
132 debug_assert!(!self.contributors.contains_key(&pos));
133 self.validate_increment(pos, 1, BLOCK_TICKING_FULL_COUNT)?;
134 self.validate_increment(pos, 2, ENTITY_TICKING_FULL_COUNT)?;
135
136 self.contributors.insert(pos, holder);
137 self.adjust_counts(pos, 1, true);
138 self.adjust_counts(pos, 2, true);
139 Ok(())
140 }
141
142 fn remove_contributor(&mut self, pos: ChunkPos) -> Result<(), FullNeighborhoodError> {
143 debug_assert!(self.contributors.contains_key(&pos));
144 self.validate_decrement(pos, 1)?;
145 self.validate_decrement(pos, 2)?;
146
147 self.contributors.remove(&pos);
148 self.adjust_counts(pos, 1, false);
149 self.adjust_counts(pos, 2, false);
150 Self::for_each_center(pos, 2, |center| {
151 if self
152 .counts
153 .get(¢er)
154 .is_some_and(|counts| counts.block_ticking == 0 && counts.entity_ticking == 0)
155 {
156 self.counts.remove(¢er);
157 }
158 });
159 Ok(())
160 }
161
162 fn validate_increment(
163 &self,
164 pos: ChunkPos,
165 radius: u8,
166 maximum: u8,
167 ) -> Result<(), FullNeighborhoodError> {
168 let mut result = Ok(());
169 Self::for_each_center(pos, radius, |center| {
170 if result.is_err() {
171 return;
172 }
173 let counts = self.counts.get(¢er).copied().unwrap_or_default();
174 let count = if radius == 1 {
175 counts.block_ticking
176 } else {
177 counts.entity_ticking
178 };
179 if count >= maximum {
180 result = Err(FullNeighborhoodError::CounterOverflow {
181 center,
182 radius,
183 count,
184 });
185 }
186 });
187 result
188 }
189
190 fn validate_decrement(&self, pos: ChunkPos, radius: u8) -> Result<(), FullNeighborhoodError> {
191 let mut result = Ok(());
192 Self::for_each_center(pos, radius, |center| {
193 if result.is_err() {
194 return;
195 }
196 let counts = self.counts.get(¢er).copied().unwrap_or_default();
197 let count = if radius == 1 {
198 counts.block_ticking
199 } else {
200 counts.entity_ticking
201 };
202 if count == 0 {
203 result = Err(FullNeighborhoodError::CounterUnderflow { center, radius });
204 }
205 });
206 result
207 }
208
209 fn adjust_counts(&mut self, pos: ChunkPos, radius: u8, increment: bool) {
210 Self::for_each_center(pos, radius, |center| {
211 let counts = self.counts.entry(center).or_default();
212 let count = if radius == 1 {
213 &mut counts.block_ticking
214 } else {
215 &mut counts.entity_ticking
216 };
217 if increment {
218 *count += 1;
219 } else {
220 *count -= 1;
221 }
222 self.dirty_centers.insert(center);
223 });
224 }
225
226 fn for_each_center(pos: ChunkPos, radius: u8, mut f: impl FnMut(ChunkPos)) {
227 let radius = i32::from(radius);
228 for dz in -radius..=radius {
229 for dx in -radius..=radius {
230 let Some(x) = pos.0.x.checked_add(dx) else {
231 continue;
232 };
233 let Some(z) = pos.0.y.checked_add(dz) else {
234 continue;
235 };
236 f(ChunkPos::new(x, z));
237 }
238 }
239 }
240
241 pub(crate) fn mark_dirty(&mut self, pos: ChunkPos) {
242 self.dirty_centers.insert(pos);
243 }
244
245 pub(crate) fn dirty_counts_snapshot(&self) -> Vec<(ChunkPos, FullNeighborhoodCounts)> {
246 self.dirty_centers
247 .iter()
248 .copied()
249 .map(|pos| (pos, self.counts.get(&pos).copied().unwrap_or_default()))
250 .collect()
251 }
252
253 pub(crate) fn take_dirty_counts(&mut self) -> Vec<(ChunkPos, FullNeighborhoodCounts)> {
254 self.dirty_centers
255 .drain()
256 .map(|pos| (pos, self.counts.get(&pos).copied().unwrap_or_default()))
257 .collect()
258 }
259
260 pub(crate) fn ensure_pending_readiness(&mut self, pos: ChunkPos, holder: &Arc<ChunkHolder>) {
261 let holder_weak = Arc::downgrade(holder);
262 if self
263 .pending_readiness
264 .get(&pos)
265 .is_some_and(|pending| pending.holder.ptr_eq(&holder_weak))
266 {
267 return;
268 }
269
270 self.pending_readiness.insert(
271 pos,
272 PendingReadiness {
273 holder: holder_weak,
274 _save_dependency: holder.add_save_dependency(),
275 },
276 );
277 }
278
279 pub(crate) fn clear_pending_readiness(&mut self, pos: ChunkPos) {
280 self.pending_readiness.remove(&pos);
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn full_square_counts_track_block_and_entity_neighborhoods() {
290 let mut index = FullNeighborhoodIndex::default();
291 for z in -2..=2 {
292 for x in -2..=2 {
293 index
294 .add_contributor(ChunkPos::new(x, z), Weak::new())
295 .expect("unique contributors stay within neighborhood bounds");
296 }
297 }
298
299 assert_eq!(
300 index.counts.get(&ChunkPos::new(0, 0)),
301 Some(&FullNeighborhoodCounts {
302 block_ticking: BLOCK_TICKING_FULL_COUNT,
303 entity_ticking: ENTITY_TICKING_FULL_COUNT,
304 })
305 );
306
307 index
308 .remove_contributor(ChunkPos::new(-2, -2))
309 .expect("existing contributor has matching counters");
310 assert_eq!(
311 index.counts.get(&ChunkPos::new(0, 0)),
312 Some(&FullNeighborhoodCounts {
313 block_ticking: BLOCK_TICKING_FULL_COUNT,
314 entity_ticking: ENTITY_TICKING_FULL_COUNT - 1,
315 })
316 );
317 }
318
319 #[test]
320 fn invalid_extreme_coordinates_do_not_overflow() {
321 let mut index = FullNeighborhoodIndex::default();
322 let pos = ChunkPos::new(i32::MAX, i32::MIN);
323
324 index
325 .add_contributor(pos, Weak::new())
326 .expect("representable neighboring centers should be counted");
327 index
328 .remove_contributor(pos)
329 .expect("the same representable centers should be decremented");
330
331 assert!(index.counts.is_empty());
332 }
333}