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
48pub trait VanillaPostNoiseStateType: DimensionNoises + 'static {
53 type State: DowncastType + Send + Sync;
55
56 fn wrap_post_noise_aquifer(aquifer: Aquifer<Self>) -> Self::State;
58
59 fn post_noise_aquifer(state: &mut Self::State) -> &mut Aquifer<Self>;
61}
62
63#[doc(hidden)]
65pub struct SteelPostNoiseState<N: DimensionNoises> {
66 aquifer: Aquifer<N>,
67}
68
69unsafe impl DowncastType for SteelPostNoiseState<OverworldNoises> {
71 const TYPE_KEY: DowncastTypeKey =
72 DowncastTypeKey::new("steel:worldgen_state/post_noise_overworld");
73}
74
75unsafe impl DowncastType for SteelPostNoiseState<NetherNoises> {
77 const TYPE_KEY: DowncastTypeKey =
78 DowncastTypeKey::new("steel:worldgen_state/post_noise_nether");
79}
80
81unsafe 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
106pub struct VanillaGenerator<N: DimensionNoises> {
115 biome_source: BiomeSourceKind,
117 uniform_carver_biome: Option<BiomeRef>,
126 noises: Box<N>,
129 splitter: RandomSplitter,
131 ore_veinifier: Option<OreVeinifier>,
133 surface_system: SurfaceSystem,
135 surface_extension_biomes: SurfaceExtensionBiomes,
137 default_block_id: BlockStateId,
139 biome_zoom_seed: i64,
141 seed: i64,
143 structure_generator: StructureGenerator,
145 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 #[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 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 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, ®ISTRY);
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 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 sampler.init_grid(chunk_x * 16, chunk_z * 16);
363
364 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 column_cache.clone(),
428 );
429
430 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 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 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 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 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 let mut start_height = chunk.world_surface_height_at(local_x, local_z);
564
565 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 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 chunk.read_column_into(local_x, local_z, &mut column_buf);
605
606 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 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 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 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 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 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 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 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 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 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 let mut random = LegacyRandom::from_seed(0);
858 let seed_i64 = self.seed;
859
860 let biome_zoom_seed = self.biome_zoom_seed;
861 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, ®ISTRY, 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 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#[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#[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
962pub(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 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
1040struct 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 candidates: [(f64, f64); 8],
1062 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 #[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 fn recompute_candidates(&mut self, parent_y: i32) {
1151 if self.cached_parent_y != i32::MIN && parent_y == self.cached_parent_y - 1 {
1152 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 #[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}