Skip to main content

steel_core/worldgen/generator/
vanilla.rs

1use std::path::Path;
2use std::{cell::Cell, marker::PhantomData};
3
4use glam::{DVec3, IVec3};
5use rustc_hash::FxHashSet;
6use smallvec::SmallVec;
7use steel_math::lerp2;
8use steel_registry::biome::BiomeRef;
9use steel_registry::blocks::block_state_ext::BlockStateExt;
10use steel_registry::carver::ConfiguredCarverKind;
11use steel_registry::{REGISTRY, RegistryEntry, RegistryExt, vanilla_biomes};
12use steel_utils::random::{
13    Random, RandomSource, RandomSplitter, legacy_random::LegacyRandom, xoroshiro::Xoroshiro,
14};
15use steel_utils::{BlockPos, BlockStateId, ChunkPos, DowncastType, DowncastTypeKey, Identifier};
16use steel_worldgen::density::{ColumnCache, DimensionNoises, NoiseSettings};
17use steel_worldgen::density_functions::{
18    end::EndNoises, nether::NetherNoises, overworld::OverworldNoises,
19};
20use steel_worldgen::noise_parameters::get_noise_parameters;
21use steel_worldgen::surface::{
22    SurfaceBiomeProvider, SurfaceConditionNoiseCache, SurfaceRuleContext,
23};
24
25use crate::chunk::Chunk;
26use crate::chunk::heightmap::{Heightmap, HeightmapType};
27use crate::worldgen::carver::{
28    CarveRun, CarverBlockIds, CarvingContext, PreliminarySurfaceCorners, SourceChunk, cave,
29};
30use crate::worldgen::feature::FeatureDecorationRunner;
31use crate::worldgen::generator::{
32    CarversPhase, ChunkGenerator, GenerationChunk, NoisePhase, SurfacePhase,
33    worldgen_region_random_from_splitter,
34};
35use crate::worldgen::region::WorldGenRegion;
36use crate::worldgen::structure::{StructureGenerator, create_structures};
37use crate::worldgen::surface::SurfaceSystem;
38use steel_worldgen::biomes::BiomeSourceKind;
39use steel_worldgen::biomes::obfuscate_biome_seed;
40use steel_worldgen::noise::Beardifier;
41use steel_worldgen::noise::NoiseChunk;
42use steel_worldgen::noise::OreVeinifier;
43use steel_worldgen::noise::{Aquifer, AquiferResult, LazyAquifer, preliminary_surface_level};
44use steel_worldgen::structure::GenerationContext;
45
46const CARVER_SOURCE_CHUNK_COUNT: usize = 17 * 17;
47
48/// Associates a dimension's statically typed Aquifer with its transient erased state.
49///
50/// Custom dimension noise implementations own their state type and downcast key. Steel
51/// only erases the value between stages; every hot-path Aquifer call remains monomorphized.
52pub trait VanillaPostNoiseStateType: DimensionNoises + 'static {
53    /// Concrete state stored transiently on the generating chunk.
54    type State: DowncastType + Send + Sync;
55
56    /// Wraps the Aquifer after the Noise stage.
57    fn wrap_post_noise_aquifer(aquifer: Aquifer<Self>) -> Self::State;
58
59    /// Recovers the concrete Aquifer at a later generation stage.
60    fn post_noise_aquifer(state: &mut Self::State) -> &mut Aquifer<Self>;
61}
62
63/// Steel-owned post-Noise state for the built-in dimensions.
64#[doc(hidden)]
65pub struct SteelPostNoiseState<N: DimensionNoises> {
66    aquifer: Aquifer<N>,
67}
68
69// SAFETY: Each key uniquely identifies this exact Steel-owned specialization.
70unsafe impl DowncastType for SteelPostNoiseState<OverworldNoises> {
71    const TYPE_KEY: DowncastTypeKey =
72        DowncastTypeKey::new("steel:worldgen_state/post_noise_overworld");
73}
74
75// SAFETY: Each key uniquely identifies this exact Steel-owned specialization.
76unsafe impl DowncastType for SteelPostNoiseState<NetherNoises> {
77    const TYPE_KEY: DowncastTypeKey =
78        DowncastTypeKey::new("steel:worldgen_state/post_noise_nether");
79}
80
81// SAFETY: Each key uniquely identifies this exact Steel-owned specialization.
82unsafe impl DowncastType for SteelPostNoiseState<EndNoises> {
83    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:worldgen_state/post_noise_end");
84}
85
86macro_rules! impl_post_noise_state_type {
87    ($noises:ty) => {
88        impl VanillaPostNoiseStateType for $noises {
89            type State = SteelPostNoiseState<Self>;
90
91            fn wrap_post_noise_aquifer(aquifer: Aquifer<Self>) -> Self::State {
92                SteelPostNoiseState { aquifer }
93            }
94
95            fn post_noise_aquifer(state: &mut Self::State) -> &mut Aquifer<Self> {
96                &mut state.aquifer
97            }
98        }
99    };
100}
101
102impl_post_noise_state_type!(OverworldNoises);
103impl_post_noise_state_type!(NetherNoises);
104impl_post_noise_state_type!(EndNoises);
105
106/// A chunk generator for vanilla (normal) world generation.
107///
108/// Matches vanilla's `NoiseBasedChunkGenerator`. The biome source is pluggable
109/// per-dimension — overworld, nether, and end each provide a different
110/// [`BiomeSourceKind`] variant.
111///
112/// Generic over `N: DimensionNoises` to support different dimensions with
113/// their own transpiled density functions and noise settings.
114pub struct VanillaGenerator<N: DimensionNoises> {
115    /// Biome source for this dimension. Determines biomes at each quart position.
116    biome_source: BiomeSourceKind,
117    /// Representative biome for source-carver lookup when every possible
118    /// biome from `biome_source` has the same carver list.
119    ///
120    /// Vanilla still samples each source biome in `apply_carvers`; Steel skips
121    /// that sampling only when the source's full possible-biome set proves the
122    /// carver list is uniform. If future biome sources can produce mixed
123    /// carver lists this remains `None` and the vanilla per-source lookup is
124    /// used.
125    uniform_carver_biome: Option<BiomeRef>,
126    /// Noise generators for this dimension's density functions.
127    /// Boxed because noise structs can be large.
128    noises: Box<N>,
129    /// Seed positional splitter for per-chunk construction of aquifers.
130    splitter: RandomSplitter,
131    /// Ore vein generator for replacing stone with ore blocks.
132    ore_veinifier: Option<OreVeinifier>,
133    /// Surface system for biome-specific block replacement.
134    surface_system: SurfaceSystem,
135    /// Which vanilla surface extension biomes this source can produce.
136    surface_extension_biomes: SurfaceExtensionBiomes,
137    /// Block state ID for the default block, cached at construction time.
138    default_block_id: BlockStateId,
139    /// Obfuscated seed for `BiomeManager` biome zoom fuzzing.
140    biome_zoom_seed: i64,
141    /// World seed as i64 (matching Java's long), used for structures and carver seeding.
142    seed: i64,
143    /// Shared structure placement/selection engine.
144    structure_generator: StructureGenerator,
145    /// Cached placed-feature order for biome decoration.
146    feature_runner: FeatureDecorationRunner,
147    _phantom: PhantomData<N>,
148}
149
150#[derive(Clone, Copy)]
151struct SurfaceExtensionBiomes {
152    eroded_badlands: bool,
153    frozen_ocean: bool,
154}
155
156impl SurfaceExtensionBiomes {
157    fn from_possible(possible_biomes: &FxHashSet<Identifier>) -> Self {
158        Self {
159            eroded_badlands: possible_biomes.contains(&vanilla_biomes::ERODED_BADLANDS.key),
160            frozen_ocean: possible_biomes.contains(&vanilla_biomes::FROZEN_OCEAN.key)
161                || possible_biomes.contains(&vanilla_biomes::DEEP_FROZEN_OCEAN.key),
162        }
163    }
164
165    const fn needs_surface_biome(self) -> bool {
166        self.eroded_badlands || self.frozen_ocean
167    }
168}
169
170impl<N: DimensionNoises> VanillaGenerator<N> {
171    /// Creates a new `VanillaGenerator` with the given biome source and seed.
172    ///
173    /// # Panics
174    /// Panics if SHA-256 hash output is shorter than 8 bytes (cannot happen).
175    #[must_use]
176    pub fn new(
177        world_path: Option<&Path>,
178        biome_source: BiomeSourceKind,
179        seed: u64,
180        thread_pool: &rayon::ThreadPool,
181    ) -> Self {
182        // Nether uses Java's LCG; overworld/end use Xoroshiro.
183        let splitter = if N::Settings::LEGACY_RANDOM_SOURCE {
184            LegacyRandom::from_seed(seed).next_positional()
185        } else {
186            Xoroshiro::from_seed(seed).next_positional()
187        };
188        let noise_params = get_noise_parameters();
189        let noises = N::create(seed, &splitter, &noise_params);
190
191        let ore_veinifier = if N::Settings::ORE_VEINS_ENABLED {
192            Some(OreVeinifier::new(&splitter))
193        } else {
194            None
195        };
196
197        let default_block_id = N::Settings::default_block_id();
198        let surface_system = SurfaceSystem::new(
199            &splitter,
200            &noise_params,
201            N::surface_noise_ids(),
202            N::surface_gradient_ids(),
203            default_block_id,
204            N::Settings::SEA_LEVEL,
205        );
206
207        let biome_zoom_seed = obfuscate_biome_seed(seed as i64);
208
209        // Force the lazy parameter-list R-tree inside the configured generation
210        // pool so its parallel construction does not initialize Rayon's global pool.
211        let possible_biome_refs = thread_pool.install(|| biome_source.possible_biome_refs());
212        let possible_biomes = biome_source.possible_biomes();
213        let surface_extension_biomes = SurfaceExtensionBiomes::from_possible(&possible_biomes);
214        let structure_generator =
215            StructureGenerator::vanilla(seed as i64, world_path, &biome_source, thread_pool);
216        let uniform_carver_biome = Self::uniform_carver_biome(&possible_biomes);
217        let feature_runner = FeatureDecorationRunner::new(&possible_biome_refs, &REGISTRY);
218
219        Self {
220            biome_source,
221            uniform_carver_biome,
222            noises: Box::new(noises),
223            splitter,
224            ore_veinifier,
225            surface_system,
226            surface_extension_biomes,
227            default_block_id,
228            biome_zoom_seed,
229            seed: seed as i64,
230            structure_generator,
231            feature_runner,
232            _phantom: PhantomData,
233        }
234    }
235
236    fn uniform_carver_biome(possible_biomes: &FxHashSet<Identifier>) -> Option<BiomeRef> {
237        let mut possible_biomes = possible_biomes.iter();
238        let first_key = possible_biomes.next()?;
239        let first = REGISTRY.biomes.by_key(first_key)?;
240
241        possible_biomes
242            .all(|key| {
243                REGISTRY
244                    .biomes
245                    .by_key(key)
246                    .is_some_and(|biome| biome.carvers == first.carvers)
247            })
248            .then_some(first)
249    }
250}
251
252impl<N: VanillaPostNoiseStateType> VanillaGenerator<N> {
253    fn preliminary_surface_corners(
254        &self,
255        chunk: GenerationChunk<'_, SurfacePhase>,
256        chunk_min_x: i32,
257        chunk_min_z: i32,
258    ) -> PreliminarySurfaceCorners {
259        let noises = &*self.noises;
260        if let Some(corners) = chunk.with_post_noise_state_mut::<N::State, _>(|state| {
261            let aquifer = N::post_noise_aquifer(state);
262            PreliminarySurfaceCorners {
263                nw: aquifer.preliminary_surface_level(noises, chunk_min_x, chunk_min_z),
264                ne: aquifer.preliminary_surface_level(noises, chunk_min_x + 16, chunk_min_z),
265                sw: aquifer.preliminary_surface_level(noises, chunk_min_x, chunk_min_z + 16),
266                se: aquifer.preliminary_surface_level(noises, chunk_min_x + 16, chunk_min_z + 16),
267            }
268        }) {
269            return corners;
270        }
271
272        let mut cache = N::ColumnCache::default();
273        PreliminarySurfaceCorners {
274            nw: preliminary_surface_level::<N>(noises, &mut cache, chunk_min_x, chunk_min_z),
275            ne: preliminary_surface_level::<N>(noises, &mut cache, chunk_min_x + 16, chunk_min_z),
276            sw: preliminary_surface_level::<N>(noises, &mut cache, chunk_min_x, chunk_min_z + 16),
277            se: preliminary_surface_level::<N>(
278                noises,
279                &mut cache,
280                chunk_min_x + 16,
281                chunk_min_z + 16,
282            ),
283        }
284    }
285}
286
287impl<N: VanillaPostNoiseStateType> ChunkGenerator for VanillaGenerator<N> {
288    fn min_y(&self) -> i32 {
289        N::Settings::MIN_Y
290    }
291
292    fn gen_depth(&self) -> i32 {
293        N::Settings::HEIGHT
294    }
295
296    fn noise_biome(&self, quart_x: i32, quart_y: i32, quart_z: i32) -> BiomeRef {
297        self.biome_source
298            .chunk_sampler()
299            .sample(quart_x, quart_y, quart_z)
300    }
301
302    fn initial_spawn_search_origin(&self) -> steel_utils::BlockPos {
303        self.biome_source.initial_spawn_search_origin()
304    }
305
306    fn structure_generator(&self) -> Option<&StructureGenerator> {
307        Some(&self.structure_generator)
308    }
309
310    fn create_structures(&self, chunk: &Chunk) {
311        let pos = chunk.pos();
312        let chunk_x = pos.0.x;
313        let chunk_z = pos.0.y;
314
315        let mut sampler = self.biome_source.chunk_sampler();
316        let chunk_min_x = chunk_x * 16;
317        let chunk_min_z = chunk_z * 16;
318
319        let mut height_cache = N::ColumnCache::default();
320        let sea_level = N::Settings::SEA_LEVEL;
321
322        // No eager `init_grid`: most chunks' structures (mineshaft, village)
323        // use their own caches, and the 1–4 column probes of the remainder
324        // hit this cache's lazy single-entry mode cheaply. Eager 5×5 quart
325        // init cost ~36µs per chunk with no payoff.
326        let mut aquifer = LazyAquifer::new(chunk_min_x, chunk_min_z, &self.splitter, &*self.noises);
327        let mut surface_y_cache: Option<i32> = None;
328        let mut height_cache_grid_ready = false;
329        let mut ctx = GenerationContext::<'_, '_, N>::new(
330            self.seed,
331            chunk_x,
332            chunk_z,
333            sea_level,
334            &self.noises,
335            &self.splitter,
336            self.structure_generator.template_pools(),
337            self.structure_generator.templates(),
338            &mut sampler,
339            &mut height_cache,
340            &mut aquifer,
341            &mut surface_y_cache,
342            &mut height_cache_grid_ready,
343        );
344
345        create_structures(&self.structure_generator, chunk, &mut ctx);
346    }
347
348    fn create_biomes(&self, chunk: &Chunk) {
349        let pos = chunk.pos();
350        let min_y = chunk.min_y();
351        let section_count = chunk.sections().sections.len();
352
353        let chunk_x = pos.0.x;
354        let chunk_z = pos.0.y;
355
356        let mut sampler = self.biome_source.chunk_sampler();
357        // Pre-compute the flat (xz-only) climate-noise grid for this chunk so the
358        // per-cell sampling below does O(1) column lookups instead of recomputing
359        // the flat noise for all 1536 cells (the noise stage's `fill_from_noise`
360        // already does this). Values are bit-identical — same functions, same quart
361        // coordinates — so biome selection is unchanged.
362        sampler.init_grid(chunk_x * 16, chunk_z * 16);
363
364        // Match vanilla's iteration order: Section(Y) → X → Y → Z.
365        // This is critical because the R-tree biome cache (persistent warm-start)
366        // determines tie-breaking for equal-distance entries, and the cache state
367        // depends on the order of biome lookups.
368        for section_index in 0..section_count {
369            let section_y = (min_y / 16) + section_index as i32;
370            let section = &chunk.sections().sections[section_index];
371            let mut section_guard = section.write();
372
373            for local_quart_x in 0..4i32 {
374                let quart_x = chunk_x * 4 + local_quart_x;
375
376                for local_quart_y in 0..4i32 {
377                    let quart_y = section_y * 4 + local_quart_y;
378
379                    for local_quart_z in 0..4i32 {
380                        let quart_z = chunk_z * 4 + local_quart_z;
381
382                        let biome = sampler.sample(quart_x, quart_y, quart_z);
383                        let biome_id = biome.id() as u16;
384
385                        section_guard.biomes.set(
386                            local_quart_x as usize,
387                            local_quart_y as usize,
388                            local_quart_z as usize,
389                            biome_id,
390                        );
391                    }
392                }
393            }
394        }
395
396        chunk.mark_dirty();
397    }
398
399    fn fill_from_noise(
400        &self,
401        chunk: GenerationChunk<'_, NoisePhase>,
402        beardifier: Option<&Beardifier>,
403    ) {
404        let pos = chunk.pos();
405        let chunk_min_x = pos.0.x * 16;
406        let chunk_min_z = pos.0.y * 16;
407
408        let min_y = N::Settings::MIN_Y;
409        let height = N::Settings::HEIGHT;
410
411        let mut noise_chunk = NoiseChunk::<N>::new(chunk_min_x, chunk_min_z);
412        let noises = &*self.noises;
413
414        let mut column_cache = N::ColumnCache::default();
415        column_cache.init_grid(chunk_min_x, chunk_min_z, noises);
416
417        let default_block_id = self.default_block_id;
418        let ore_veinifier = &self.ore_veinifier;
419        let mut aquifer = Aquifer::<N>::new(
420            chunk_min_x,
421            chunk_min_z,
422            min_y,
423            height,
424            &self.splitter,
425            noises,
426            // Aquifer samples at arbitrary (x,z) outside the chunk, so it needs its own cache
427            column_cache.clone(),
428        );
429
430        // Collect writes per (x,z) column and flush in batch to avoid per-block
431        // write lock acquisition on sections.
432        let mut pending_writes: Vec<(usize, usize, usize, BlockStateId)> = Vec::new();
433        let mut prev_x: usize = usize::MAX;
434        let mut prev_z: usize = usize::MAX;
435        let mut ocean_floor_wg =
436            Heightmap::new(HeightmapType::OceanFloorWg, min_y, N::Settings::HEIGHT);
437        let mut world_surface_wg =
438            Heightmap::new(HeightmapType::WorldSurfaceWg, min_y, N::Settings::HEIGHT);
439
440        noise_chunk.fill(
441            noises,
442            &mut column_cache,
443            beardifier,
444            |local_x, world_y, local_z, density, interpolated, cache| {
445                // Flush when we move to a new column
446                if local_x != prev_x || local_z != prev_z {
447                    if !pending_writes.is_empty() {
448                        chunk.write_block_batch(&pending_writes);
449                        pending_writes.clear();
450                    }
451                    prev_x = local_x;
452                    prev_z = local_z;
453                }
454
455                let relative_y = (world_y - min_y) as usize;
456                let world_x = chunk_min_x + local_x as i32;
457                let world_z = chunk_min_z + local_z as i32;
458
459                match aquifer.compute_substance(noises, world_x, world_y, world_z, density) {
460                    AquiferResult::Solid => {
461                        let block = ore_veinifier
462                            .as_ref()
463                            .and_then(|ov| {
464                                ov.compute_interpolated(
465                                    noises,
466                                    cache,
467                                    interpolated,
468                                    world_x,
469                                    world_y,
470                                    world_z,
471                                )
472                            })
473                            .unwrap_or(default_block_id);
474                        pending_writes.push((local_x, relative_y, local_z, block));
475                        ocean_floor_wg.update_for_initial_fill(local_x, world_y, local_z, block);
476                        world_surface_wg.update_for_initial_fill(local_x, world_y, local_z, block);
477                    }
478                    AquiferResult::Fluid(id) => {
479                        pending_writes.push((local_x, relative_y, local_z, id));
480                        ocean_floor_wg.update_for_initial_fill(local_x, world_y, local_z, id);
481                        world_surface_wg.update_for_initial_fill(local_x, world_y, local_z, id);
482                        if aquifer.should_schedule_fluid_update() && id.has_fluid() {
483                            chunk.mark_pos_for_postprocessing(BlockPos::new(
484                                world_x, world_y, world_z,
485                            ));
486                        }
487                    }
488                    AquiferResult::Air => {}
489                }
490            },
491        );
492
493        // Flush remaining writes
494        if !pending_writes.is_empty() {
495            chunk.write_block_batch(&pending_writes);
496        }
497
498        chunk.replace_noise_heightmaps(ocean_floor_wg, world_surface_wg);
499
500        if N::Settings::AQUIFERS_ENABLED {
501            chunk.install_post_noise_state(N::wrap_post_noise_aquifer(aquifer));
502        }
503    }
504
505    #[expect(clippy::too_many_lines, reason = "splitting would hurt readability")]
506    fn build_surface(
507        &self,
508        chunk: GenerationChunk<'_, SurfacePhase>,
509        neighbor_biomes: &dyn Fn(IVec3) -> u16,
510    ) {
511        let min_y = N::Settings::MIN_Y;
512        let pos = chunk.pos();
513        let chunk_min_x = pos.0.x * 16;
514        let chunk_min_z = pos.0.y * 16;
515        let default_block_id = self.default_block_id;
516        let surface_rule_block_states = N::surface_rule_block_states();
517        let surface_rule_uses_biome = N::surface_rule_uses_biome();
518        let surface_rule_uses_preliminary_surface = N::surface_rule_uses_preliminary_surface();
519        let surface_rule_uses_surface_secondary = N::surface_rule_uses_surface_secondary();
520        let surface_rule_uses_steep = N::surface_rule_uses_steep();
521        let lazy_surface_rule_biome =
522            surface_rule_uses_biome && surface_rule_uses_preliminary_surface;
523        let surface_needs_min_surface_level =
524            surface_rule_uses_preliminary_surface || self.surface_extension_biomes.frozen_ocean;
525        let surface_needs_biomes =
526            surface_rule_uses_biome || self.surface_extension_biomes.needs_surface_biome();
527        let chunk_quart_x = pos.0.x * 4;
528        let chunk_quart_z = pos.0.y * 4;
529
530        chunk.prime_world_surface_heightmap();
531
532        // Pre-compute preliminary surface corners only for rules/extensions that read them.
533        let preliminary_surface_corners = surface_needs_min_surface_level
534            .then(|| self.preliminary_surface_corners(chunk, chunk_min_x, chunk_min_z));
535
536        let eroded_badlands_id = (*vanilla_biomes::ERODED_BADLANDS).id() as u16;
537        let frozen_ocean_id = (*vanilla_biomes::FROZEN_OCEAN).id() as u16;
538        let deep_frozen_ocean_id = (*vanilla_biomes::DEEP_FROZEN_OCEAN).id() as u16;
539
540        // Pre-extract biome palette values only if surface rules/extensions need them.
541        let biome_data = surface_needs_biomes.then(|| chunk.read_all_biomes());
542        let section_count = chunk.section_count();
543
544        let mut pending_writes: Vec<(usize, BlockStateId)> = Vec::new();
545        let mut column_buf: Vec<BlockStateId> = Vec::new();
546        let condition_noise_values = N::surface_noise_ids()
547            .iter()
548            .map(|_| Cell::new(0.0))
549            .collect::<Vec<_>>();
550        let condition_noise_initialized = N::surface_noise_ids()
551            .iter()
552            .map(|_| Cell::new(false))
553            .collect::<Vec<_>>();
554        let condition_noise_cache =
555            SurfaceConditionNoiseCache::new(&condition_noise_values, &condition_noise_initialized);
556
557        for local_x in 0..16usize {
558            for local_z in 0..16usize {
559                let block_x = chunk_min_x + local_x as i32;
560                let block_z = chunk_min_z + local_z as i32;
561
562                // Start scanning from one above the highest non-air block
563                let mut start_height = chunk.world_surface_height_at(local_x, local_z);
564
565                // Column-local Voronoi cache for fuzzed biome lookups.
566                let mut biome_col = biome_data.as_deref().map(|biome_data| {
567                    FuzzedBiomeColumn::new(
568                        biome_data,
569                        section_count,
570                        self.biome_zoom_seed,
571                        block_x,
572                        block_z,
573                        min_y,
574                        chunk_quart_x,
575                        chunk_quart_z,
576                        neighbor_biomes,
577                    )
578                });
579
580                // Eroded badlands extension: add terracotta pillars above surface
581                let surface_biome_id = if self.surface_extension_biomes.needs_surface_biome() {
582                    biome_col
583                        .as_mut()
584                        .map(|biome_col| biome_col.get(start_height))
585                } else {
586                    None
587                };
588                if self.surface_extension_biomes.eroded_badlands
589                    && surface_biome_id == Some(eroded_badlands_id)
590                {
591                    start_height = self.surface_system.eroded_badlands_extension(
592                        chunk,
593                        local_x,
594                        local_z,
595                        block_x,
596                        block_z,
597                        start_height,
598                        min_y,
599                    );
600                }
601
602                // Snapshot the column once — avoids per-block section locking in the Y scan.
603                // Taken after eroded_badlands_extension which may write blocks above the surface.
604                chunk.read_column_into(local_x, local_z, &mut column_buf);
605
606                // Surface depth for this column
607                let surface_depth = self.surface_system.get_surface_depth(block_x, block_z);
608
609                let surface_secondary = if surface_rule_uses_surface_secondary {
610                    self.surface_system.get_surface_secondary(block_x, block_z)
611                } else {
612                    0.0
613                };
614                condition_noise_cache.reset();
615
616                let min_surface_level = if let Some(corners) = preliminary_surface_corners {
617                    // Vanilla: (float)(blockX & 15) / 16.0F — exact for 0-15.
618                    let t_x = f64::from(local_x as u8) / 16.0;
619                    let t_z = f64::from(local_z as u8) / 16.0;
620                    let interp = lerp2(
621                        t_x,
622                        t_z,
623                        f64::from(corners.nw),
624                        f64::from(corners.ne),
625                        f64::from(corners.sw),
626                        f64::from(corners.se),
627                    );
628                    interp.floor() as i32 + surface_depth - 8
629                } else {
630                    0
631                };
632
633                // Steep condition: vanilla only checks south >= north + 4 and
634                // west >= east + 4 (asymmetric, not absolute difference).
635                let steep = surface_rule_uses_steep && {
636                    let z_north = local_z.saturating_sub(1);
637                    let z_south = (local_z + 1).min(15);
638                    let h_north = chunk.world_surface_height_at(local_x, z_north) - 1;
639                    let h_south = chunk.world_surface_height_at(local_x, z_south) - 1;
640                    if h_south >= h_north + 4 {
641                        true
642                    } else {
643                        let x_west = local_x.saturating_sub(1);
644                        let x_east = (local_x + 1).min(15);
645                        let h_west = chunk.world_surface_height_at(x_west, local_z) - 1;
646                        let h_east = chunk.world_surface_height_at(x_east, local_z) - 1;
647                        h_west >= h_east + 4
648                    }
649                };
650
651                let mut stone_depth_above: i32 = 0;
652                let mut water_height: i32 = i32::MIN;
653                let mut next_ceiling_stone_y: i32 = i32::MAX;
654                pending_writes.clear();
655
656                for y in (min_y..=start_height).rev() {
657                    let relative_y = (y - min_y) as usize;
658                    let state = column_buf[relative_y];
659
660                    if state.is_air() {
661                        stone_depth_above = 0;
662                        water_height = i32::MIN;
663                        continue;
664                    }
665
666                    if state.get_block().config.liquid {
667                        if water_height == i32::MIN {
668                            water_height = y + 1;
669                        }
670                        continue;
671                    }
672
673                    // Solid block — scan for stone_depth_below (lookahead)
674                    if next_ceiling_stone_y >= y {
675                        next_ceiling_stone_y = i32::MIN;
676                        for la_y in (min_y - 1..y).rev() {
677                            if la_y < min_y {
678                                next_ceiling_stone_y = la_y + 1;
679                                break;
680                            }
681                            let la_rel = (la_y - min_y) as usize;
682                            let la_state = column_buf[la_rel];
683                            // isStone = !isAir && !isLiquid
684                            if la_state.is_air() || la_state.get_block().config.liquid {
685                                next_ceiling_stone_y = la_y + 1;
686                                break;
687                            }
688                        }
689                    }
690
691                    stone_depth_above += 1;
692                    let stone_depth_below = y - next_ceiling_stone_y + 1;
693
694                    // Only apply surface rules to the default block
695                    if state == default_block_id {
696                        let eager_biome_id = if surface_rule_uses_biome && !lazy_surface_rule_biome
697                        {
698                            biome_col.as_mut().map(|biome_col| biome_col.get(y))
699                        } else {
700                            None
701                        };
702                        let biome_provider = if lazy_surface_rule_biome {
703                            biome_col
704                                .as_mut()
705                                .map(|biome_col| biome_col as &mut dyn SurfaceBiomeProvider)
706                        } else {
707                            None
708                        };
709
710                        let mut ctx = SurfaceRuleContext::new(
711                            block_x,
712                            block_z,
713                            surface_depth,
714                            surface_secondary,
715                            min_surface_level,
716                            steep,
717                            y,
718                            stone_depth_above,
719                            stone_depth_below,
720                            water_height,
721                            eager_biome_id,
722                            biome_provider,
723                            &self.surface_system,
724                            &condition_noise_cache,
725                            surface_rule_block_states,
726                        );
727
728                        let rule_result = N::try_apply_surface_rule(&mut ctx);
729
730                        if let Some(new_block) = rule_result {
731                            pending_writes.push((relative_y, new_block));
732                        }
733                    }
734                }
735
736                // Flush batched writes — holds each section's write guard once
737                if !pending_writes.is_empty() {
738                    chunk.write_column(local_x, local_z, &pending_writes);
739                    for &(relative_y, state) in &pending_writes {
740                        column_buf[relative_y] = state;
741                    }
742                }
743
744                // Frozen ocean iceberg extension: add packed ice and snow
745                if self.surface_extension_biomes.frozen_ocean
746                    && let Some(surface_biome_id) = surface_biome_id
747                        .filter(|id| *id == frozen_ocean_id || *id == deep_frozen_ocean_id)
748                {
749                    pending_writes.clear();
750                    self.surface_system.collect_frozen_ocean_extension_writes(
751                        surface_biome_id,
752                        block_x,
753                        block_z,
754                        start_height,
755                        min_surface_level,
756                        min_y,
757                        &column_buf,
758                        &mut pending_writes,
759                    );
760                    if !pending_writes.is_empty() {
761                        chunk.write_column(local_x, local_z, &pending_writes);
762                    }
763                }
764            }
765        }
766    }
767
768    fn apply_carvers(&self, chunk: GenerationChunk<'_, CarversPhase>) {
769        if self
770            .uniform_carver_biome
771            .is_some_and(|biome| biome.carvers.is_empty())
772        {
773            chunk.clear_post_noise_state();
774            return;
775        }
776
777        chunk.consume_post_noise_state::<N::State, _>(|retained_state| {
778            chunk.prime_world_surface_heightmap();
779
780            let pos = chunk.pos();
781            let chunk_min_x = pos.0.x * 16;
782            let chunk_min_z = pos.0.y * 16;
783            let min_y = N::Settings::MIN_Y;
784            let height = N::Settings::HEIGHT;
785            let noises = &*self.noises;
786
787            let mut rebuilt_aquifer = None;
788            let aquifer = if let Some(state) = retained_state {
789                N::post_noise_aquifer(state)
790            } else {
791                let mut column_cache = N::ColumnCache::default();
792                if N::Settings::AQUIFERS_ENABLED {
793                    column_cache.init_grid(chunk_min_x, chunk_min_z, noises);
794                }
795                rebuilt_aquifer.insert(Aquifer::<N>::new(
796                    chunk_min_x,
797                    chunk_min_z,
798                    min_y,
799                    height,
800                    &self.splitter,
801                    noises,
802                    column_cache,
803                ))
804            };
805
806            // Preliminary surface level at the chunk's 4 corners — used by
807            // top_material min_surface_level interpolation.
808            let psl_corners = PreliminarySurfaceCorners {
809                nw: aquifer.preliminary_surface_level(noises, chunk_min_x, chunk_min_z),
810                ne: aquifer.preliminary_surface_level(noises, chunk_min_x + 16, chunk_min_z),
811                sw: aquifer.preliminary_surface_level(noises, chunk_min_x, chunk_min_z + 16),
812                se: aquifer.preliminary_surface_level(noises, chunk_min_x + 16, chunk_min_z + 16),
813            };
814
815            let mut ctx = CarvingContext {
816                min_y,
817                gen_depth: height,
818                surface_system: &self.surface_system,
819                aquifer,
820                default_block_id: self.default_block_id,
821                psl_corners,
822                chunk_min_x,
823                chunk_min_z,
824            };
825
826            let ids = CarverBlockIds::load();
827
828            // Pre-fetch the 17×17 source-chunk carver lists. Done up front so we
829            // can later close over `biome_sampler` mutably inside `biome_getter`.
830            // Vanilla samples every source biome here; when this generator's full
831            // possible-biome set has a uniform carver list, the representative
832            // biome gives the same carver keys without 289 climate lookups.
833            let mut biome_sampler = self.biome_source.chunk_sampler();
834            let mut source_biomes: SmallVec<[SourceChunk; CARVER_SOURCE_CHUNK_COUNT]> =
835                SmallVec::new();
836            for dx in -8i32..=8 {
837                for dz in -8i32..=8 {
838                    let sx = pos.0.x + dx;
839                    let sz = pos.0.y + dz;
840                    let biome = if let Some(biome) = self.uniform_carver_biome {
841                        biome
842                    } else {
843                        let qx = (sx * 16) >> 2;
844                        let qz = (sz * 16) >> 2;
845                        biome_sampler.sample(qx, 0, qz)
846                    };
847                    source_biomes.push(SourceChunk {
848                        pos: ChunkPos::new(sx, sz),
849                        biome,
850                    });
851                }
852            }
853
854            // `WorldgenRandom(LegacyRandomSource(generateUniqueSeed()))` — initial
855            // seed is irrelevant; every carver overwrites it via
856            // `set_large_feature_seed` before its probability check.
857            let mut random = LegacyRandom::from_seed(0);
858            let seed_i64 = self.seed;
859
860            let biome_zoom_seed = self.biome_zoom_seed;
861            // BiomeManager-fuzzed lookup — matches vanilla's `BiomeManager.getBiome`
862            // used by the carver's top-material path. An unfuzzed quart lookup
863            // would mismatch vanilla at quart-cell boundaries.
864            let mut biome_getter = |pos: BlockPos| -> u16 {
865                fuzzed_biome_at_block(biome_zoom_seed, pos, |q_pos| {
866                    biome_sampler.sample(q_pos.x, q_pos.y, q_pos.z).id() as u16
867                })
868            };
869
870            chunk.with_carving_mask(|mask| {
871                let mut run = CarveRun {
872                    ctx: &mut ctx,
873                    noises,
874                    chunk,
875                    chunk_min_x,
876                    chunk_min_z,
877                    biome_getter: &mut biome_getter,
878                    mask,
879                    ids,
880                };
881
882                run.run_all(&source_biomes, seed_i64, &mut random);
883            });
884        });
885    }
886
887    fn create_worldgen_region_random(&self, _world_seed: i64, center: ChunkPos) -> RandomSource {
888        worldgen_region_random_from_splitter(&self.splitter, center)
889    }
890
891    fn apply_biome_decorations(&self, region: &mut WorldGenRegion<'_>) {
892        self.feature_runner
893            .decorate(region, &REGISTRY, self.seed, self.biome_zoom_seed);
894    }
895}
896
897impl<N, F> CarveRun<'_, '_, N, F>
898where
899    N: DimensionNoises,
900    F: FnMut(BlockPos) -> u16,
901{
902    /// Drive the 17×17 source-chunk carver loop. Each carver in each source
903    /// biome is seeded via `set_large_feature_seed`, probability-checked,
904    /// then dispatched to the appropriate `carve_*` method.
905    fn run_all(&mut self, source_biomes: &[SourceChunk], seed_i64: i64, random: &mut LegacyRandom) {
906        for source in source_biomes {
907            for (index, carver_key) in source.biome.carvers.iter().enumerate() {
908                let Some(carver) = REGISTRY.configured_carvers.by_key(carver_key) else {
909                    panic!(
910                        "biome {} references unknown configured carver {}",
911                        source.biome.key, carver_key
912                    );
913                };
914                let index_i64 = index as i64;
915                random.set_large_feature_seed(
916                    seed_i64.wrapping_add(index_i64),
917                    source.pos.0.x,
918                    source.pos.0.y,
919                );
920
921                let probability = carver.base().probability;
922                if random.next_f32() > probability {
923                    continue;
924                }
925
926                match &carver.kind {
927                    ConfiguredCarverKind::Cave(cfg) => {
928                        self.carve_cave(cfg, cave::CaveKind::Overworld, source.pos, random);
929                    }
930                    ConfiguredCarverKind::NetherCave(cfg) => {
931                        self.carve_cave(cfg, cave::CaveKind::Nether, source.pos, random);
932                    }
933                    ConfiguredCarverKind::Canyon(cfg) => {
934                        self.carve_canyon(cfg, source.pos, random);
935                    }
936                }
937            }
938        }
939    }
940}
941
942// ── BiomeManager biome zoom helpers ──────────────────────────────────────────
943
944/// Vanilla's `LinearCongruentialGenerator.next()`.
945#[inline]
946const fn lcg_next(mut rval: i64, c: i64) -> i64 {
947    rval = rval.wrapping_mul(
948        rval.wrapping_mul(6_364_136_223_846_793_005)
949            .wrapping_add(1_442_695_040_888_963_407),
950    );
951    rval = rval.wrapping_add(c);
952    rval
953}
954
955/// Vanilla's `BiomeManager.getFiddle()`.
956#[inline]
957fn get_fiddle(rval: i64) -> f64 {
958    let uniform = ((rval >> 24).rem_euclid(1024)) as f64 / 1024.0;
959    (uniform - 0.5) * 0.9
960}
961
962/// Single-shot fuzzed biome lookup at a block position. Matches vanilla's
963/// `BiomeManager.getBiome(BlockPos)`: the block is shifted by `-2`, snapped to
964/// the enclosing quart cell, and the winning biome is chosen from the 8
965/// corners of that cell by `get_fiddle`-perturbed squared distance.
966///
967/// `quart_biome` returns the unfuzzed biome at a quart-coordinate — typically
968/// `biome_sampler.sample(qx, qy, qz).id()`.
969///
970/// Used by carver top-material lookups where a simple unfuzzed lookup would
971/// differ from vanilla at the quart-cell boundaries.
972pub(crate) fn fuzzed_biome_at_block<F: FnMut(IVec3) -> u16>(
973    biome_zoom_seed: i64,
974    pos: BlockPos,
975    mut quart_biome: F,
976) -> u16 {
977    let abs = pos.0 - IVec3::splat(2);
978    let parent = IVec3::new(abs.x >> 2, abs.y >> 2, abs.z >> 2);
979    let fract = DVec3::new(
980        f64::from(abs.x & 3),
981        f64::from(abs.y & 3),
982        f64::from(abs.z & 3),
983    ) / 4.0;
984
985    let mut min_i = 0usize;
986    let mut min_dist = f64::INFINITY;
987
988    for i in 0..8usize {
989        let x_even = (i & 4) == 0;
990        let y_even = (i & 2) == 0;
991        let z_even = (i & 1) == 0;
992        let cx = if x_even { parent.x } else { parent.x + 1 };
993        let cy = if y_even { parent.y } else { parent.y + 1 };
994        let cz = if z_even { parent.z } else { parent.z + 1 };
995        let dx = if x_even { fract.x } else { fract.x - 1.0 };
996        let dy = if y_even { fract.y } else { fract.y - 1.0 };
997        let dz = if z_even { fract.z } else { fract.z - 1.0 };
998
999        // BiomeManager.getFiddledDistance — identical sequence to
1000        // FuzzedBiomeColumn::compute_cy_group but without the column cache.
1001        let mut rval = lcg_next(biome_zoom_seed, i64::from(cx));
1002        rval = lcg_next(rval, i64::from(cy));
1003        rval = lcg_next(rval, i64::from(cz));
1004        rval = lcg_next(rval, i64::from(cx));
1005        rval = lcg_next(rval, i64::from(cy));
1006        rval = lcg_next(rval, i64::from(cz));
1007        let fx = get_fiddle(rval);
1008        rval = lcg_next(rval, biome_zoom_seed);
1009        let fy = get_fiddle(rval);
1010        rval = lcg_next(rval, biome_zoom_seed);
1011        let fz = get_fiddle(rval);
1012
1013        let dist = (dx + fx).powi(2) + (dy + fy).powi(2) + (dz + fz).powi(2);
1014        if min_dist > dist {
1015            min_i = i;
1016            min_dist = dist;
1017        }
1018    }
1019
1020    let b = IVec3::new(
1021        if (min_i & 4) == 0 {
1022            parent.x
1023        } else {
1024            parent.x + 1
1025        },
1026        if (min_i & 2) == 0 {
1027            parent.y
1028        } else {
1029            parent.y + 1
1030        },
1031        if (min_i & 1) == 0 {
1032            parent.z
1033        } else {
1034            parent.z + 1
1035        },
1036    );
1037    quart_biome(b)
1038}
1039
1040/// Column-local cache for fuzzed biome lookups (vanilla `BiomeManager.getBiome()`).
1041///
1042/// Within a column, `parent_x`, `parent_z`, `fract_x`, `fract_z` are constant.
1043/// The 8 Voronoi candidate fiddle values (computed via 8 serial LCG calls each)
1044/// only change when `parent_y` changes (every 4 blocks). This cache precomputes
1045/// the fiddle values and X/Z distance components per `parent_y` group, reducing
1046/// per-block work to 8 additions + 8 multiplies + 8 comparisons.
1047struct FuzzedBiomeColumn<'a> {
1048    biome_data: &'a [u16],
1049    section_count: usize,
1050    biome_zoom_seed: i64,
1051    parent_x: i32,
1052    parent_z: i32,
1053    fract_x: f64,
1054    fract_z: f64,
1055    min_y: i32,
1056    chunk_quart_x: i32,
1057    chunk_quart_z: i32,
1058    neighbor_biomes: &'a dyn Fn(IVec3) -> u16,
1059    cached_parent_y: i32,
1060    /// Per-candidate cached values: (`fy`, `xz_partial_distance`).
1061    candidates: [(f64, f64); 8],
1062    /// Precomputed `lcg_next(seed, parent_x)` and `lcg_next(seed, parent_x + 1)`.
1063    rval_after_cx: [i64; 2],
1064}
1065
1066impl<'a> FuzzedBiomeColumn<'a> {
1067    #[expect(
1068        clippy::too_many_arguments,
1069        reason = "matches vanilla BiomeManager.getBiome signature"
1070    )]
1071    fn new(
1072        biome_data: &'a [u16],
1073        section_count: usize,
1074        biome_zoom_seed: i64,
1075        block_x: i32,
1076        block_z: i32,
1077        min_y: i32,
1078        chunk_quart_x: i32,
1079        chunk_quart_z: i32,
1080        neighbor_biomes: &'a dyn Fn(IVec3) -> u16,
1081    ) -> Self {
1082        let abs_x = block_x - 2;
1083        let abs_z = block_z - 2;
1084        let parent_x = abs_x >> 2;
1085        let parent_z = abs_z >> 2;
1086        Self {
1087            biome_data,
1088            section_count,
1089            biome_zoom_seed,
1090            parent_x,
1091            parent_z,
1092            fract_x: f64::from(abs_x & 3) / 4.0,
1093            fract_z: f64::from(abs_z & 3) / 4.0,
1094            min_y,
1095            chunk_quart_x,
1096            chunk_quart_z,
1097            neighbor_biomes,
1098            cached_parent_y: i32::MIN,
1099            candidates: [(0.0, 0.0); 8],
1100            rval_after_cx: [
1101                lcg_next(biome_zoom_seed, i64::from(parent_x)),
1102                lcg_next(biome_zoom_seed, i64::from(parent_x + 1)),
1103            ],
1104        }
1105    }
1106
1107    /// Compute candidates for a given `cy`, writing to either the low (bit1=0)
1108    /// or high (bit1=1) slots. Shares the `lcg_next(seed, cx)` precomputation
1109    /// and the `lcg_next(_, cy)` step within each cx group.
1110    #[inline]
1111    fn compute_cy_group(&mut self, cy: i32, high: bool) {
1112        let base_idx = if high { 2 } else { 0 };
1113        for cx_idx in 0..2usize {
1114            let cx = self.parent_x + cx_idx as i32;
1115            let dx = if cx_idx == 0 {
1116                self.fract_x
1117            } else {
1118                self.fract_x - 1.0
1119            };
1120            let rval_cy = lcg_next(self.rval_after_cx[cx_idx], i64::from(cy));
1121            for cz_off in 0..2usize {
1122                let cz = self.parent_z + cz_off as i32;
1123                let dz = if cz_off == 0 {
1124                    self.fract_z
1125                } else {
1126                    self.fract_z - 1.0
1127                };
1128
1129                let mut rval = lcg_next(rval_cy, i64::from(cz));
1130                rval = lcg_next(rval, i64::from(cx));
1131                rval = lcg_next(rval, i64::from(cy));
1132                rval = lcg_next(rval, i64::from(cz));
1133                let fx = get_fiddle(rval);
1134                rval = lcg_next(rval, self.biome_zoom_seed);
1135                let fy = get_fiddle(rval);
1136                rval = lcg_next(rval, self.biome_zoom_seed);
1137                let fz = get_fiddle(rval);
1138
1139                let xz_partial = (dx + fx) * (dx + fx) + (dz + fz) * (dz + fz);
1140                self.candidates[cx_idx * 4 + base_idx + cz_off] = (fy, xz_partial);
1141            }
1142        }
1143    }
1144
1145    /// Recompute the 8 candidate fiddle values and X/Z distance for a new `parent_y`.
1146    ///
1147    /// When scanning downward (`parent_y` decreases by 1), the old low-cy candidates
1148    /// (`cy=old_parent_y`) match the new high-cy slots (`cy=new_parent_y+1`), so only
1149    /// the 4 new low-cy candidates need fresh LCG computation.
1150    fn recompute_candidates(&mut self, parent_y: i32) {
1151        if self.cached_parent_y != i32::MIN && parent_y == self.cached_parent_y - 1 {
1152            // Reuse: old low-cy group → new high-cy group
1153            self.candidates[2] = self.candidates[0];
1154            self.candidates[3] = self.candidates[1];
1155            self.candidates[6] = self.candidates[4];
1156            self.candidates[7] = self.candidates[5];
1157            self.compute_cy_group(parent_y, false);
1158        } else {
1159            self.compute_cy_group(parent_y, false);
1160            self.compute_cy_group(parent_y + 1, true);
1161        }
1162        self.cached_parent_y = parent_y;
1163    }
1164
1165    /// Fuzzed biome lookup for a given `block_y`.
1166    #[expect(
1167        clippy::similar_names,
1168        reason = "matches vanilla variable names: fract_x/y/z, parent_x/y/z"
1169    )]
1170    #[inline]
1171    fn get(&mut self, block_y: i32) -> u16 {
1172        let abs_y = block_y - 2;
1173        let parent_y = abs_y >> 2;
1174        let fract_y = f64::from(abs_y & 3) / 4.0;
1175
1176        if parent_y != self.cached_parent_y {
1177            self.recompute_candidates(parent_y);
1178        }
1179
1180        let mut min_i = 0usize;
1181        let mut min_dist = f64::INFINITY;
1182        for i in 0..8usize {
1183            let (fy, xz_partial) = self.candidates[i];
1184            let dy = if (i & 2) == 0 { fract_y } else { fract_y - 1.0 };
1185            let dist = xz_partial + (dy + fy) * (dy + fy);
1186            if min_dist > dist {
1187                min_i = i;
1188                min_dist = dist;
1189            }
1190        }
1191
1192        let biome_quart = IVec3::new(
1193            if (min_i & 4) == 0 {
1194                self.parent_x
1195            } else {
1196                self.parent_x + 1
1197            },
1198            if (min_i & 2) == 0 {
1199                parent_y
1200            } else {
1201                parent_y + 1
1202            },
1203            if (min_i & 1) == 0 {
1204                self.parent_z
1205            } else {
1206                self.parent_z + 1
1207            },
1208        );
1209
1210        let in_chunk = biome_quart.x >= self.chunk_quart_x
1211            && biome_quart.x < self.chunk_quart_x + 4
1212            && biome_quart.z >= self.chunk_quart_z
1213            && biome_quart.z < self.chunk_quart_z + 4;
1214
1215        if in_chunk {
1216            let min_qy = self.min_y >> 2;
1217            let total_quarts_y = self.section_count * 4;
1218            let local_qx = (biome_quart.x - self.chunk_quart_x) as usize;
1219            let local_qz = (biome_quart.z - self.chunk_quart_z) as usize;
1220            let qy_in_chunk = (biome_quart.y - min_qy).clamp(0, total_quarts_y as i32 - 1) as usize;
1221            let section_idx = qy_in_chunk / 4;
1222            let local_qy = qy_in_chunk % 4;
1223            self.biome_data[section_idx * 64 + local_qy * 16 + local_qz * 4 + local_qx]
1224        } else {
1225            (self.neighbor_biomes)(biome_quart)
1226        }
1227    }
1228}
1229
1230impl SurfaceBiomeProvider for FuzzedBiomeColumn<'_> {
1231    #[inline]
1232    fn biome_id(&mut self, block_y: i32) -> u16 {
1233        self.get(block_y)
1234    }
1235}
1236
1237#[cfg(test)]
1238mod tests {
1239    use std::sync::Weak;
1240
1241    use glam::IVec3;
1242    use steel_registry::{init_vanilla_registry, vanilla_dimension_types};
1243    use steel_worldgen::biomes::BiomeSourceKind;
1244
1245    use crate::behavior::init_behaviors;
1246    use crate::chunk::{
1247        Chunk,
1248        heightmap::HeightmapType,
1249        section::{ChunkSection, Sections},
1250    };
1251    use crate::worldgen::carving_mask::CarvingMask;
1252    use crate::worldgen::generator::{
1253        CarversPhase, ChunkGenerator as _, GenerationChunk, NoisePhase, SurfacePhase,
1254        context::OverworldGenerator,
1255    };
1256
1257    fn make_overworld_chunk() -> Chunk {
1258        let dimension = &vanilla_dimension_types::OVERWORLD;
1259        let sections = (0..dimension.height / 16)
1260            .map(|_| ChunkSection::new_empty())
1261            .collect::<Vec<_>>()
1262            .into_boxed_slice();
1263        Chunk::new(
1264            Sections::from_owned(sections),
1265            steel_utils::ChunkPos::new(0, 0),
1266            dimension.min_y,
1267            dimension.height,
1268            Weak::new(),
1269        )
1270    }
1271
1272    fn self_neighbor_biome(chunk: &Chunk, quart: IVec3) -> u16 {
1273        let sections = chunk.sections();
1274        let min_quart_y = chunk.min_y() >> 2;
1275        let quart_y =
1276            (quart.y - min_quart_y).clamp(0, (sections.sections.len() * 4) as i32 - 1) as usize;
1277        sections.sections[quart_y / 4].read().biomes.get(
1278            (quart.x & 3) as usize,
1279            quart_y % 4,
1280            (quart.z & 3) as usize,
1281        )
1282    }
1283
1284    fn blocks(chunk: &Chunk) -> Vec<steel_utils::BlockStateId> {
1285        let mut blocks = Vec::with_capacity((chunk.height() * 16 * 16) as usize);
1286        for relative_y in 0..chunk.height() as usize {
1287            for z in 0..16 {
1288                for x in 0..16 {
1289                    let Some(state) = chunk.get_relative_block(x, relative_y, z) else {
1290                        panic!("test coordinates must stay inside the chunk");
1291                    };
1292                    blocks.push(state);
1293                }
1294            }
1295        }
1296        blocks
1297    }
1298
1299    fn has_overworld_post_noise_state(chunk: &Chunk) -> bool {
1300        chunk
1301            .with_transient_generation_state_mut::<
1302                super::SteelPostNoiseState<super::OverworldNoises>,
1303                _,
1304            >(|_| ())
1305            .is_some()
1306    }
1307
1308    #[test]
1309    fn retained_and_rebuilt_aquifers_produce_identical_carver_output() {
1310        init_vanilla_registry();
1311        init_behaviors();
1312        let pool = rayon::ThreadPoolBuilder::new()
1313            .num_threads(1)
1314            .build()
1315            .expect("test generation pool should build");
1316        let generator = OverworldGenerator::new(None, BiomeSourceKind::overworld(0), 0, &pool);
1317        let warm = make_overworld_chunk();
1318        let cold = make_overworld_chunk();
1319
1320        for chunk in [&warm, &cold] {
1321            generator.create_biomes(chunk);
1322            generator.fill_from_noise(GenerationChunk::<NoisePhase>::for_test(chunk), None);
1323        }
1324        assert!(has_overworld_post_noise_state(&warm));
1325        assert!(has_overworld_post_noise_state(&cold));
1326
1327        cold.clear_transient_generation_state();
1328        for chunk in [&warm, &cold] {
1329            generator.build_surface(GenerationChunk::<SurfacePhase>::for_test(chunk), &|quart| {
1330                self_neighbor_biome(chunk, quart)
1331            });
1332        }
1333        assert!(has_overworld_post_noise_state(&warm));
1334        assert!(!has_overworld_post_noise_state(&cold));
1335
1336        generator.apply_carvers(GenerationChunk::<CarversPhase>::for_test(&warm));
1337        generator.apply_carvers(GenerationChunk::<CarversPhase>::for_test(&cold));
1338        assert!(!has_overworld_post_noise_state(&warm));
1339        assert!(!has_overworld_post_noise_state(&cold));
1340
1341        assert_eq!(blocks(&warm), blocks(&cold));
1342        assert_eq!(
1343            warm.carving_mask
1344                .read()
1345                .as_ref()
1346                .map(CarvingMask::to_packed_u64s),
1347            cold.carving_mask
1348                .read()
1349                .as_ref()
1350                .map(CarvingMask::to_packed_u64s)
1351        );
1352        assert_eq!(&*warm.postprocessing.lock(), &*cold.postprocessing.lock());
1353        for x in 0..16 {
1354            for z in 0..16 {
1355                assert_eq!(
1356                    warm.generation_height_at(HeightmapType::WorldSurfaceWg, x, z),
1357                    cold.generation_height_at(HeightmapType::WorldSurfaceWg, x, z)
1358                );
1359            }
1360        }
1361    }
1362}