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