Skip to main content

steel_core/worldgen/
region.rs

1//! Region access for chunk feature generation.
2//!
3//! Feature placement needs the center chunk plus its direct dependencies, while writes must
4//! stay inside the stage's block-state write radius. `WorldGenRegion` centralizes that
5//! contract so feature, structure, and vegetation code cannot bypass the chunk pyramid.
6
7use std::{
8    cell::RefCell,
9    sync::{Arc, Weak},
10    time::Instant,
11};
12
13use parking_lot::{RwLockReadGuard, RwLockWriteGuard};
14use simdnbt::owned::NbtCompound;
15use small_map::FxSmallMap;
16use steel_registry::{
17    REGISTRY, block_entity_type::BlockEntityTypeRef, blocks::BlockRef,
18    blocks::block_state_ext::BlockStateExt as _, blocks::properties::Direction,
19    blocks::shapes::SupportType, fluid::FluidRef, vanilla_blocks,
20};
21use steel_utils::random::RandomSource;
22use steel_utils::{
23    BlockPos, BlockStateId, ChunkPos, PackedChunkPos, PackedSectionBlockPos, SectionPos,
24    types::UpdateFlags,
25};
26use steel_worldgen::structure::{StructureReferenceMap, StructureStartMap};
27
28use crate::behavior::{BLOCK_BEHAVIORS, FLUID_BEHAVIORS};
29use crate::block_entity::{BLOCK_ENTITIES, SharedBlockEntity};
30use crate::chunk::{
31    Chunk,
32    chunk_generation_task::StaticCache2D,
33    chunk_holder::ChunkHolder,
34    chunk_pyramid::ChunkStep,
35    full_chunk::FullChunkRef,
36    heightmap::{Heightmap, HeightmapType},
37    section::{ChunkSection, SectionHolder, SectionWriteGuard, Sections},
38    status::ChunkStatus,
39};
40use crate::entity::SharedEntity;
41use crate::world::tick_scheduler::TickPriority;
42use crate::world::{LevelAccessor, LevelReader, ScheduledTickAccess, World};
43use crate::worldgen::feature::instrumentation::OreFeatureStats;
44use crate::worldgen::generator::context::WorldGenContext;
45
46/// Chunk-cache backed worldgen view for the current generation step.
47///
48/// This deliberately differs from vanilla's `WorldGenRegion` in one area: it only exposes
49/// chunks already collected by Steel's `StaticCache2D` and validated against `ChunkStep`.
50/// That keeps generation deterministic and makes missing dependency declarations fail at
51/// the region boundary instead of silently reading farther chunks.
52pub struct WorldGenRegion<'a> {
53    context: &'a WorldGenContext,
54    step: &'a ChunkStep,
55    cache: &'a StaticCache2D<Arc<ChunkHolder>>,
56    center: ChunkPos,
57    chunk_cache_radius: i32,
58    chunks: RefCell<Box<[Option<CachedWorldGenChunk<'a>>]>>,
59    worldgen_heightmaps: RefCell<Box<[CachedWorldgenHeightmaps]>>,
60    random: RandomSource,
61}
62
63/// Size of 8 is fine here because analysis showed that 91.7% only ever reach <=2 and the largest one ever grew was 6.
64/// Decreasing the size below 8 resulted in no change in speed.
65const BULK_SECTION_INLINE_CHUNKS: usize = 8;
66
67type BulkSectionChunkMap<'region> =
68    FxSmallMap<BULK_SECTION_INLINE_CHUNKS, i64, CachedWorldGenChunk<'region>>;
69
70/// Cached section-level access for feature code that mirrors vanilla `BulkSectionAccess`.
71///
72/// Vanilla exposes acquired `LevelChunkSection`s and lets some features mutate section-local
73/// block states directly. Steel keeps chunk views cached instead of section references because
74/// sections live behind Rust locks; callers still get the same direct section semantics without
75/// bypassing the worldgen dependency and write-radius checks.
76pub(crate) struct WorldGenBulkSectionAccess<'region, 'world, 'profile> {
77    region: &'region WorldGenRegion<'world>,
78    chunk_cache_radius: i32,
79    /// Sparse per-chunk cache. Features only touch the few chunks around their
80    /// origin, so a map avoids materializing the full radius² grid per placement.
81    chunks: BulkSectionChunkMap<'region>,
82    air: BlockStateId,
83    ore_profile: Option<&'profile RefCell<OreFeatureStats>>,
84}
85
86struct CachedWorldGenChunk<'region> {
87    holder: &'region ChunkHolder,
88    chunk: &'region Chunk,
89    verified_status: ChunkStatus,
90    access_mode: WorldGenAccessMode,
91}
92
93/// A generation dependency with access semantics captured when it was acquired.
94///
95/// The underlying chunk has stable storage, while this view preserves vanilla's
96/// stale-reference behavior across promotion: a view acquired before Full remains
97/// writable, and a newly acquired Full view behaves like `ImposterProtoChunk(false)`.
98#[derive(Clone, Copy)]
99pub(crate) struct WorldGenChunkRef<'a> {
100    holder: &'a ChunkHolder,
101    chunk: &'a Chunk,
102    access_mode: WorldGenAccessMode,
103}
104
105impl WorldGenChunkRef<'_> {
106    fn published_status(self) -> ChunkStatus {
107        let Some(status) = self.holder.published_status() else {
108            panic!("worldgen chunk data cannot lose its published status");
109        };
110        status
111    }
112
113    #[must_use]
114    pub(crate) const fn sections(&self) -> &Sections {
115        self.chunk.sections()
116    }
117
118    pub(crate) fn structure_starts_mut(&self) -> RwLockWriteGuard<'_, StructureStartMap> {
119        self.chunk.structure_starts_mut()
120    }
121
122    pub(crate) fn structure_references(&self) -> RwLockReadGuard<'_, StructureReferenceMap> {
123        self.chunk.structure_references()
124    }
125
126    fn full_imposter_height_at(
127        self,
128        heightmap_type: HeightmapType,
129        local_x: usize,
130        local_z: usize,
131    ) -> i32 {
132        let mapped_type = match heightmap_type {
133            HeightmapType::WorldSurfaceWg => HeightmapType::WorldSurface,
134            HeightmapType::OceanFloorWg => HeightmapType::OceanFloor,
135            other => other,
136        };
137        FullChunkRef::from_full_context(self.chunk).get_height(mapped_type, local_x, local_z)
138    }
139}
140
141/// The access mode is captured when a generation dependency is acquired.
142///
143/// This mirrors vanilla's distinction between a writable `ProtoChunk` and a
144/// read-only `ImposterProtoChunk(false)`. It must not be recomputed after
145/// acquisition: later holder publication will replace the holder view, but an
146/// in-flight generation region keeps the semantics it originally acquired.
147#[derive(Clone, Copy, PartialEq, Eq)]
148enum WorldGenAccessMode {
149    WritableProto,
150    ReadOnlyFull,
151}
152
153impl WorldGenAccessMode {
154    const fn capture(status: ChunkStatus) -> Self {
155        if matches!(status, ChunkStatus::Full) {
156            Self::ReadOnlyFull
157        } else {
158            Self::WritableProto
159        }
160    }
161
162    const fn allows_writes(self) -> bool {
163        matches!(self, Self::WritableProto)
164    }
165}
166
167#[derive(Default)]
168struct CachedWorldgenHeightmaps {
169    world_surface_wg: Option<Box<[i32; 256]>>,
170    ocean_floor_wg: Option<Box<[i32; 256]>>,
171}
172
173impl CachedWorldgenHeightmaps {
174    const fn supports(heightmap_type: HeightmapType) -> bool {
175        matches!(
176            heightmap_type,
177            HeightmapType::WorldSurfaceWg | HeightmapType::OceanFloorWg
178        )
179    }
180
181    fn get(&self, heightmap_type: HeightmapType) -> Option<&[i32; 256]> {
182        match heightmap_type {
183            HeightmapType::WorldSurfaceWg => self.world_surface_wg.as_deref(),
184            HeightmapType::OceanFloorWg => self.ocean_floor_wg.as_deref(),
185            _ => None,
186        }
187    }
188
189    fn set(&mut self, heightmap_type: HeightmapType, columns: Box<[i32; 256]>) {
190        match heightmap_type {
191            HeightmapType::WorldSurfaceWg => self.world_surface_wg = Some(columns),
192            HeightmapType::OceanFloorWg => self.ocean_floor_wg = Some(columns),
193            _ => {}
194        }
195    }
196}
197
198#[derive(Clone, Copy, PartialEq, Eq)]
199struct WritableSectionKey {
200    chunk_x: i32,
201    chunk_z: i32,
202    status: ChunkStatus,
203    section_index: usize,
204}
205
206impl<'a> WorldGenRegion<'a> {
207    /// Creates a new region over the chunks collected for a generation step.
208    #[must_use]
209    pub fn new(
210        context: &'a WorldGenContext,
211        step: &'a ChunkStep,
212        cache: &'a StaticCache2D<Arc<ChunkHolder>>,
213        center: ChunkPos,
214        random: RandomSource,
215    ) -> Self {
216        let chunk_cache_radius =
217            i32::try_from(step.direct_dependencies.get_radius()).unwrap_or(i32::MAX);
218        let chunk_cache_size = chunk_cache_radius.saturating_mul(2).saturating_add(1);
219        let chunk_cache_len =
220            usize::try_from(chunk_cache_size.saturating_mul(chunk_cache_size)).unwrap_or(0);
221        let chunks = (0..chunk_cache_len).map(|_| None).collect();
222        let worldgen_heightmaps = (0..chunk_cache_len)
223            .map(|_| CachedWorldgenHeightmaps::default())
224            .collect();
225
226        Self {
227            context,
228            step,
229            cache,
230            center,
231            chunk_cache_radius,
232            chunks: RefCell::new(chunks),
233            worldgen_heightmaps: RefCell::new(worldgen_heightmaps),
234            random,
235        }
236    }
237
238    /// Returns the center chunk being generated.
239    #[must_use]
240    pub const fn center(&self) -> ChunkPos {
241        self.center
242    }
243
244    /// Returns the random source exposed by vanilla `WorldGenRegion.getRandom()`.
245    pub const fn random_mut(&mut self) -> &mut RandomSource {
246        &mut self.random
247    }
248
249    /// Returns the minimum build height.
250    #[must_use]
251    pub const fn min_y(&self) -> i32 {
252        self.context.min_y()
253    }
254
255    /// Returns the world height.
256    #[must_use]
257    pub const fn height(&self) -> i32 {
258        self.context.height()
259    }
260
261    /// Returns the minimum Y coordinate used by vanilla `WorldGenerationContext`.
262    #[must_use]
263    pub fn generation_min_y(&self) -> i32 {
264        self.context.generation_min_y()
265    }
266
267    /// Returns the vertical generation depth used by vanilla `WorldGenerationContext`.
268    #[must_use]
269    pub fn generation_height(&self) -> i32 {
270        self.context.generation_height()
271    }
272
273    /// Returns this dimension's sea level.
274    #[must_use]
275    pub const fn sea_level(&self) -> i32 {
276        self.context.sea_level()
277    }
278
279    /// Returns the world seed.
280    #[must_use]
281    pub fn seed(&self) -> i64 {
282        self.context.world().seed()
283    }
284
285    /// Returns the weak world reference used by generated chunks and entities.
286    #[must_use]
287    pub fn weak_world(&self) -> Weak<World> {
288        self.context.weak_world()
289    }
290
291    /// Returns block light as seen by feature-stage worldgen.
292    ///
293    /// Vanilla routes this through the level light engine from `WorldGenRegion`, but block light
294    /// is not generated for the feature-stage proto chunks. Treating the region as dark keeps
295    /// snow and freeze checks aligned with vanilla feature placement.
296    #[must_use]
297    #[expect(
298        clippy::unused_self,
299        reason = "keeps light lookup callable through the region instance"
300    )]
301    pub const fn block_light_at(&self, _pos: BlockPos) -> u8 {
302        0
303    }
304
305    /// Returns the exclusive maximum build height.
306    #[must_use]
307    pub const fn max_y_exclusive(&self) -> i32 {
308        self.min_y() + self.height()
309    }
310
311    /// Checks if a Y coordinate is outside the build height.
312    #[must_use]
313    pub const fn is_outside_build_height(&self, y: i32) -> bool {
314        y < self.min_y() || y >= self.max_y_exclusive()
315    }
316
317    /// Returns the strongest status directly available for a chunk position in this step.
318    #[must_use]
319    pub const fn required_status_at(&self, chunk_x: i32, chunk_z: i32) -> Option<ChunkStatus> {
320        self.step
321            .direct_dependencies
322            .get(Self::chessboard_distance(self.center, chunk_x, chunk_z))
323    }
324
325    /// Returns whether block writes are allowed in the given chunk.
326    #[must_use]
327    pub const fn can_write_to_chunk(&self, chunk_x: i32, chunk_z: i32) -> bool {
328        let radius = self.step.block_state_write_radius;
329        radius >= 0
330            && (chunk_x - self.center.0.x).abs() <= radius
331            && (chunk_z - self.center.0.y).abs() <= radius
332    }
333
334    /// Gets a chunk if the step declares enough direct dependency status for it.
335    #[must_use]
336    pub(crate) fn try_chunk(
337        &self,
338        chunk_x: i32,
339        chunk_z: i32,
340        status: ChunkStatus,
341    ) -> Option<WorldGenChunkRef<'a>> {
342        let available_status = self.required_status_at(chunk_x, chunk_z)?;
343        if status > available_status {
344            return None;
345        }
346
347        let holder = self.cache.get(chunk_x, chunk_z);
348        let chunk = holder.try_chunk(available_status)?;
349        let published_status = holder.published_status()?;
350        Some(WorldGenChunkRef {
351            holder,
352            chunk,
353            access_mode: WorldGenAccessMode::capture(published_status),
354        })
355    }
356
357    /// Gets a chunk or panics if generation requested an undeclared dependency.
358    ///
359    /// # Panics
360    /// Panics if the chunk is outside this step's direct dependencies, if the requested
361    /// status is higher than the dependency contract, or if the holder has not reached
362    /// the declared status. Those cases indicate a chunk-pyramid or scheduler bug.
363    pub(crate) fn chunk(
364        &self,
365        chunk_x: i32,
366        chunk_z: i32,
367        status: ChunkStatus,
368    ) -> WorldGenChunkRef<'a> {
369        let Some(chunk) = self.try_chunk(chunk_x, chunk_z, status) else {
370            let available = self.required_status_at(chunk_x, chunk_z);
371            panic!(
372                "Worldgen requested chunk ({chunk_x}, {chunk_z}) at status {status:?}, \
373                 but the {:?} step only provides {available:?} at that distance from ({}, {})",
374                self.step.target_status, self.center.0.x, self.center.0.y
375            );
376        };
377
378        chunk
379    }
380
381    fn with_cached_chunk<R>(
382        &self,
383        chunk_x: i32,
384        chunk_z: i32,
385        status: ChunkStatus,
386        f: impl FnOnce(WorldGenChunkRef<'_>) -> R,
387    ) -> R {
388        let Some(cache_index) = self.chunk_cache_index(chunk_x, chunk_z) else {
389            let chunk = self.chunk(chunk_x, chunk_z, status);
390            return f(chunk);
391        };
392
393        let cache_needs_update = self.chunks.borrow().get(cache_index).is_none_or(|cached| {
394            cached
395                .as_ref()
396                .is_none_or(|cached| status > cached.verified_status)
397        });
398
399        if cache_needs_update {
400            let chunk = self.chunk(chunk_x, chunk_z, status);
401            let mut chunks = self.chunks.borrow_mut();
402            let Some(slot) = chunks.get_mut(cache_index) else {
403                panic!("Worldgen region cache index {cache_index} escaped its storage");
404            };
405            if let Some(cached) = slot {
406                cached.verified_status = status;
407            } else {
408                *slot = Some(CachedWorldGenChunk {
409                    access_mode: chunk.access_mode,
410                    holder: chunk.holder,
411                    chunk: chunk.chunk,
412                    verified_status: status,
413                });
414            }
415        }
416
417        let chunks = self.chunks.borrow();
418        let Some(Some(cached)) = chunks.get(cache_index) else {
419            panic!("Worldgen region cache failed to store chunk ({chunk_x}, {chunk_z})");
420        };
421        f(WorldGenChunkRef {
422            holder: cached.holder,
423            chunk: cached.chunk,
424            access_mode: cached.access_mode,
425        })
426    }
427
428    fn chunk_cache_index(&self, chunk_x: i32, chunk_z: i32) -> Option<usize> {
429        let radius = self.chunk_cache_radius;
430        let size = radius.checked_mul(2)?.checked_add(1)?;
431        let rel_x = chunk_x.checked_sub(self.center.0.x)?.checked_add(radius)?;
432        let rel_z = chunk_z.checked_sub(self.center.0.y)?.checked_add(radius)?;
433        if rel_x < 0 || rel_x >= size || rel_z < 0 || rel_z >= size {
434            return None;
435        }
436
437        usize::try_from(rel_z.checked_mul(size)?.checked_add(rel_x)?).ok()
438    }
439
440    /// Gets a block state through the region dependency contract.
441    ///
442    /// # Panics
443    /// Panics if the position's chunk is outside this step's direct dependencies.
444    #[must_use]
445    pub fn block_state(&self, pos: BlockPos) -> BlockStateId {
446        let chunk_x = SectionPos::block_to_section_coord(pos.x());
447        let chunk_z = SectionPos::block_to_section_coord(pos.z());
448        self.with_cached_chunk(chunk_x, chunk_z, ChunkStatus::Empty, |chunk| {
449            chunk.chunk.get_block_state(pos)
450        })
451    }
452
453    /// Gets a block entity through the region dependency contract.
454    ///
455    /// # Panics
456    /// Panics if the position's chunk is outside this step's direct dependencies.
457    #[must_use]
458    pub fn block_entity(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
459        let chunk_x = SectionPos::block_to_section_coord(pos.x());
460        let chunk_z = SectionPos::block_to_section_coord(pos.z());
461        self.with_cached_chunk(chunk_x, chunk_z, ChunkStatus::Empty, |chunk| {
462            let block_entity = match chunk.access_mode {
463                WorldGenAccessMode::ReadOnlyFull => {
464                    FullChunkRef::from_full_context(chunk.chunk).get_block_entity(pos)
465                }
466                WorldGenAccessMode::WritableProto => chunk
467                    .chunk
468                    .get_block_entity(pos)
469                    .or_else(|| chunk.chunk.promote_pending_block_entity(pos)),
470            };
471            if block_entity.is_none() && chunk.chunk.get_block_state(pos).has_block_entity() {
472                log::warn!("Tried to access a block entity before it was created at {pos:?}");
473            }
474            block_entity
475        })
476    }
477
478    /// Gets the biome id at quart coordinates through the region dependency contract.
479    ///
480    /// The vertical quart coordinate is clamped to the chunk's biome section range,
481    /// matching vanilla `ChunkAccess.getNoiseBiome`.
482    ///
483    /// # Panics
484    /// Panics if the position's chunk is outside this step's direct dependencies.
485    #[must_use]
486    pub fn noise_biome_id(&self, quart_x: i32, quart_y: i32, quart_z: i32) -> u16 {
487        let chunk_x = quart_x >> 2;
488        let chunk_z = quart_z >> 2;
489        let local_quart_x = (quart_x & 3) as usize;
490        let local_quart_z = (quart_z & 3) as usize;
491
492        self.with_cached_chunk(chunk_x, chunk_z, ChunkStatus::Biomes, |chunk| {
493            let sections = chunk.chunk.sections();
494            let (section_index, local_quart_y) =
495                Self::biome_quart_y_indices(self.min_y(), sections.sections.len(), quart_y);
496            let section = &sections.sections[section_index];
497
498            section
499                .read()
500                .biomes
501                .get(local_quart_x, local_quart_y, local_quart_z)
502        })
503    }
504
505    /// Sets a block state if the position is inside the step's write radius.
506    ///
507    /// Returns whether the write was accepted by the region. Positions outside the write
508    /// radius are rejected without touching chunk data, matching vanilla's
509    /// `WorldGenRegion.ensureCanWrite` gate.
510    ///
511    /// # Panics
512    /// Panics if a position inside the write radius is not covered by this step's
513    /// direct dependencies, or if the holder has not reached the declared status.
514    #[must_use]
515    pub fn set_block_state(&self, pos: BlockPos, state: BlockStateId, flags: UpdateFlags) -> bool {
516        let Some((chunk_x, chunk_z, status)) = self.writable_chunk_for_pos(pos, "write block")
517        else {
518            return false;
519        };
520        let changed_proto = self.with_cached_chunk(chunk_x, chunk_z, status, |chunk| {
521            if !chunk.access_mode.allows_writes() {
522                // Vanilla exposes an already-Full dependency through
523                // `ImposterProtoChunk(false)`, whose block writes are no-ops.
524                return false;
525            }
526            let old_state = chunk.chunk.set_block_state_for_generation(
527                chunk.published_status(),
528                pos,
529                state,
530                flags,
531            );
532            if state.has_block_entity() {
533                chunk.chunk.set_pending_block_entity_if_state(pos, state);
534            } else if old_state.is_some_and(|old_state| old_state.has_block_entity()) {
535                chunk.chunk.remove_block_entity_if_state(pos, state);
536            }
537            old_state.is_some()
538        });
539        if changed_proto {
540            self.invalidate_cached_worldgen_heightmaps(chunk_x, chunk_z);
541        }
542        if !flags.contains(UpdateFlags::UPDATE_KNOWN_SHAPE)
543            && let Some(postprocess_pos) = Self::postprocess_pos_for_state(state, pos)
544        {
545            self.mark_pos_for_postprocessing(postprocess_pos);
546        }
547        true
548    }
549
550    /// Mirrors the vanilla `Blocks` post-process hooks that can affect worldgen output.
551    ///
552    /// Vanilla hardcodes these callbacks in `Blocks.java`: mushrooms postprocess themselves,
553    /// while soul sand and magma blocks postprocess the block above.
554    fn postprocess_pos_for_state(state: BlockStateId, pos: BlockPos) -> Option<BlockPos> {
555        let block = state.get_block();
556        if block == &vanilla_blocks::BROWN_MUSHROOM || block == &vanilla_blocks::RED_MUSHROOM {
557            Some(pos)
558        } else if block == &vanilla_blocks::SOUL_SAND || block == &vanilla_blocks::MAGMA_BLOCK {
559            Some(pos.above())
560        } else {
561            None
562        }
563    }
564
565    /// Attaches block entity data at a writable worldgen position.
566    ///
567    /// This mirrors vanilla's feature paths that place a block first, then configure its block
568    /// entity. For unimplemented types, the fallback keeps the NBT intact for later save/load.
569    #[must_use]
570    pub fn set_block_entity_data(
571        &self,
572        pos: BlockPos,
573        block_entity_type: BlockEntityTypeRef,
574        state: BlockStateId,
575        nbt: NbtCompound,
576    ) -> bool {
577        let Some((chunk_x, chunk_z, status)) =
578            self.writable_chunk_for_pos(pos, "write block entity")
579        else {
580            return false;
581        };
582        if !block_entity_type.is_valid(state.get_block()) {
583            log::warn!(
584                "Worldgen block entity {} at {pos:?} does not accept block {}",
585                block_entity_type.key,
586                state.get_block().key,
587            );
588            return false;
589        }
590
591        self.with_cached_chunk(chunk_x, chunk_z, status, |chunk| {
592            if !chunk.access_mode.allows_writes() {
593                return false;
594            }
595            let entity = BLOCK_ENTITIES.create_and_load_owned_or_unimplemented(
596                block_entity_type,
597                chunk.chunk.level_weak(),
598                pos,
599                state,
600                nbt,
601            );
602            chunk.chunk.set_block_entity(entity)
603        })
604    }
605
606    /// Removes block entity data at a writable worldgen position.
607    #[must_use]
608    pub fn remove_block_entity(&self, pos: BlockPos) -> bool {
609        let Some((chunk_x, chunk_z, status)) =
610            self.writable_chunk_for_pos(pos, "remove block entity")
611        else {
612            return false;
613        };
614
615        self.with_cached_chunk(chunk_x, chunk_z, status, |chunk| {
616            if chunk.access_mode.allows_writes() {
617                chunk.chunk.remove_block_entity(pos);
618            }
619        });
620        true
621    }
622
623    /// Adds an entity to the chunk that owns its position.
624    ///
625    /// Vanilla `WorldGenRegion.addFreshEntity` does not call `ensureCanWrite`, so entity
626    /// insertion is allowed anywhere covered by the generation step's chunk dependencies.
627    #[must_use]
628    pub fn add_fresh_entity(&self, entity: SharedEntity) -> bool {
629        let pos = BlockPos::from(entity.position());
630        let (chunk_x, chunk_z, status) = self.dependency_chunk_for_pos(pos, "add entity");
631
632        self.with_cached_chunk(chunk_x, chunk_z, status, |chunk| {
633            if chunk.access_mode.allows_writes() {
634                chunk.chunk.add_entity(entity)
635            } else {
636                drop(entity);
637                true
638            }
639        })
640    }
641
642    /// Schedules a block tick in the chunk that owns the target position.
643    ///
644    /// Vanilla `WorldGenTickAccess` resolves the owning chunk directly and does not apply
645    /// `WorldGenRegion.ensureCanWrite`, so ticks can be recorded outside the block write radius
646    /// as long as the generation step declared the chunk dependency.
647    #[must_use]
648    pub fn schedule_block_tick(
649        &self,
650        pos: BlockPos,
651        block: BlockRef,
652        _delay: i32,
653        priority: TickPriority,
654    ) -> bool {
655        let (chunk_x, chunk_z, status) = self.dependency_chunk_for_pos(pos, "schedule block tick");
656        self.with_cached_chunk(chunk_x, chunk_z, status, |chunk| {
657            if chunk.access_mode.allows_writes() {
658                chunk.chunk.schedule_block_tick(pos, block, priority);
659            }
660        });
661        true
662    }
663
664    /// Schedules a block tick with vanilla's normal priority.
665    #[must_use]
666    pub fn schedule_block_tick_default(&self, pos: BlockPos, block: BlockRef, delay: i32) -> bool {
667        self.schedule_block_tick(pos, block, delay, TickPriority::Normal)
668    }
669
670    /// Schedules a fluid tick in the chunk that owns the target position.
671    ///
672    /// This mirrors vanilla tick scheduling and intentionally does not apply the block write radius.
673    #[must_use]
674    pub fn schedule_fluid_tick(
675        &self,
676        pos: BlockPos,
677        fluid: FluidRef,
678        _delay: i32,
679        priority: TickPriority,
680    ) -> bool {
681        let (chunk_x, chunk_z, status) = self.dependency_chunk_for_pos(pos, "schedule fluid tick");
682        self.with_cached_chunk(chunk_x, chunk_z, status, |chunk| {
683            if chunk.access_mode.allows_writes() {
684                chunk.chunk.schedule_fluid_tick(pos, fluid, priority);
685            }
686        });
687        true
688    }
689
690    /// Schedules a fluid tick with vanilla's normal priority.
691    #[must_use]
692    pub fn schedule_fluid_tick_default(&self, pos: BlockPos, fluid: FluidRef, delay: i32) -> bool {
693        self.schedule_fluid_tick(pos, fluid, delay, TickPriority::Normal)
694    }
695
696    /// Marks a position for vanilla proto-chunk postprocessing after full promotion.
697    ///
698    /// # Panics
699    /// Panics if the target chunk is outside this step's direct dependencies.
700    pub fn mark_pos_for_postprocessing(&self, pos: BlockPos) {
701        let chunk_x = SectionPos::block_to_section_coord(pos.x());
702        let chunk_z = SectionPos::block_to_section_coord(pos.z());
703        self.with_cached_chunk(chunk_x, chunk_z, ChunkStatus::Empty, |chunk| {
704            if chunk.access_mode.allows_writes() {
705                chunk.chunk.mark_pos_for_postprocessing(pos);
706            }
707        });
708    }
709
710    /// Gets the first available Y coordinate for a heightmap column.
711    ///
712    /// Mirrors vanilla `WorldGenRegion.getHeight`, which requests the target
713    /// chunk at `EMPTY` and then reads whichever generated status the step
714    /// dependency cache already holds for that chunk.
715    #[must_use]
716    pub fn height_at(&self, heightmap_type: HeightmapType, x: i32, z: i32) -> i32 {
717        let chunk_x = SectionPos::block_to_section_coord(x);
718        let chunk_z = SectionPos::block_to_section_coord(z);
719        let local_x = (x & 15) as usize;
720        let local_z = (z & 15) as usize;
721        self.with_cached_chunk(chunk_x, chunk_z, ChunkStatus::Empty, |chunk| {
722            match chunk.access_mode {
723                // Never copy a Full heightmap into the region cache. Runtime
724                // block changes update the canonical final maps, and vanilla's
725                // read-only imposter delegates each query to the wrapped chunk.
726                WorldGenAccessMode::ReadOnlyFull => {
727                    chunk.full_imposter_height_at(heightmap_type, local_x, local_z)
728                }
729                WorldGenAccessMode::WritableProto => self.cached_proto_height_at(
730                    chunk.chunk,
731                    heightmap_type,
732                    chunk_x,
733                    chunk_z,
734                    local_x + local_z * 16,
735                ),
736            }
737        })
738    }
739
740    fn cached_proto_height_at(
741        &self,
742        proto: &Chunk,
743        heightmap_type: HeightmapType,
744        chunk_x: i32,
745        chunk_z: i32,
746        column_index: usize,
747    ) -> i32 {
748        if !CachedWorldgenHeightmaps::supports(heightmap_type) {
749            return proto.generation_height_at(
750                heightmap_type,
751                column_index % 16,
752                column_index / 16,
753            );
754        }
755        let Some(cache_index) = self.chunk_cache_index(chunk_x, chunk_z) else {
756            return proto.generation_height_at(
757                heightmap_type,
758                column_index % 16,
759                column_index / 16,
760            );
761        };
762
763        {
764            let heightmaps = self.worldgen_heightmaps.borrow();
765            let Some(cached) = heightmaps.get(cache_index) else {
766                panic!("Worldgen heightmap cache index {cache_index} escaped its storage");
767            };
768            if let Some(columns) = cached.get(heightmap_type) {
769                return columns[column_index];
770            }
771        }
772
773        let columns = Self::proto_heightmap_columns(proto, heightmap_type);
774        let height = columns[column_index];
775        let mut heightmaps = self.worldgen_heightmaps.borrow_mut();
776        let Some(cached) = heightmaps.get_mut(cache_index) else {
777            panic!("Worldgen heightmap cache index {cache_index} escaped its storage");
778        };
779        cached.set(heightmap_type, columns);
780        height
781    }
782
783    fn proto_heightmap_columns(proto: &Chunk, heightmap_type: HeightmapType) -> Box<[i32; 256]> {
784        {
785            let heightmaps = proto.heightmaps.read();
786            if let Some(heightmap) = heightmaps.get(heightmap_type) {
787                return Self::copy_heightmap_columns(heightmap, proto.min_y());
788            }
789        }
790
791        let mut heightmaps = proto.heightmaps.write();
792        heightmaps.prime_from_sections(
793            &[heightmap_type],
794            proto.min_y(),
795            proto.height(),
796            &proto.sections.sections,
797        );
798        let Some(heightmap) = heightmaps.get(heightmap_type) else {
799            panic!("heightmap {heightmap_type:?} missing after priming");
800        };
801
802        Self::copy_heightmap_columns(heightmap, proto.min_y())
803    }
804
805    fn invalidate_cached_worldgen_heightmaps(&self, chunk_x: i32, chunk_z: i32) {
806        let Some(cache_index) = self.chunk_cache_index(chunk_x, chunk_z) else {
807            return;
808        };
809        let mut heightmaps = self.worldgen_heightmaps.borrow_mut();
810        let Some(cached) = heightmaps.get_mut(cache_index) else {
811            panic!("Worldgen heightmap cache index {cache_index} escaped its storage");
812        };
813        *cached = CachedWorldgenHeightmaps::default();
814    }
815
816    fn copy_heightmap_columns(heightmap: &Heightmap, min_y: i32) -> Box<[i32; 256]> {
817        let mut columns = Box::new([0; 256]);
818        for (index, &height) in heightmap.raw_data().iter().enumerate() {
819            columns[index] = i32::from(height) + min_y;
820        }
821        columns
822    }
823
824    pub(crate) fn bulk_section_access_for_ore<'profile>(
825        &self,
826        profile: Option<&'profile RefCell<OreFeatureStats>>,
827    ) -> WorldGenBulkSectionAccess<'_, 'a, 'profile> {
828        WorldGenBulkSectionAccess::new(self, profile)
829    }
830
831    fn writable_chunk_for_pos(
832        &self,
833        pos: BlockPos,
834        action: &str,
835    ) -> Option<(i32, i32, ChunkStatus)> {
836        let chunk_x = SectionPos::block_to_section_coord(pos.x());
837        let chunk_z = SectionPos::block_to_section_coord(pos.z());
838
839        if !self.can_write_to_chunk(chunk_x, chunk_z) {
840            log::error!(
841                "Worldgen attempted to {action} at ({}, {}, {}) outside {:?} write radius {} centered on ({}, {})",
842                pos.x(),
843                pos.y(),
844                pos.z(),
845                self.step.target_status,
846                self.step.block_state_write_radius,
847                self.center.0.x,
848                self.center.0.y,
849            );
850            return None;
851        }
852
853        let Some(status) = self.required_status_at(chunk_x, chunk_z) else {
854            panic!(
855                "Worldgen attempted to {action} at ({}, {}, {}) in chunk ({chunk_x}, {chunk_z}), \
856                 but {:?} declares no direct dependency for that chunk",
857                pos.x(),
858                pos.y(),
859                pos.z(),
860                self.step.target_status,
861            );
862        };
863
864        Some((chunk_x, chunk_z, status))
865    }
866
867    fn dependency_chunk_for_pos(&self, pos: BlockPos, action: &str) -> (i32, i32, ChunkStatus) {
868        let chunk_x = SectionPos::block_to_section_coord(pos.x());
869        let chunk_z = SectionPos::block_to_section_coord(pos.z());
870        let Some(status) = self.required_status_at(chunk_x, chunk_z) else {
871            panic!(
872                "Worldgen attempted to {action} at ({}, {}, {}) in chunk ({chunk_x}, {chunk_z}), \
873                 but {:?} declares no direct dependency for that chunk",
874                pos.x(),
875                pos.y(),
876                pos.z(),
877                self.step.target_status,
878            );
879        };
880
881        (chunk_x, chunk_z, status)
882    }
883
884    const fn chessboard_distance(center: ChunkPos, chunk_x: i32, chunk_z: i32) -> usize {
885        let dx = abs_diff(center.0.x, chunk_x);
886        let dz = abs_diff(center.0.y, chunk_z);
887        if dx > dz { dx as usize } else { dz as usize }
888    }
889
890    fn biome_quart_y_indices(min_y: i32, section_count: usize, quart_y: i32) -> (usize, usize) {
891        let Some(total_quart_y) = section_count.checked_mul(4) else {
892            panic!("Worldgen chunk section count {section_count} overflows biome quart range");
893        };
894        assert!(
895            total_quart_y > 0,
896            "Worldgen chunk must have at least one biome section"
897        );
898
899        let relative_quart_y = i64::from(quart_y) - i64::from(min_y >> 2);
900        let max_relative_quart_y = total_quart_y - 1;
901        let clamped_relative_quart_y = if relative_quart_y <= 0 {
902            0
903        } else {
904            usize::try_from(relative_quart_y).map_or(max_relative_quart_y, |relative| {
905                relative.min(max_relative_quart_y)
906            })
907        };
908
909        (clamped_relative_quart_y / 4, clamped_relative_quart_y & 3)
910    }
911}
912
913impl<'region, 'world, 'profile> WorldGenBulkSectionAccess<'region, 'world, 'profile> {
914    fn new(
915        region: &'region WorldGenRegion<'world>,
916        ore_profile: Option<&'profile RefCell<OreFeatureStats>>,
917    ) -> Self {
918        Self {
919            region,
920            chunk_cache_radius: region.chunk_cache_radius,
921            chunks: BulkSectionChunkMap::default(),
922            air: REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR),
923            ore_profile,
924        }
925    }
926
927    pub(crate) fn record_ore_candidate_position(&mut self) {
928        self.with_ore_profile(OreFeatureStats::record_candidate_position);
929    }
930
931    pub(crate) fn record_ore_unique_position(&mut self) {
932        self.with_ore_profile(OreFeatureStats::record_unique_position);
933    }
934
935    pub(crate) fn record_ore_write_allowed_position(&mut self) {
936        self.with_ore_profile(OreFeatureStats::record_write_allowed_position);
937    }
938
939    pub(crate) fn ore_target_block_state(&mut self, pos: BlockPos) -> BlockStateId {
940        self.with_ore_profile(OreFeatureStats::record_target_read);
941        self.block_state(pos)
942    }
943
944    pub(crate) fn ore_neighbor_block_state(&mut self, pos: BlockPos) -> BlockStateId {
945        self.with_ore_profile(OreFeatureStats::record_neighbor_read);
946        self.block_state(pos)
947    }
948
949    /// Replaces an ore target block after reading it under the section write lock.
950    ///
951    /// This is only suitable for ore paths that do not need neighbor reads while deciding
952    /// whether the replacement is allowed.
953    #[must_use]
954    pub(crate) fn replace_ore_target_block_state(
955        &mut self,
956        pos: BlockPos,
957        replacement: impl FnOnce(BlockStateId) -> Option<BlockStateId>,
958    ) -> bool {
959        self.with_ore_profile(OreFeatureStats::record_target_read);
960        let ore_profile = self.ore_profile;
961        let started_at = ore_profile.map(|_| Instant::now());
962        let Some(key) = self.writable_section_key(pos) else {
963            Self::record_ore_write_time(ore_profile, started_at);
964            return false;
965        };
966
967        let chunk = self.chunk(key.chunk_x, key.chunk_z, key.status);
968        if !chunk.access_mode.allows_writes() {
969            Self::record_ore_write_time(ore_profile, started_at);
970            return false;
971        }
972        let Some(section) = chunk.chunk.sections().sections.get(key.section_index) else {
973            panic!(
974                "Worldgen bulk section write at ({}, {}, {}) resolved missing section index {}",
975                pos.x(),
976                pos.y(),
977                pos.z(),
978                key.section_index
979            );
980        };
981
982        let mut section_guard = Self::ore_section_write_guard(ore_profile, section, key);
983
984        let local_x = Self::local_coord(pos.x());
985        let local_y = Self::local_coord(pos.y());
986        let local_z = Self::local_coord(pos.z());
987        let old_state = section_guard.states.get(local_x, local_y, local_z);
988        let Some(state) = replacement(old_state) else {
989            Self::record_ore_write_time(ore_profile, started_at);
990            return false;
991        };
992
993        let old_state = Self::set_bulk_block_state(
994            chunk.holder,
995            &mut section_guard,
996            local_x,
997            local_y,
998            local_z,
999            state,
1000        );
1001        Self::with_ore_profile_ref(ore_profile, OreFeatureStats::record_write);
1002        if old_state != state {
1003            chunk.chunk.mark_dirty();
1004        }
1005
1006        Self::record_ore_write_time(ore_profile, started_at);
1007        true
1008    }
1009
1010    /// Replaces already-filtered ore target positions that all belong to one section.
1011    pub(crate) fn replace_ore_target_block_states_in_section(
1012        &mut self,
1013        chunk_x: i32,
1014        chunk_z: i32,
1015        section_index: usize,
1016        positions: &[PackedSectionBlockPos],
1017        mut replacement: impl FnMut(BlockStateId) -> Option<BlockStateId>,
1018    ) -> u64 {
1019        let ore_profile = self.ore_profile;
1020        let started_at = ore_profile.map(|_| Instant::now());
1021        if !self.region.can_write_to_chunk(chunk_x, chunk_z) {
1022            Self::record_ore_write_time(ore_profile, started_at);
1023            return 0;
1024        }
1025        Self::with_ore_profile_ref(ore_profile, |profile| {
1026            profile.record_write_allowed_positions(positions.len() as u64);
1027        });
1028        let Some(status) = self.region.required_status_at(chunk_x, chunk_z) else {
1029            panic!(
1030                "Worldgen attempted to bulk write ore in chunk ({chunk_x}, {chunk_z}), \
1031                 but {:?} declares no direct dependency for that chunk",
1032                self.region.step.target_status,
1033            );
1034        };
1035        let key = WritableSectionKey {
1036            chunk_x,
1037            chunk_z,
1038            status,
1039            section_index,
1040        };
1041        let chunk = self.chunk(chunk_x, chunk_z, status);
1042        if !chunk.access_mode.allows_writes() {
1043            Self::record_ore_write_time(ore_profile, started_at);
1044            return 0;
1045        }
1046        let Some(section) = chunk.chunk.sections().sections.get(section_index) else {
1047            panic!(
1048                "Worldgen bulk section write in chunk ({chunk_x}, {chunk_z}) resolved missing section index {section_index}",
1049            );
1050        };
1051
1052        let mut section_guard = Self::ore_section_write_guard(ore_profile, section, key);
1053        let mut placed = 0_u64;
1054        let mut dirty = false;
1055
1056        if let Some(profile) = ore_profile {
1057            for &pos in positions {
1058                Self::with_ore_profile_ref(Some(profile), OreFeatureStats::record_target_read);
1059                let local_x = usize::from(pos.x());
1060                let local_y = usize::from(pos.y());
1061                let local_z = usize::from(pos.z());
1062                let old_state = section_guard.states.get(local_x, local_y, local_z);
1063                if let Some(state) = replacement(old_state) {
1064                    let old_state = Self::set_bulk_block_state(
1065                        chunk.holder,
1066                        &mut section_guard,
1067                        local_x,
1068                        local_y,
1069                        local_z,
1070                        state,
1071                    );
1072                    Self::with_ore_profile_ref(Some(profile), OreFeatureStats::record_write);
1073                    dirty |= old_state != state;
1074                    placed += 1;
1075                }
1076            }
1077        } else {
1078            for &pos in positions {
1079                let local_x = usize::from(pos.x());
1080                let local_y = usize::from(pos.y());
1081                let local_z = usize::from(pos.z());
1082                let old_state = section_guard.states.get(local_x, local_y, local_z);
1083                if let Some(state) = replacement(old_state) {
1084                    let old_state = Self::set_bulk_block_state(
1085                        chunk.holder,
1086                        &mut section_guard,
1087                        local_x,
1088                        local_y,
1089                        local_z,
1090                        state,
1091                    );
1092                    dirty |= old_state != state;
1093                    placed += 1;
1094                }
1095            }
1096        }
1097
1098        drop(section_guard);
1099        if dirty {
1100            chunk.chunk.mark_dirty();
1101        }
1102
1103        Self::record_ore_write_time(ore_profile, started_at);
1104        placed
1105    }
1106
1107    /// Reads a block state through cached section access.
1108    ///
1109    /// Out-of-height reads return air, matching vanilla `BulkSectionAccess.getBlockState`.
1110    #[must_use]
1111    pub(crate) fn block_state(&mut self, pos: BlockPos) -> BlockStateId {
1112        let ore_profile = self.ore_profile;
1113        let started_at = ore_profile.map(|_| Instant::now());
1114        let air = self.air;
1115        let Some(section_index) =
1116            Self::section_index(self.region.min_y(), self.region.height(), pos.y())
1117        else {
1118            Self::record_ore_read_time(ore_profile, started_at);
1119            return air;
1120        };
1121
1122        let chunk_x = SectionPos::block_to_section_coord(pos.x());
1123        let chunk_z = SectionPos::block_to_section_coord(pos.z());
1124        let chunk = self.chunk(chunk_x, chunk_z, ChunkStatus::Empty);
1125        let Some(section) = Self::section_for_read(chunk, section_index) else {
1126            Self::record_ore_read_time(ore_profile, started_at);
1127            return air;
1128        };
1129
1130        Self::with_ore_profile_ref(ore_profile, |profile| {
1131            profile.record_section_read_attempt(chunk_x, chunk_z, section_index);
1132        });
1133        let section_guard = if let Some(profile) = ore_profile {
1134            if let Some(guard) = section.try_read() {
1135                guard
1136            } else {
1137                if let Ok(mut profile) = profile.try_borrow_mut() {
1138                    profile.record_section_read_contention();
1139                }
1140                let wait_started_at = Instant::now();
1141                let guard = section.read();
1142                if let Ok(mut profile) = profile.try_borrow_mut() {
1143                    profile.record_read_contention_wait_time(wait_started_at.elapsed());
1144                }
1145                guard
1146            }
1147        } else {
1148            section.read()
1149        };
1150        if section_guard.states.has_only_air() {
1151            Self::record_ore_read_time(ore_profile, started_at);
1152            return air;
1153        }
1154
1155        let state = section_guard.states.get(
1156            Self::local_coord(pos.x()),
1157            Self::local_coord(pos.y()),
1158            Self::local_coord(pos.z()),
1159        );
1160        Self::record_ore_read_time(ore_profile, started_at);
1161        state
1162    }
1163
1164    /// Resolves the section exposed by vanilla `BulkSectionAccess`.
1165    ///
1166    /// A read-only `ImposterProtoChunk` exposes its own empty proto sections through
1167    /// `getSection`, even though ordinary chunk reads delegate to the wrapped Full chunk.
1168    fn section_for_read<'chunk>(
1169        chunk: &'chunk CachedWorldGenChunk<'_>,
1170        section_index: usize,
1171    ) -> Option<&'chunk SectionHolder> {
1172        if !chunk.access_mode.allows_writes() {
1173            return None;
1174        }
1175        chunk.chunk.sections().sections.get(section_index)
1176    }
1177
1178    /// Writes a block state directly to the containing section.
1179    ///
1180    /// This mirrors vanilla `BulkSectionAccess` by skipping heightmaps, neighbor updates,
1181    /// block entity callbacks, and other `WorldGenRegion.setBlock` side effects. Steel also
1182    /// defers section block counts until light initialization, matching the rest of its
1183    /// pre-light worldgen write paths.
1184    #[must_use]
1185    pub(crate) fn set_block_state(&mut self, pos: BlockPos, state: BlockStateId) -> bool {
1186        let ore_profile = self.ore_profile;
1187        let started_at = ore_profile.map(|_| Instant::now());
1188        let Some(key) = self.writable_section_key(pos) else {
1189            Self::record_ore_write_time(ore_profile, started_at);
1190            return false;
1191        };
1192
1193        let chunk = self.chunk(key.chunk_x, key.chunk_z, key.status);
1194        if !chunk.access_mode.allows_writes() {
1195            Self::record_ore_write_time(ore_profile, started_at);
1196            return true;
1197        }
1198        let Some(section) = chunk.chunk.sections().sections.get(key.section_index) else {
1199            panic!(
1200                "Worldgen bulk section write at ({}, {}, {}) resolved missing section index {}",
1201                pos.x(),
1202                pos.y(),
1203                pos.z(),
1204                key.section_index
1205            );
1206        };
1207
1208        let mut section_guard = Self::ore_section_write_guard(ore_profile, section, key);
1209        let old_state = Self::set_bulk_block_state(
1210            chunk.holder,
1211            &mut section_guard,
1212            Self::local_coord(pos.x()),
1213            Self::local_coord(pos.y()),
1214            Self::local_coord(pos.z()),
1215            state,
1216        );
1217        Self::with_ore_profile_ref(ore_profile, OreFeatureStats::record_write);
1218        if old_state != state {
1219            chunk.chunk.mark_dirty();
1220        }
1221
1222        Self::record_ore_write_time(ore_profile, started_at);
1223        true
1224    }
1225
1226    fn writable_section_key(&self, pos: BlockPos) -> Option<WritableSectionKey> {
1227        let (chunk_x, chunk_z, status) = self
1228            .region
1229            .writable_chunk_for_pos(pos, "bulk write block")?;
1230        let section_index =
1231            Self::section_index(self.region.min_y(), self.region.height(), pos.y())?;
1232        Some(WritableSectionKey {
1233            chunk_x,
1234            chunk_z,
1235            status,
1236            section_index,
1237        })
1238    }
1239
1240    fn set_bulk_block_state(
1241        holder: &ChunkHolder,
1242        section: &mut ChunkSection,
1243        local_x: usize,
1244        local_y: usize,
1245        local_z: usize,
1246        state: BlockStateId,
1247    ) -> BlockStateId {
1248        let Some(status) = holder.published_status() else {
1249            panic!("worldgen chunk data cannot lose its published status");
1250        };
1251        if status < ChunkStatus::InitializeLight {
1252            return section.set_block_state_for_generation(local_x, local_y, local_z, state);
1253        }
1254
1255        section.finalize_generation_counts_if_needed();
1256        section.set_block_state(local_x, local_y, local_z, state)
1257    }
1258
1259    fn ore_section_write_guard<'section>(
1260        ore_profile: Option<&RefCell<OreFeatureStats>>,
1261        section: &'section SectionHolder,
1262        key: WritableSectionKey,
1263    ) -> SectionWriteGuard<'section> {
1264        Self::with_ore_profile_ref(ore_profile, |profile| {
1265            profile.record_section_write_attempt(key.chunk_x, key.chunk_z, key.section_index);
1266        });
1267        if let Some(profile) = ore_profile {
1268            if let Some(guard) = section.try_write() {
1269                guard
1270            } else {
1271                if let Ok(mut profile) = profile.try_borrow_mut() {
1272                    profile.record_section_write_contention();
1273                }
1274                let wait_started_at = Instant::now();
1275                let guard = section.write();
1276                if let Ok(mut profile) = profile.try_borrow_mut() {
1277                    profile.record_write_contention_wait_time(wait_started_at.elapsed());
1278                }
1279                guard
1280            }
1281        } else {
1282            section.write()
1283        }
1284    }
1285
1286    /// Returns whether a section-local write would be allowed for this position.
1287    #[must_use]
1288    pub(crate) const fn can_write_to_pos(&self, pos: BlockPos) -> bool {
1289        self.region.can_write_to_chunk(
1290            SectionPos::block_to_section_coord(pos.x()),
1291            SectionPos::block_to_section_coord(pos.z()),
1292        )
1293    }
1294
1295    fn chunk(
1296        &mut self,
1297        chunk_x: i32,
1298        chunk_z: i32,
1299        status: ChunkStatus,
1300    ) -> &CachedWorldGenChunk<'region> {
1301        let Some(cache_key) = self.chunk_cache_key(chunk_x, chunk_z) else {
1302            panic!(
1303                "Worldgen bulk section requested chunk ({chunk_x}, {chunk_z}) outside the region cache centered on ({}, {})",
1304                self.region.center.0.x, self.region.center.0.y
1305            );
1306        };
1307
1308        match self
1309            .chunks
1310            .get(&cache_key)
1311            .map(|cached| cached.verified_status)
1312        {
1313            Some(verified_status) if status > verified_status => {
1314                self.with_ore_profile(OreFeatureStats::record_chunk_status_upgrade);
1315                let _ = self.region.chunk(chunk_x, chunk_z, status);
1316                let Some(cached) = self.chunks.get_mut(&cache_key) else {
1317                    panic!(
1318                        "Worldgen bulk section cache lost verified chunk ({chunk_x}, {chunk_z})"
1319                    );
1320                };
1321                cached.verified_status = status;
1322            }
1323            Some(_) => {}
1324            None => {
1325                self.with_ore_profile(OreFeatureStats::record_chunk_cache_miss);
1326                let chunk = self.region.chunk(chunk_x, chunk_z, status);
1327                self.chunks.insert(
1328                    cache_key,
1329                    CachedWorldGenChunk {
1330                        access_mode: chunk.access_mode,
1331                        holder: chunk.holder,
1332                        chunk: chunk.chunk,
1333                        verified_status: status,
1334                    },
1335                );
1336            }
1337        }
1338
1339        let Some(cached) = self.chunks.get(&cache_key) else {
1340            panic!("Worldgen bulk section cache failed to store chunk ({chunk_x}, {chunk_z})");
1341        };
1342        cached
1343    }
1344
1345    fn chunk_cache_key(&self, chunk_x: i32, chunk_z: i32) -> Option<i64> {
1346        let radius = self.chunk_cache_radius;
1347        let rel_x = chunk_x.checked_sub(self.region.center.0.x)?;
1348        let rel_z = chunk_z.checked_sub(self.region.center.0.y)?;
1349        if rel_x < -radius || rel_x > radius || rel_z < -radius || rel_z > radius {
1350            return None;
1351        }
1352
1353        Some(PackedChunkPos::from(ChunkPos::new(rel_x, rel_z)).as_raw())
1354    }
1355
1356    fn section_index(min_y: i32, height: i32, y: i32) -> Option<usize> {
1357        if y < min_y || y >= min_y + height {
1358            return None;
1359        }
1360
1361        usize::try_from((y - min_y) / 16).ok()
1362    }
1363
1364    const fn local_coord(coord: i32) -> usize {
1365        (coord & 15) as usize
1366    }
1367
1368    fn record_ore_read_time(
1369        profile: Option<&RefCell<OreFeatureStats>>,
1370        started_at: Option<Instant>,
1371    ) {
1372        if let Some(started_at) = started_at {
1373            Self::with_ore_profile_ref(profile, |profile| {
1374                profile.record_read_time(started_at.elapsed());
1375            });
1376        }
1377    }
1378
1379    fn record_ore_write_time(
1380        profile: Option<&RefCell<OreFeatureStats>>,
1381        started_at: Option<Instant>,
1382    ) {
1383        if let Some(started_at) = started_at {
1384            Self::with_ore_profile_ref(profile, |profile| {
1385                profile.record_write_time(started_at.elapsed());
1386            });
1387        }
1388    }
1389
1390    fn with_ore_profile(&self, f: impl FnOnce(&mut OreFeatureStats)) {
1391        Self::with_ore_profile_ref(self.ore_profile, f);
1392    }
1393
1394    fn with_ore_profile_ref(
1395        profile: Option<&RefCell<OreFeatureStats>>,
1396        f: impl FnOnce(&mut OreFeatureStats),
1397    ) {
1398        let Some(profile) = profile else {
1399            return;
1400        };
1401        if let Ok(mut profile) = profile.try_borrow_mut() {
1402            f(&mut profile);
1403        }
1404    }
1405}
1406
1407const fn abs_diff(left: i32, right: i32) -> i32 {
1408    if left >= right {
1409        left - right
1410    } else {
1411        right - left
1412    }
1413}
1414
1415impl LevelReader for WorldGenRegion<'_> {
1416    fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
1417        self.block_state(pos)
1418    }
1419
1420    fn get_block_entity(&self, pos: BlockPos) -> Option<SharedBlockEntity> {
1421        self.block_entity(pos)
1422    }
1423
1424    fn is_face_sturdy_for(
1425        &self,
1426        state: BlockStateId,
1427        pos: BlockPos,
1428        direction: Direction,
1429        support_type: SupportType,
1430    ) -> bool {
1431        BLOCK_BEHAVIORS
1432            .get_behavior(state.get_block())
1433            .is_face_sturdy(state, self, pos, direction, support_type)
1434    }
1435
1436    fn raw_brightness(&self, pos: BlockPos, sky_darkening: u8) -> u8 {
1437        let sky_light = if self.context.world().dimension_type.has_skylight {
1438            15_u8.saturating_sub(sky_darkening)
1439        } else {
1440            0
1441        };
1442
1443        sky_light.max(self.block_light_at(pos))
1444    }
1445
1446    fn can_see_sky(&self, pos: BlockPos) -> bool {
1447        if !self.context.world().dimension_type.has_skylight {
1448            return false;
1449        }
1450
1451        self.height_at(HeightmapType::MotionBlocking, pos.x(), pos.z()) <= pos.y()
1452    }
1453
1454    fn ambient_light(&self) -> f32 {
1455        self.context.world().dimension_type.ambient_light
1456    }
1457
1458    fn height_at(&self, heightmap_type: HeightmapType, x: i32, z: i32) -> i32 {
1459        WorldGenRegion::height_at(self, heightmap_type, x, z)
1460    }
1461
1462    fn min_y(&self) -> i32 {
1463        WorldGenRegion::min_y(self)
1464    }
1465
1466    fn height(&self) -> i32 {
1467        WorldGenRegion::height(self)
1468    }
1469}
1470
1471impl ScheduledTickAccess for WorldGenRegion<'_> {
1472    fn fluid_tick_delay(&self, fluid: FluidRef) -> i32 {
1473        FLUID_BEHAVIORS
1474            .get_behavior(fluid)
1475            .tick_delay(&self.context.world())
1476    }
1477
1478    fn schedule_block_tick_default(&self, pos: BlockPos, block: BlockRef, delay: i32) -> bool {
1479        WorldGenRegion::schedule_block_tick_default(self, pos, block, delay)
1480    }
1481
1482    fn schedule_fluid_tick_default(&self, pos: BlockPos, fluid: FluidRef, delay: i32) -> bool {
1483        WorldGenRegion::schedule_fluid_tick_default(self, pos, fluid, delay)
1484    }
1485}
1486
1487impl LevelAccessor for WorldGenRegion<'_> {
1488    fn set_block_state(&self, pos: BlockPos, state: BlockStateId, flags: UpdateFlags) -> bool {
1489        WorldGenRegion::set_block_state(self, pos, state, flags)
1490    }
1491
1492    fn can_write_to_chunk(&self, chunk_x: i32, chunk_z: i32) -> bool {
1493        WorldGenRegion::can_write_to_chunk(self, chunk_x, chunk_z)
1494    }
1495
1496    fn destroy_block(&self, pos: BlockPos, _drop_items: bool) -> bool {
1497        if self.block_state(pos).is_air() {
1498            return false;
1499        }
1500
1501        self.set_block_state(
1502            pos,
1503            vanilla_blocks::AIR.default_state(),
1504            UpdateFlags::UPDATE_ALL,
1505        )
1506    }
1507}
1508
1509#[cfg(test)]
1510mod tests {
1511    use std::sync::{Arc, Weak};
1512
1513    use steel_registry::{init_vanilla_registry, vanilla_blocks};
1514    use steel_utils::{BlockPos, ChunkPos, types::UpdateFlags};
1515
1516    use crate::behavior::init_behaviors;
1517    use crate::chunk::{
1518        Chunk,
1519        chunk_holder::ChunkHolder,
1520        chunk_ticket_manager::ChunkTicketLevel,
1521        heightmap::{Heightmap, HeightmapType},
1522        section::{ChunkSection, Sections},
1523        status::ChunkStatus,
1524    };
1525
1526    use super::{
1527        CachedWorldGenChunk, WorldGenAccessMode, WorldGenBulkSectionAccess, WorldGenChunkRef,
1528        WorldGenRegion,
1529    };
1530
1531    fn test_holder() -> ChunkHolder {
1532        ChunkHolder::new(
1533            ChunkPos::new(0, 0),
1534            ChunkTicketLevel::FULL_CHUNK,
1535            Some(ChunkTicketLevel::FULL_CHUNK),
1536            0,
1537            16,
1538        )
1539    }
1540
1541    fn published_holder(status: ChunkStatus) -> Arc<ChunkHolder> {
1542        let holder = Arc::new(test_holder());
1543        holder.insert_chunk(
1544            Chunk::new(
1545                Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1546                ChunkPos::new(0, 0),
1547                0,
1548                16,
1549                Weak::new(),
1550            ),
1551            status,
1552        );
1553        holder
1554    }
1555
1556    #[test]
1557    fn full_imposter_maps_worldgen_height_queries_to_final_heightmaps() {
1558        init_vanilla_registry();
1559        let chunk = Chunk::new(
1560            Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1561            ChunkPos::new(0, 0),
1562            0,
1563            16,
1564            Weak::new(),
1565        );
1566        let mut worldgen = Heightmap::new(HeightmapType::WorldSurfaceWg, 0, 16);
1567        worldgen.set_height(3, 7, 4);
1568        let mut final_map = Heightmap::new(HeightmapType::WorldSurface, 0, 16);
1569        final_map.set_height(3, 7, 11);
1570        {
1571            let mut heightmaps = chunk.heightmaps.write();
1572            heightmaps.replace(worldgen);
1573            heightmaps.replace(final_map);
1574        }
1575        let _ = chunk.promote_to_full();
1576        let holder = test_holder();
1577        let view = WorldGenChunkRef {
1578            holder: &holder,
1579            chunk: &chunk,
1580            access_mode: WorldGenAccessMode::ReadOnlyFull,
1581        };
1582
1583        assert_eq!(
1584            view.full_imposter_height_at(HeightmapType::WorldSurfaceWg, 3, 7),
1585            11
1586        );
1587    }
1588
1589    #[test]
1590    fn chessboard_distance_matches_chunk_dependency_radius() {
1591        let center = ChunkPos::new(4, -2);
1592
1593        assert_eq!(WorldGenRegion::chessboard_distance(center, 4, -2), 0);
1594        assert_eq!(WorldGenRegion::chessboard_distance(center, 5, -3), 1);
1595        assert_eq!(WorldGenRegion::chessboard_distance(center, -4, 6), 8);
1596    }
1597
1598    #[test]
1599    fn biome_quart_y_indices_clamp_to_vertical_biome_range() {
1600        assert_eq!(WorldGenRegion::biome_quart_y_indices(-64, 24, -17), (0, 0));
1601        assert_eq!(WorldGenRegion::biome_quart_y_indices(-64, 24, -16), (0, 0));
1602        assert_eq!(WorldGenRegion::biome_quart_y_indices(-64, 24, -13), (0, 3));
1603        assert_eq!(WorldGenRegion::biome_quart_y_indices(-64, 24, -12), (1, 0));
1604        assert_eq!(WorldGenRegion::biome_quart_y_indices(-64, 24, 79), (23, 3));
1605        assert_eq!(WorldGenRegion::biome_quart_y_indices(-64, 24, 80), (23, 3));
1606        assert_eq!(WorldGenRegion::biome_quart_y_indices(-64, 24, 81), (23, 3));
1607    }
1608
1609    #[test]
1610    fn bulk_section_index_matches_world_height_bounds() {
1611        assert_eq!(
1612            WorldGenBulkSectionAccess::section_index(-64, 384, -65),
1613            None
1614        );
1615        assert_eq!(
1616            WorldGenBulkSectionAccess::section_index(-64, 384, -64),
1617            Some(0)
1618        );
1619        assert_eq!(
1620            WorldGenBulkSectionAccess::section_index(-64, 384, -49),
1621            Some(0)
1622        );
1623        assert_eq!(
1624            WorldGenBulkSectionAccess::section_index(-64, 384, -48),
1625            Some(1)
1626        );
1627        assert_eq!(
1628            WorldGenBulkSectionAccess::section_index(-64, 384, 319),
1629            Some(23)
1630        );
1631        assert_eq!(
1632            WorldGenBulkSectionAccess::section_index(-64, 384, 320),
1633            None
1634        );
1635    }
1636
1637    #[test]
1638    fn bulk_section_local_coord_uses_vanilla_section_mask() {
1639        assert_eq!(WorldGenBulkSectionAccess::local_coord(-17), 15);
1640        assert_eq!(WorldGenBulkSectionAccess::local_coord(-16), 0);
1641        assert_eq!(WorldGenBulkSectionAccess::local_coord(-1), 15);
1642        assert_eq!(WorldGenBulkSectionAccess::local_coord(0), 0);
1643        assert_eq!(WorldGenBulkSectionAccess::local_coord(31), 15);
1644    }
1645
1646    #[test]
1647    fn bulk_section_reads_use_empty_proto_sections_for_full_imposters() {
1648        init_vanilla_registry();
1649        let chunk = Chunk::new(
1650            Sections::from_owned(vec![ChunkSection::new_empty()].into_boxed_slice()),
1651            ChunkPos::new(0, 0),
1652            0,
1653            16,
1654            Weak::new(),
1655        );
1656        let holder = test_holder();
1657
1658        let writable = CachedWorldGenChunk {
1659            holder: &holder,
1660            chunk: &chunk,
1661            verified_status: ChunkStatus::Empty,
1662            access_mode: WorldGenAccessMode::WritableProto,
1663        };
1664        let full_imposter = CachedWorldGenChunk {
1665            holder: &holder,
1666            chunk: &chunk,
1667            verified_status: ChunkStatus::Full,
1668            access_mode: WorldGenAccessMode::ReadOnlyFull,
1669        };
1670
1671        assert!(WorldGenBulkSectionAccess::section_for_read(&writable, 0).is_some());
1672        assert!(WorldGenBulkSectionAccess::section_for_read(&full_imposter, 0).is_none());
1673    }
1674
1675    #[test]
1676    fn bulk_write_defers_counts_only_for_pre_light_proto_chunks() {
1677        init_vanilla_registry();
1678        init_behaviors();
1679        let stone = vanilla_blocks::STONE.default_state();
1680        let air = vanilla_blocks::AIR.default_state();
1681
1682        let mut pre_light_section = ChunkSection::new_empty();
1683        let pre_light_holder = published_holder(ChunkStatus::Empty);
1684
1685        WorldGenBulkSectionAccess::set_bulk_block_state(
1686            &pre_light_holder,
1687            &mut pre_light_section,
1688            1,
1689            2,
1690            3,
1691            stone,
1692        );
1693
1694        assert_eq!(pre_light_section.non_empty_block_count(), 0);
1695
1696        let mut initialized_section = ChunkSection::new_empty();
1697        let initialized_holder = published_holder(ChunkStatus::InitializeLight);
1698
1699        WorldGenBulkSectionAccess::set_bulk_block_state(
1700            &initialized_holder,
1701            &mut initialized_section,
1702            1,
1703            2,
1704            3,
1705            stone,
1706        );
1707
1708        assert_eq!(initialized_section.non_empty_block_count(), 1);
1709
1710        let mut initialized_building_section = ChunkSection::new_empty();
1711        initialized_building_section.set_block_state_for_generation(1, 2, 3, stone);
1712
1713        let old_state = WorldGenBulkSectionAccess::set_bulk_block_state(
1714            &initialized_holder,
1715            &mut initialized_building_section,
1716            4,
1717            5,
1718            6,
1719            stone,
1720        );
1721
1722        assert_eq!(old_state, air);
1723        assert_eq!(initialized_building_section.non_empty_block_count(), 2);
1724    }
1725
1726    #[test]
1727    fn cached_worldgen_view_observes_holder_status_advancement() {
1728        init_vanilla_registry();
1729        init_behaviors();
1730        let holder = published_holder(ChunkStatus::Features);
1731        let Some(chunk) = holder.try_chunk(ChunkStatus::Features) else {
1732            panic!("test chunk was not published");
1733        };
1734        let view = WorldGenChunkRef {
1735            holder: &holder,
1736            chunk,
1737            access_mode: WorldGenAccessMode::WritableProto,
1738        };
1739
1740        holder.finish_generation_status_for_test(ChunkStatus::InitializeLight);
1741
1742        let stone = vanilla_blocks::STONE.default_state();
1743        let old_state = view.chunk.set_block_state_for_generation(
1744            view.published_status(),
1745            BlockPos::new(1, 2, 3),
1746            stone,
1747            UpdateFlags::UPDATE_NONE,
1748        );
1749        assert!(old_state.is_some());
1750        assert_eq!(
1751            chunk.sections().sections[0].read().non_empty_block_count(),
1752            1
1753        );
1754
1755        let mut section = chunk.sections().sections[0].write();
1756        WorldGenBulkSectionAccess::set_bulk_block_state(&holder, &mut section, 4, 5, 6, stone);
1757        assert_eq!(section.non_empty_block_count(), 2);
1758    }
1759}