1use std::sync::Arc;
2
3use parking_lot::RwLockReadGuard;
4use smallvec::SmallVec;
5use steel_registry::{REGISTRY, vanilla_blocks};
6use steel_utils::{BlockPos, BlockStateId, ChunkPos, SectionPos};
7
8use crate::chunk::{
9 chunk_holder::ChunkHolder, paletted_container::BlockPalette, section::ChunkSection,
10 status::ChunkStatus,
11};
12
13use super::World;
14
15pub(crate) const MAX_BLOCK_REGION_WORKSET_SLOTS: usize = 64;
17const SECTION_EDGE_BLOCKS: i32 = BlockPalette::SIZE as i32;
18const SECTION_LOCAL_COORDINATE_MASK: i32 = SECTION_EDGE_BLOCKS - 1;
19
20const fn section_local_coordinate(block_coordinate: i32) -> usize {
21 (block_coordinate & SECTION_LOCAL_COORDINATE_MASK) as usize
22}
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub(crate) struct BlockRegionBounds {
27 min: BlockPos,
28 max: BlockPos,
29}
30
31impl BlockRegionBounds {
32 #[must_use]
34 pub(crate) const fn from_corners(first: BlockPos, second: BlockPos) -> Self {
35 Self {
36 min: BlockPos::min(first, second),
37 max: BlockPos::max(first, second),
38 }
39 }
40
41 #[must_use]
43 pub(crate) const fn corners(self) -> (BlockPos, BlockPos) {
44 (self.min, self.max)
45 }
46
47 #[must_use]
48 const fn contains(self, pos: BlockPos) -> bool {
49 pos.x() >= self.min.x()
50 && pos.y() >= self.min.y()
51 && pos.z() >= self.min.z()
52 && pos.x() <= self.max.x()
53 && pos.y() <= self.max.y()
54 && pos.z() <= self.max.z()
55 }
56}
57
58struct BlockRegionWorkset {
59 bounds: BlockRegionBounds,
60 chunks: SmallVec<[Option<Arc<ChunkHolder>>; 4]>,
61 min_chunk_x: i32,
62 min_chunk_z: i32,
63 chunk_z_count: usize,
64 min_section_y: i32,
65 section_y_count: usize,
66 world_min_y: i32,
67 world_max_y: i32,
68}
69
70impl BlockRegionWorkset {
71 fn try_new(world: &World, bounds: BlockRegionBounds) -> Option<Self> {
72 let min_chunk_x =
73 SectionPos::block_to_section_coord(bounds.min.x()).max(-ChunkPos::MAX_COORDINATE_VALUE);
74 let max_chunk_x =
75 SectionPos::block_to_section_coord(bounds.max.x()).min(ChunkPos::MAX_COORDINATE_VALUE);
76 let min_chunk_z =
77 SectionPos::block_to_section_coord(bounds.min.z()).max(-ChunkPos::MAX_COORDINATE_VALUE);
78 let max_chunk_z =
79 SectionPos::block_to_section_coord(bounds.max.z()).min(ChunkPos::MAX_COORDINATE_VALUE);
80
81 let chunk_width = inclusive_count(min_chunk_x, max_chunk_x);
82 let chunk_depth = inclusive_count(min_chunk_z, max_chunk_z);
83 let world_min_y = world.get_min_y();
84 let world_max_y = world.get_max_y();
85 let min_section_y = SectionPos::block_to_section_coord(bounds.min.y().max(world_min_y));
86 let max_section_y = SectionPos::block_to_section_coord(bounds.max.y().min(world_max_y));
87 let section_y_count = inclusive_count(min_section_y, max_section_y);
88
89 let chunk_count = chunk_width.checked_mul(chunk_depth)?;
90 let section_slot_count = chunk_count.checked_mul(section_y_count)?;
91 let workset_slot_count = chunk_count.checked_add(section_slot_count)?;
92 if workset_slot_count > MAX_BLOCK_REGION_WORKSET_SLOTS {
93 return None;
94 }
95
96 let mut chunks = SmallVec::new();
97 for chunk_x_offset in 0..chunk_width {
98 let chunk_x = min_chunk_x + chunk_x_offset as i32;
99 for chunk_z_offset in 0..chunk_depth {
100 let chunk_z = min_chunk_z + chunk_z_offset as i32;
101 chunks.push(
102 world
103 .chunk_map
104 .active_full_chunk_holder(ChunkPos::new(chunk_x, chunk_z)),
105 );
106 }
107 }
108
109 Some(Self {
110 bounds,
111 chunks,
112 min_chunk_x,
113 min_chunk_z,
114 chunk_z_count: chunk_depth,
115 min_section_y,
116 section_y_count,
117 world_min_y,
118 world_max_y,
119 })
120 }
121
122 fn with_read<R>(&self, f: impl FnOnce(&BlockRegionRead<'_>) -> R) -> R {
123 let mut sections = SmallVec::new();
124
125 for holder in &self.chunks {
128 let chunk = holder
129 .as_ref()
130 .and_then(|holder| holder.try_chunk(ChunkStatus::Full));
131 for section_y_offset in 0..self.section_y_count {
132 let guard = chunk.and_then(|chunk| {
133 let section_y = self.min_section_y + section_y_offset as i32;
134 let section_index = usize::try_from(
135 section_y - SectionPos::block_to_section_coord(self.world_min_y),
136 )
137 .ok()?;
138 chunk
139 .sections()
140 .sections
141 .get(section_index)
142 .map(|section| section.read())
143 });
144 sections.push(guard);
145 }
146 }
147
148 let read = BlockRegionRead {
149 bounds: self.bounds,
150 sections,
151 min_chunk_x: self.min_chunk_x,
152 min_chunk_z: self.min_chunk_z,
153 chunk_z_count: self.chunk_z_count,
154 min_section_y: self.min_section_y,
155 section_y_count: self.section_y_count,
156 world_min_y: self.world_min_y,
157 world_max_y: self.world_max_y,
158 air: REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR),
159 void_air: REGISTRY.blocks.get_base_state_id(&vanilla_blocks::VOID_AIR),
160 };
161 f(&read)
162 }
163}
164
165pub(crate) struct BlockSectionRead<'a> {
167 section_pos: SectionPos,
168 section: &'a ChunkSection,
169}
170
171impl BlockSectionRead<'_> {
172 #[must_use]
174 pub(crate) fn get_block_state(&self, pos: BlockPos) -> Option<BlockStateId> {
175 if SectionPos::from_block_pos(pos) != self.section_pos {
176 return None;
177 }
178 Some(self.section.states().get(
179 section_local_coordinate(pos.x()),
180 section_local_coordinate(pos.y()),
181 section_local_coordinate(pos.z()),
182 ))
183 }
184
185 fn stable_air_box_is_clear(&self, min: BlockPos, max: BlockPos) -> bool {
186 debug_assert_eq!(SectionPos::from_block_pos(min), self.section_pos);
187 debug_assert_eq!(SectionPos::from_block_pos(max), self.section_pos);
188 self.section.stable_air_box_is_clear(
189 section_local_coordinate(min.x()),
190 section_local_coordinate(max.x()),
191 section_local_coordinate(min.y()),
192 section_local_coordinate(max.y()),
193 section_local_coordinate(min.z()),
194 section_local_coordinate(max.z()),
195 )
196 }
197}
198
199pub(crate) struct BlockRegionRead<'a> {
201 bounds: BlockRegionBounds,
202 sections: SmallVec<[Option<RwLockReadGuard<'a, ChunkSection>>; 8]>,
203 min_chunk_x: i32,
204 min_chunk_z: i32,
205 chunk_z_count: usize,
206 min_section_y: i32,
207 section_y_count: usize,
208 world_min_y: i32,
209 world_max_y: i32,
210 air: BlockStateId,
211 void_air: BlockStateId,
212}
213
214impl BlockRegionRead<'_> {
215 #[must_use]
220 pub(crate) fn get_block_state(&self, pos: BlockPos) -> Option<BlockStateId> {
221 if !self.bounds.contains(pos) {
222 return None;
223 }
224 if pos.y() < self.world_min_y
225 || pos.y() > self.world_max_y
226 || !ChunkPos::is_valid(
227 SectionPos::block_to_section_coord(pos.x()),
228 SectionPos::block_to_section_coord(pos.z()),
229 )
230 {
231 return Some(self.void_air);
232 }
233
234 let section_pos = SectionPos::from_block_pos(pos);
235 let Some(section) = self.section(section_pos) else {
236 return Some(self.air);
237 };
238 section.get_block_state(pos)
239 }
240
241 #[must_use]
243 pub(crate) fn has_complete_data(&self) -> bool {
244 self.sections.iter().all(Option::is_some)
245 }
246
247 #[must_use]
249 pub(crate) fn maybe_has_special_colliding_blocks(&self) -> bool {
250 self.sections
251 .iter()
252 .flatten()
253 .any(|section| section.maybe_has_special_colliding_blocks())
254 }
255
256 #[must_use]
258 pub(crate) fn maybe_has_extensible_collision_behavior(&self) -> bool {
259 self.sections
260 .iter()
261 .flatten()
262 .any(|section| section.maybe_has_extensible_collision_behavior())
263 }
264
265 #[must_use]
270 pub(crate) fn contains_only_stable_air(&self) -> bool {
271 if !self.has_complete_data() {
272 return false;
273 }
274
275 let (bounds_min, bounds_max) = self.bounds.corners();
276 let min_y = bounds_min.y().max(self.world_min_y);
277 let max_y = bounds_max.y().min(self.world_max_y);
278 let min_section_x =
279 SectionPos::block_to_section_coord(bounds_min.x()).max(-ChunkPos::MAX_COORDINATE_VALUE);
280 let max_section_x =
281 SectionPos::block_to_section_coord(bounds_max.x()).min(ChunkPos::MAX_COORDINATE_VALUE);
282 let min_section_z =
283 SectionPos::block_to_section_coord(bounds_min.z()).max(-ChunkPos::MAX_COORDINATE_VALUE);
284 let max_section_z =
285 SectionPos::block_to_section_coord(bounds_max.z()).min(ChunkPos::MAX_COORDINATE_VALUE);
286 if min_y > max_y || min_section_x > max_section_x || min_section_z > max_section_z {
287 return true;
288 }
289
290 let min_section_y = SectionPos::block_to_section_coord(min_y);
291 let max_section_y = SectionPos::block_to_section_coord(max_y);
292 for section_x in min_section_x..=max_section_x {
293 for section_z in min_section_z..=max_section_z {
294 for section_y in min_section_y..=max_section_y {
295 let section_pos = SectionPos::new(section_x, section_y, section_z);
296 let Some(section) = self.section(section_pos) else {
297 return false;
298 };
299 let section_min = BlockPos::new(
300 section_x * SECTION_EDGE_BLOCKS,
301 section_y * SECTION_EDGE_BLOCKS,
302 section_z * SECTION_EDGE_BLOCKS,
303 );
304 let section_max = BlockPos::new(
305 section_min.x() + SECTION_LOCAL_COORDINATE_MASK,
306 section_min.y() + SECTION_LOCAL_COORDINATE_MASK,
307 section_min.z() + SECTION_LOCAL_COORDINATE_MASK,
308 );
309 let clipped_min = BlockPos::max(
310 BlockPos::new(bounds_min.x(), min_y, bounds_min.z()),
311 section_min,
312 );
313 let clipped_max = BlockPos::min(
314 BlockPos::new(bounds_max.x(), max_y, bounds_max.z()),
315 section_max,
316 );
317 if !section.stable_air_box_is_clear(clipped_min, clipped_max) {
318 return false;
319 }
320 }
321 }
322 }
323 true
324 }
325
326 #[must_use]
328 pub(crate) fn section(&self, section_pos: SectionPos) -> Option<BlockSectionRead<'_>> {
329 let chunk_x_offset =
330 usize::try_from(section_pos.x().checked_sub(self.min_chunk_x)?).ok()?;
331 let chunk_z_offset =
332 usize::try_from(section_pos.z().checked_sub(self.min_chunk_z)?).ok()?;
333 let section_y_offset =
334 usize::try_from(section_pos.y().checked_sub(self.min_section_y)?).ok()?;
335 if chunk_z_offset >= self.chunk_z_count || section_y_offset >= self.section_y_count {
336 return None;
337 }
338
339 let chunk_slot = chunk_x_offset
340 .checked_mul(self.chunk_z_count)?
341 .checked_add(chunk_z_offset)?;
342 let section_slot = chunk_slot
343 .checked_mul(self.section_y_count)?
344 .checked_add(section_y_offset)?;
345 let section = self.sections.get(section_slot)?.as_ref()?;
346 Some(BlockSectionRead {
347 section_pos,
348 section,
349 })
350 }
351}
352
353impl World {
354 pub(crate) fn try_with_block_region<R>(
366 &self,
367 bounds: BlockRegionBounds,
368 f: impl FnOnce(&BlockRegionRead<'_>) -> R,
369 ) -> Option<R> {
370 Some(BlockRegionWorkset::try_new(self, bounds)?.with_read(f))
371 }
372}
373
374fn inclusive_count(min: i32, max: i32) -> usize {
375 if min > max {
376 return 0;
377 }
378 usize::try_from(i64::from(max) - i64::from(min) + 1).unwrap_or(0)
379}
380
381#[cfg(test)]
382mod tests {
383 use steel_registry::vanilla_blocks;
384 use steel_utils::{BlockPos, ChunkPos, SectionPos, WorldAabb, types::UpdateFlags};
385
386 use crate::{
387 behavior::init_behaviors,
388 test_support::{fresh_test_world, insert_ready_full_chunk},
389 };
390
391 use super::{BlockRegionBounds, BlockRegionWorkset, SECTION_EDGE_BLOCKS};
392
393 #[test]
394 #[expect(
395 clippy::redundant_closure_for_method_calls,
396 reason = "the method pointer cannot satisfy the region callback's higher-ranked lifetime"
397 )]
398 fn stable_air_region_requires_full_data_and_tracks_live_block_writes() {
399 let fixture = fresh_test_world("stable_air_region");
400 let world = &fixture.world;
401 init_behaviors();
402 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
403 insert_ready_full_chunk(world, ChunkPos::new(1, 0));
404 let first_chunk_max_x = SECTION_EDGE_BLOCKS - 1;
405 let second_chunk_min_x = SECTION_EDGE_BLOCKS;
406 let missing_chunk_min_x = SECTION_EDGE_BLOCKS * 2;
407 let test_y = 64;
408 let bounds = BlockRegionBounds::from_corners(
409 BlockPos::new(first_chunk_max_x, test_y, 0),
410 BlockPos::new(second_chunk_min_x, test_y, 0),
411 );
412
413 assert_eq!(
414 world.try_with_block_region(bounds, |region| region.contains_only_stable_air()),
415 Some(true)
416 );
417 assert!(world.set_block(
418 BlockPos::new(second_chunk_min_x, test_y, 0),
419 vanilla_blocks::STONE.default_state(),
420 UpdateFlags::UPDATE_NONE,
421 ));
422 assert_eq!(
423 world.try_with_block_region(bounds, |region| region.contains_only_stable_air()),
424 Some(false)
425 );
426 assert!(world.set_block(
427 BlockPos::new(second_chunk_min_x, test_y, 0),
428 vanilla_blocks::CAVE_AIR.default_state(),
429 UpdateFlags::UPDATE_NONE,
430 ));
431 assert_eq!(
432 world.try_with_block_region(bounds, |region| region.contains_only_stable_air()),
433 Some(true)
434 );
435
436 let missing = BlockRegionBounds::from_corners(
437 BlockPos::new(first_chunk_max_x, test_y, 0),
438 BlockPos::new(missing_chunk_min_x, test_y, 0),
439 );
440 assert_eq!(
441 world.try_with_block_region(missing, |region| region.contains_only_stable_air()),
442 Some(false)
443 );
444 }
445
446 #[test]
447 fn region_reuses_section_reads_across_chunk_and_section_boundaries() {
448 let world_fixture = fresh_test_world("block_region_reads");
449 let world = &world_fixture.world;
450 init_behaviors();
451 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
452 insert_ready_full_chunk(world, ChunkPos::new(1, 0));
453
454 let first = BlockPos::new(15, 64, 0);
455 let second = BlockPos::new(16, 80, 0);
456 assert!(world.set_block(
457 first,
458 vanilla_blocks::STONE.default_state(),
459 UpdateFlags::UPDATE_NONE,
460 ));
461 assert!(world.set_block(
462 second,
463 vanilla_blocks::DIRT.default_state(),
464 UpdateFlags::UPDATE_NONE,
465 ));
466
467 let below_world = BlockPos::new(15, world.get_min_y() - 1, 0);
468 let missing_chunk = BlockPos::new(32, 64, 0);
469 let bounds = BlockRegionBounds::from_corners(below_world, BlockPos::new(32, 80, 0));
470 world
471 .try_with_block_region(bounds, |region| {
472 assert_eq!(
473 region.get_block_state(first),
474 Some(vanilla_blocks::STONE.default_state())
475 );
476 assert_eq!(
477 region.get_block_state(second),
478 Some(vanilla_blocks::DIRT.default_state())
479 );
480 assert_eq!(
481 region.get_block_state(missing_chunk),
482 Some(vanilla_blocks::AIR.default_state())
483 );
484 assert!(!region.has_complete_data());
485 assert!(!region.maybe_has_special_colliding_blocks());
486 assert_eq!(
487 region.get_block_state(below_world),
488 Some(vanilla_blocks::VOID_AIR.default_state())
489 );
490 assert_eq!(region.get_block_state(first.west()), None);
491
492 let Some(section) = region.section(SectionPos::from_block_pos(first)) else {
493 panic!("the first block's loaded section should be cached");
494 };
495 assert_eq!(
496 section.get_block_state(first),
497 Some(vanilla_blocks::STONE.default_state())
498 );
499 assert_eq!(section.get_block_state(second), None);
500 assert!(
501 region
502 .section(SectionPos::new(i32::MAX, i32::MAX, i32::MAX))
503 .is_none()
504 );
505 })
506 .expect("focused region should fit the bounded workset");
507
508 assert!(
509 !world.block_states_in_aabb_are_air(WorldAabb::new(15.0, 64.0, 0.0, 15.9, 64.9, 0.9,))
510 );
511 assert!(
512 world.block_states_in_aabb_are_air(WorldAabb::new(32.0, 64.0, 0.0, 32.9, 64.9, 0.9,))
513 );
514 }
515
516 #[test]
517 fn oversized_region_uses_streaming_reads() {
518 let world_fixture = fresh_test_world("oversized_block_region_reads");
519 let world = &world_fixture.world;
520 init_behaviors();
521 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
522
523 let first = BlockPos::new(0, 64, 0);
524 assert!(world.set_block(
525 first,
526 vanilla_blocks::STONE.default_state(),
527 UpdateFlags::UPDATE_NONE,
528 ));
529
530 let oversized_bounds =
531 BlockRegionBounds::from_corners(first, BlockPos::new(16 * 64, first.y(), first.z()));
532 assert!(BlockRegionWorkset::try_new(world, oversized_bounds).is_none());
533 assert!(!world.block_states_in_aabb_are_air(WorldAabb::new(
534 f64::from(first.x()),
535 f64::from(first.y()),
536 f64::from(first.z()),
537 f64::from(16 * 64),
538 f64::from(first.y()),
539 f64::from(first.z()),
540 )));
541 }
542}