Skip to main content

steel_core/world/
block_region.rs

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
12/// Maximum combined chunk-holder and section slots acquired by one bulk region read.
13pub(crate) const MAX_BLOCK_REGION_WORKSET_SLOTS: usize = 64;
14
15/// Inclusive block bounds for one scoped region read.
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub(crate) struct BlockRegionBounds {
18    min: BlockPos,
19    max: BlockPos,
20}
21
22impl BlockRegionBounds {
23    /// Creates bounds containing both corners regardless of their order.
24    #[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        // This is the lock order for all block-region operations: chunk X, chunk Z,
111        // then section Y. A future write-region API must use the same order.
112        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
150/// A read-only view of one section already locked by a [`BlockRegionRead`].
151pub(crate) struct BlockSectionRead<'a> {
152    section_pos: SectionPos,
153    section: &'a ChunkSection,
154}
155
156impl BlockSectionRead<'_> {
157    /// Returns a state when `pos` belongs to this section.
158    #[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
171/// Scoped block-state reads over a rectangular set of prelocked sections.
172pub(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    /// Returns the block state at `pos`, or `None` when it is outside this region.
188    ///
189    /// Positions outside world bounds are void air. Missing Full chunks are air,
190    /// matching [`World::get_block_state`].
191    #[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    /// Returns a prelocked section view, or `None` for an unloaded or uncached section.
214    #[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    /// Acquires every loaded section intersecting a small `bounds` once and exposes a scoped read
242    /// view.
243    ///
244    /// Setup and lock count scale with every intersecting chunk and section, so this is an internal
245    /// primitive for bounded synchronous gameplay queries rather than arbitrary or
246    /// player-controlled regions.
247    ///
248    /// The callback must use [`BlockRegionRead`] for reads covered by `bounds` and must not perform
249    /// world writes. Re-entering a world read for a covered section can deadlock if a writer is
250    /// already waiting because the requested section read guards remain held until the callback
251    /// returns.
252    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}