Skip to main content

steel_core/worldgen/generator/
vanilla.rs

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