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::{chunk_holder::ChunkHolder, section::ChunkSection, status::ChunkStatus};
9
10use super::World;
11
12pub(crate) const MAX_BLOCK_REGION_WORKSET_SLOTS: usize = 64;
14
15#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub(crate) struct BlockRegionBounds {
18 min: BlockPos,
19 max: BlockPos,
20}
21
22impl BlockRegionBounds {
23 #[must_use]
25 pub(crate) const fn from_corners(first: BlockPos, second: BlockPos) -> Self {
26 Self {
27 min: BlockPos::min(first, second),
28 max: BlockPos::max(first, second),
29 }
30 }
31
32 #[must_use]
33 const fn contains(self, pos: BlockPos) -> bool {
34 pos.x() >= self.min.x()
35 && pos.y() >= self.min.y()
36 && pos.z() >= self.min.z()
37 && pos.x() <= self.max.x()
38 && pos.y() <= self.max.y()
39 && pos.z() <= self.max.z()
40 }
41}
42
43struct BlockRegionWorkset {
44 bounds: BlockRegionBounds,
45 chunks: SmallVec<[Option<Arc<ChunkHolder>>; 4]>,
46 min_chunk_x: i32,
47 min_chunk_z: i32,
48 chunk_z_count: usize,
49 min_section_y: i32,
50 section_y_count: usize,
51 world_min_y: i32,
52 world_max_y: i32,
53}
54
55impl BlockRegionWorkset {
56 fn try_new(world: &World, bounds: BlockRegionBounds) -> Option<Self> {
57 let min_chunk_x =
58 SectionPos::block_to_section_coord(bounds.min.x()).max(-ChunkPos::MAX_COORDINATE_VALUE);
59 let max_chunk_x =
60 SectionPos::block_to_section_coord(bounds.max.x()).min(ChunkPos::MAX_COORDINATE_VALUE);
61 let min_chunk_z =
62 SectionPos::block_to_section_coord(bounds.min.z()).max(-ChunkPos::MAX_COORDINATE_VALUE);
63 let max_chunk_z =
64 SectionPos::block_to_section_coord(bounds.max.z()).min(ChunkPos::MAX_COORDINATE_VALUE);
65
66 let chunk_width = inclusive_count(min_chunk_x, max_chunk_x);
67 let chunk_depth = inclusive_count(min_chunk_z, max_chunk_z);
68 let world_min_y = world.get_min_y();
69 let world_max_y = world.get_max_y();
70 let min_section_y = SectionPos::block_to_section_coord(bounds.min.y().max(world_min_y));
71 let max_section_y = SectionPos::block_to_section_coord(bounds.max.y().min(world_max_y));
72 let section_y_count = inclusive_count(min_section_y, max_section_y);
73
74 let chunk_count = chunk_width.checked_mul(chunk_depth)?;
75 let section_slot_count = chunk_count.checked_mul(section_y_count)?;
76 let workset_slot_count = chunk_count.checked_add(section_slot_count)?;
77 if workset_slot_count > MAX_BLOCK_REGION_WORKSET_SLOTS {
78 return None;
79 }
80
81 let mut chunks = SmallVec::new();
82 for chunk_x_offset in 0..chunk_width {
83 let chunk_x = min_chunk_x + chunk_x_offset as i32;
84 for chunk_z_offset in 0..chunk_depth {
85 let chunk_z = min_chunk_z + chunk_z_offset as i32;
86 chunks.push(
87 world
88 .chunk_map
89 .active_full_chunk_holder(ChunkPos::new(chunk_x, chunk_z)),
90 );
91 }
92 }
93
94 Some(Self {
95 bounds,
96 chunks,
97 min_chunk_x,
98 min_chunk_z,
99 chunk_z_count: chunk_depth,
100 min_section_y,
101 section_y_count,
102 world_min_y,
103 world_max_y,
104 })
105 }
106
107 fn with_read<R>(&self, f: impl FnOnce(&BlockRegionRead<'_>) -> R) -> R {
108 let mut sections = SmallVec::new();
109
110 for holder in &self.chunks {
113 let chunk = holder
114 .as_ref()
115 .and_then(|holder| holder.try_chunk(ChunkStatus::Full));
116 for section_y_offset in 0..self.section_y_count {
117 let guard = chunk.and_then(|chunk| {
118 let section_y = self.min_section_y + section_y_offset as i32;
119 let section_index = usize::try_from(
120 section_y - SectionPos::block_to_section_coord(self.world_min_y),
121 )
122 .ok()?;
123 chunk
124 .sections()
125 .sections
126 .get(section_index)
127 .map(|section| section.read())
128 });
129 sections.push(guard);
130 }
131 }
132
133 let read = BlockRegionRead {
134 bounds: self.bounds,
135 sections,
136 min_chunk_x: self.min_chunk_x,
137 min_chunk_z: self.min_chunk_z,
138 chunk_z_count: self.chunk_z_count,
139 min_section_y: self.min_section_y,
140 section_y_count: self.section_y_count,
141 world_min_y: self.world_min_y,
142 world_max_y: self.world_max_y,
143 air: REGISTRY.blocks.get_base_state_id(&vanilla_blocks::AIR),
144 void_air: REGISTRY.blocks.get_base_state_id(&vanilla_blocks::VOID_AIR),
145 };
146 f(&read)
147 }
148}
149
150pub(crate) struct BlockSectionRead<'a> {
152 section_pos: SectionPos,
153 section: &'a ChunkSection,
154}
155
156impl BlockSectionRead<'_> {
157 #[must_use]
159 pub(crate) fn get_block_state(&self, pos: BlockPos) -> Option<BlockStateId> {
160 if SectionPos::from_block_pos(pos) != self.section_pos {
161 return None;
162 }
163 Some(self.section.states.get(
164 (pos.x() & 15) as usize,
165 (pos.y() & 15) as usize,
166 (pos.z() & 15) as usize,
167 ))
168 }
169}
170
171pub(crate) struct BlockRegionRead<'a> {
173 bounds: BlockRegionBounds,
174 sections: SmallVec<[Option<RwLockReadGuard<'a, ChunkSection>>; 8]>,
175 min_chunk_x: i32,
176 min_chunk_z: i32,
177 chunk_z_count: usize,
178 min_section_y: i32,
179 section_y_count: usize,
180 world_min_y: i32,
181 world_max_y: i32,
182 air: BlockStateId,
183 void_air: BlockStateId,
184}
185
186impl BlockRegionRead<'_> {
187 #[must_use]
192 pub(crate) fn get_block_state(&self, pos: BlockPos) -> Option<BlockStateId> {
193 if !self.bounds.contains(pos) {
194 return None;
195 }
196 if pos.y() < self.world_min_y
197 || pos.y() > self.world_max_y
198 || !ChunkPos::is_valid(
199 SectionPos::block_to_section_coord(pos.x()),
200 SectionPos::block_to_section_coord(pos.z()),
201 )
202 {
203 return Some(self.void_air);
204 }
205
206 let section_pos = SectionPos::from_block_pos(pos);
207 let Some(section) = self.section(section_pos) else {
208 return Some(self.air);
209 };
210 section.get_block_state(pos)
211 }
212
213 #[must_use]
215 pub(crate) fn section(&self, section_pos: SectionPos) -> Option<BlockSectionRead<'_>> {
216 let chunk_x_offset =
217 usize::try_from(section_pos.x().checked_sub(self.min_chunk_x)?).ok()?;
218 let chunk_z_offset =
219 usize::try_from(section_pos.z().checked_sub(self.min_chunk_z)?).ok()?;
220 let section_y_offset =
221 usize::try_from(section_pos.y().checked_sub(self.min_section_y)?).ok()?;
222 if chunk_z_offset >= self.chunk_z_count || section_y_offset >= self.section_y_count {
223 return None;
224 }
225
226 let chunk_slot = chunk_x_offset
227 .checked_mul(self.chunk_z_count)?
228 .checked_add(chunk_z_offset)?;
229 let section_slot = chunk_slot
230 .checked_mul(self.section_y_count)?
231 .checked_add(section_y_offset)?;
232 let section = self.sections.get(section_slot)?.as_ref()?;
233 Some(BlockSectionRead {
234 section_pos,
235 section,
236 })
237 }
238}
239
240impl World {
241 pub(crate) fn try_with_block_region<R>(
253 &self,
254 bounds: BlockRegionBounds,
255 f: impl FnOnce(&BlockRegionRead<'_>) -> R,
256 ) -> Option<R> {
257 Some(BlockRegionWorkset::try_new(self, bounds)?.with_read(f))
258 }
259}
260
261fn inclusive_count(min: i32, max: i32) -> usize {
262 if min > max {
263 return 0;
264 }
265 usize::try_from(i64::from(max) - i64::from(min) + 1).unwrap_or(0)
266}
267
268#[cfg(test)]
269mod tests {
270 use steel_registry::vanilla_blocks;
271 use steel_utils::{BlockPos, ChunkPos, SectionPos, WorldAabb, types::UpdateFlags};
272
273 use crate::{
274 behavior::init_behaviors,
275 test_support::{fresh_test_world, insert_ready_full_chunk},
276 };
277
278 use super::{BlockRegionBounds, BlockRegionWorkset};
279
280 #[test]
281 fn region_reuses_section_reads_across_chunk_and_section_boundaries() {
282 let world = fresh_test_world("block_region_reads");
283 init_behaviors();
284 insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
285 insert_ready_full_chunk(&world, ChunkPos::new(1, 0));
286
287 let first = BlockPos::new(15, 64, 0);
288 let second = BlockPos::new(16, 80, 0);
289 assert!(world.set_block(
290 first,
291 vanilla_blocks::STONE.default_state(),
292 UpdateFlags::UPDATE_NONE,
293 ));
294 assert!(world.set_block(
295 second,
296 vanilla_blocks::DIRT.default_state(),
297 UpdateFlags::UPDATE_NONE,
298 ));
299
300 let below_world = BlockPos::new(15, world.get_min_y() - 1, 0);
301 let missing_chunk = BlockPos::new(32, 64, 0);
302 let bounds = BlockRegionBounds::from_corners(below_world, BlockPos::new(32, 80, 0));
303 world
304 .try_with_block_region(bounds, |region| {
305 assert_eq!(
306 region.get_block_state(first),
307 Some(vanilla_blocks::STONE.default_state())
308 );
309 assert_eq!(
310 region.get_block_state(second),
311 Some(vanilla_blocks::DIRT.default_state())
312 );
313 assert_eq!(
314 region.get_block_state(missing_chunk),
315 Some(vanilla_blocks::AIR.default_state())
316 );
317 assert_eq!(
318 region.get_block_state(below_world),
319 Some(vanilla_blocks::VOID_AIR.default_state())
320 );
321 assert_eq!(region.get_block_state(first.west()), None);
322
323 let Some(section) = region.section(SectionPos::from_block_pos(first)) else {
324 panic!("the first block's loaded section should be cached");
325 };
326 assert_eq!(
327 section.get_block_state(first),
328 Some(vanilla_blocks::STONE.default_state())
329 );
330 assert_eq!(section.get_block_state(second), None);
331 assert!(
332 region
333 .section(SectionPos::new(i32::MAX, i32::MAX, i32::MAX))
334 .is_none()
335 );
336 })
337 .expect("focused region should fit the bounded workset");
338
339 assert!(
340 !world.block_states_in_aabb_are_air(WorldAabb::new(15.0, 64.0, 0.0, 15.9, 64.9, 0.9,))
341 );
342 assert!(
343 world.block_states_in_aabb_are_air(WorldAabb::new(32.0, 64.0, 0.0, 32.9, 64.9, 0.9,))
344 );
345 }
346
347 #[test]
348 fn oversized_region_uses_streaming_reads() {
349 let world = fresh_test_world("oversized_block_region_reads");
350 init_behaviors();
351 insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
352
353 let first = BlockPos::new(0, 64, 0);
354 assert!(world.set_block(
355 first,
356 vanilla_blocks::STONE.default_state(),
357 UpdateFlags::UPDATE_NONE,
358 ));
359
360 let oversized_bounds =
361 BlockRegionBounds::from_corners(first, BlockPos::new(16 * 64, first.y(), first.z()));
362 assert!(BlockRegionWorkset::try_new(&world, oversized_bounds).is_none());
363 assert!(!world.block_states_in_aabb_are_air(WorldAabb::new(
364 f64::from(first.x()),
365 f64::from(first.y()),
366 f64::from(first.z()),
367 f64::from(16 * 64),
368 f64::from(first.y()),
369 f64::from(first.z()),
370 )));
371 }
372}