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