1use rustc_hash::FxHashMap;
7use steel_registry::biome::TemperatureModifier;
8use steel_registry::blocks::block_state_ext::BlockStateExt;
9use steel_registry::vanilla_blocks;
10use steel_registry::{REGISTRY, RegistryExt};
11use steel_utils::BlockStateId;
12use steel_utils::random::legacy_random::LegacyRandom;
13use steel_utils::random::name_hash::NameHash;
14use steel_utils::random::{PositionalRandom, Random, RandomSource, RandomSplitter};
15use steel_worldgen::density::NoiseParameters;
16use steel_worldgen::noise::{NormalNoise, PerlinSimplexNoise};
17use steel_worldgen::surface::SurfaceNoiseProvider;
18
19use crate::worldgen::generator::{GenerationChunk, SurfacePhase};
20
21const CLAY_BAND_LENGTH: usize = 192;
22
23pub struct TemperatureXzCache {
36 block_x: i32,
37 block_z: i32,
38 frozen_large_x7: f64,
39 frozen_edge: f64,
40 frozen_small: f64,
41 height_temp_noise_x8: f64,
42}
43
44impl TemperatureXzCache {
45 #[must_use]
47 pub const fn new(block_x: i32, block_z: i32) -> Self {
48 Self {
49 block_x,
50 block_z,
51 frozen_large_x7: f64::NAN,
52 frozen_edge: f64::NAN,
53 frozen_small: f64::NAN,
54 height_temp_noise_x8: f64::NAN,
55 }
56 }
57}
58
59pub struct SurfaceSystem {
64 surface_noise: NormalNoise,
66 surface_secondary_noise: NormalNoise,
68 clay_bands_offset_noise: NormalNoise,
70 clay_bands: [BlockStateId; CLAY_BAND_LENGTH],
72 noise_random: RandomSplitter,
74 condition_noises: Vec<NormalNoise>,
77 vertical_gradient_randoms: Vec<RandomSplitter>,
79
80 badlands_pillar_noise: NormalNoise,
82 badlands_pillar_roof_noise: NormalNoise,
83 badlands_surface_noise: NormalNoise,
84 iceberg_pillar_noise: NormalNoise,
85 iceberg_pillar_roof_noise: NormalNoise,
86 iceberg_surface_noise: NormalNoise,
87
88 temperature_noise: PerlinSimplexNoise,
91 frozen_temperature_noise: PerlinSimplexNoise,
93 biome_info_noise: PerlinSimplexNoise,
95
96 pub default_block: BlockStateId,
98 pub sea_level: i32,
100}
101
102impl SurfaceSystem {
103 #[must_use]
108 pub fn new(
109 splitter: &RandomSplitter,
110 noise_params: &FxHashMap<String, NoiseParameters>,
111 condition_noise_ids: &[&str],
112 vertical_gradient_ids: &[&str],
113 default_block: BlockStateId,
114 sea_level: i32,
115 ) -> Self {
116 const CLAY_BANDS_HASH: NameHash = NameHash::new("minecraft:clay_bands");
118
119 let noise_random = splitter.clone();
122 let mut band_random = noise_random.with_hash_of(&CLAY_BANDS_HASH);
123 let clay_bands = Self::generate_bands(&mut band_random);
124
125 let condition_noises: Vec<NormalNoise> = condition_noise_ids
128 .iter()
129 .map(|&id| create_noise(splitter, id, noise_params))
130 .collect();
131 let vertical_gradient_randoms: Vec<RandomSplitter> = vertical_gradient_ids
132 .iter()
133 .map(|&id| {
134 let hash = NameHash::new(id);
135 let mut random = splitter.with_hash_of(&hash);
136 random.next_positional()
137 })
138 .collect();
139
140 Self {
141 surface_noise: create_noise(splitter, "minecraft:surface", noise_params),
142 surface_secondary_noise: create_noise(
143 splitter,
144 "minecraft:surface_secondary",
145 noise_params,
146 ),
147 clay_bands_offset_noise: create_noise(
148 splitter,
149 "minecraft:clay_bands_offset",
150 noise_params,
151 ),
152 clay_bands,
153 noise_random,
154 condition_noises,
155 vertical_gradient_randoms,
156 badlands_pillar_noise: create_noise(
157 splitter,
158 "minecraft:badlands_pillar",
159 noise_params,
160 ),
161 badlands_pillar_roof_noise: create_noise(
162 splitter,
163 "minecraft:badlands_pillar_roof",
164 noise_params,
165 ),
166 badlands_surface_noise: create_noise(
167 splitter,
168 "minecraft:badlands_surface",
169 noise_params,
170 ),
171 iceberg_pillar_noise: create_noise(splitter, "minecraft:iceberg_pillar", noise_params),
172 iceberg_pillar_roof_noise: create_noise(
173 splitter,
174 "minecraft:iceberg_pillar_roof",
175 noise_params,
176 ),
177 iceberg_surface_noise: create_noise(
178 splitter,
179 "minecraft:iceberg_surface",
180 noise_params,
181 ),
182 temperature_noise: {
184 let mut rng = RandomSource::Legacy(LegacyRandom::from_seed(1234));
185 PerlinSimplexNoise::new(&mut rng, &[0])
186 },
187 frozen_temperature_noise: {
188 let mut rng = RandomSource::Legacy(LegacyRandom::from_seed(3456));
189 PerlinSimplexNoise::new(&mut rng, &[-2, -1, 0])
190 },
191 biome_info_noise: {
192 let mut rng = RandomSource::Legacy(LegacyRandom::from_seed(2345));
193 PerlinSimplexNoise::new(&mut rng, &[0])
194 },
195 default_block,
196 sea_level,
197 }
198 }
199
200 #[must_use]
205 pub fn get_surface_depth(&self, x: i32, z: i32) -> i32 {
206 let noise_value = self
207 .surface_noise
208 .get_value(f64::from(x), 0.0, f64::from(z));
209 let jitter = self.noise_random.at(x, 0, z).next_f64() * 0.25;
210 (noise_value * 2.75 + 3.0 + jitter) as i32
211 }
212
213 #[must_use]
215 pub fn get_surface_secondary(&self, x: i32, z: i32) -> f64 {
216 self.surface_secondary_noise
217 .get_value(f64::from(x), 0.0, f64::from(z))
218 }
219
220 #[inline]
225 fn frozen_large_x7(&self, xz: &mut TemperatureXzCache) -> f64 {
226 if !xz.frozen_large_x7.is_nan() {
227 return xz.frozen_large_x7;
228 }
229 let v = self
230 .frozen_temperature_noise
231 .get_value(f64::from(xz.block_x) * 0.05, f64::from(xz.block_z) * 0.05)
232 * 7.0;
233 xz.frozen_large_x7 = v;
234 v
235 }
236
237 #[inline]
239 fn frozen_edge(&self, xz: &mut TemperatureXzCache) -> f64 {
240 if !xz.frozen_edge.is_nan() {
241 return xz.frozen_edge;
242 }
243 let v = self
244 .biome_info_noise
245 .get_value(f64::from(xz.block_x) * 0.2, f64::from(xz.block_z) * 0.2);
246 xz.frozen_edge = v;
247 v
248 }
249
250 #[inline]
252 fn frozen_small(&self, xz: &mut TemperatureXzCache) -> f64 {
253 if !xz.frozen_small.is_nan() {
254 return xz.frozen_small;
255 }
256 let v = self
257 .biome_info_noise
258 .get_value(f64::from(xz.block_x) * 0.09, f64::from(xz.block_z) * 0.09);
259 xz.frozen_small = v;
260 v
261 }
262
263 #[inline]
265 fn height_temp_noise_x8(&self, xz: &mut TemperatureXzCache) -> f64 {
266 if !xz.height_temp_noise_x8.is_nan() {
267 return xz.height_temp_noise_x8;
268 }
269 let v = self
270 .temperature_noise
271 .get_value(f64::from(xz.block_x) / 8.0, f64::from(xz.block_z) / 8.0)
272 * 8.0;
273 xz.height_temp_noise_x8 = v;
274 v
275 }
276
277 fn get_temperature(&self, biome_id: u16, block_y: i32, xz: &mut TemperatureXzCache) -> f32 {
288 let biome = REGISTRY
289 .biomes
290 .by_id(biome_id as usize)
291 .expect("invalid biome id");
292 let base_temp = biome.temperature;
293
294 let modified_temp = match biome.temperature_modifier {
296 TemperatureModifier::None => base_temp,
297 TemperatureModifier::Frozen => {
298 let combined = self.frozen_large_x7(xz) + self.frozen_edge(xz);
299 if combined < 0.3 {
300 if self.frozen_small(xz) < 0.8 {
301 0.2 } else {
303 base_temp
304 }
305 } else {
306 base_temp
307 }
308 }
309 };
310
311 let snow_level = self.sea_level + 17;
313 if block_y > snow_level {
314 let v = self.height_temp_noise_x8(xz) as f32;
315 modified_temp - (v + block_y as f32 - snow_level as f32) * 0.05 / 40.0
316 } else {
317 modified_temp
318 }
319 }
320
321 #[must_use]
327 pub fn cold_enough_to_snow(
328 &self,
329 biome_id: u16,
330 block_y: i32,
331 xz: &mut TemperatureXzCache,
332 ) -> bool {
333 self.get_temperature(biome_id, block_y, xz) < 0.15
334 }
335
336 fn should_melt_frozen_ocean_iceberg_slightly(
341 &self,
342 biome_id: u16,
343 block_x: i32,
344 block_z: i32,
345 ) -> bool {
346 let mut xz = TemperatureXzCache::new(block_x, block_z);
347 self.get_temperature(biome_id, self.sea_level, &mut xz) > 0.1
348 }
349
350 fn generate_bands(random: &mut RandomSource) -> [BlockStateId; CLAY_BAND_LENGTH] {
356 let terracotta = vanilla_blocks::TERRACOTTA.default_state();
357 let orange = vanilla_blocks::ORANGE_TERRACOTTA.default_state();
358 let yellow = vanilla_blocks::YELLOW_TERRACOTTA.default_state();
359 let brown = vanilla_blocks::BROWN_TERRACOTTA.default_state();
360 let red = vanilla_blocks::RED_TERRACOTTA.default_state();
361 let white = vanilla_blocks::WHITE_TERRACOTTA.default_state();
362 let light_gray = vanilla_blocks::LIGHT_GRAY_TERRACOTTA.default_state();
363
364 let mut bands = [terracotta; CLAY_BAND_LENGTH];
365
366 let mut i = 0usize;
369 while i < CLAY_BAND_LENGTH {
370 i += random.next_i32_bounded(5) as usize + 1;
371 if i < CLAY_BAND_LENGTH {
372 bands[i] = orange;
373 }
374 i += 1;
375 }
376
377 Self::make_bands(random, &mut bands, 1, yellow);
378 Self::make_bands(random, &mut bands, 2, brown);
379 Self::make_bands(random, &mut bands, 1, red);
380
381 let white_count = random.next_i32_between(9, 15);
383 let mut placed = 0;
384 let mut start = 0usize;
385 while placed < white_count && start < CLAY_BAND_LENGTH {
386 bands[start] = white;
387 if start > 1 && random.next_bool() {
388 bands[start - 1] = light_gray;
389 }
390 if start + 1 < CLAY_BAND_LENGTH && random.next_bool() {
391 bands[start + 1] = light_gray;
392 }
393 placed += 1;
394 start += random.next_i32_bounded(16) as usize + 4;
395 }
396
397 bands
398 }
399
400 fn make_bands(
404 random: &mut RandomSource,
405 bands: &mut [BlockStateId; CLAY_BAND_LENGTH],
406 base_width: i32,
407 state: BlockStateId,
408 ) {
409 let band_count = random.next_i32_between(6, 15);
410 for _ in 0..band_count {
411 let width = (base_width + random.next_i32_bounded(3)) as usize;
412 let start = random.next_i32_bounded(CLAY_BAND_LENGTH as i32) as usize;
413 for p in 0..width {
414 if start + p >= CLAY_BAND_LENGTH {
415 break;
416 }
417 bands[start + p] = state;
418 }
419 }
420 }
421}
422
423impl SurfaceSystem {
424 #[expect(
429 clippy::too_many_arguments,
430 reason = "matches vanilla SurfaceSystem.erodedBadlandsExtension signature"
431 )]
432 #[must_use]
433 pub fn eroded_badlands_extension(
434 &self,
435 chunk: GenerationChunk<'_, SurfacePhase>,
436 local_x: usize,
437 local_z: usize,
438 block_x: i32,
439 block_z: i32,
440 height: i32,
441 min_y: i32,
442 ) -> i32 {
443 let pillar_buffer = f64::min(
444 (self
445 .badlands_surface_noise
446 .get_value(f64::from(block_x), 0.0, f64::from(block_z))
447 * 8.25)
448 .abs(),
449 self.badlands_pillar_noise.get_value(
450 f64::from(block_x) * 0.2,
451 0.0,
452 f64::from(block_z) * 0.2,
453 ) * 15.0,
454 );
455
456 if pillar_buffer <= 0.0 {
457 return height;
458 }
459
460 let pillar_floor = (self.badlands_pillar_roof_noise.get_value(
461 f64::from(block_x) * 0.75,
462 0.0,
463 f64::from(block_z) * 0.75,
464 ) * 1.5)
465 .abs();
466
467 let extension_top = 64.0
468 + f64::min(
469 pillar_buffer * pillar_buffer * 2.5,
470 (pillar_floor * 50.0).ceil() + 24.0,
471 );
472 let start_y = extension_top.floor() as i32;
473
474 if height > start_y {
475 return height;
476 }
477
478 for y in (min_y..=start_y).rev() {
480 let rel_y = (y - min_y) as usize;
481 let state = chunk
482 .get_relative_block(local_x, rel_y, local_z)
483 .unwrap_or(BlockStateId(0));
484 if state == self.default_block {
485 break;
486 }
487 if state.get_block().config.liquid {
488 return height; }
490 }
491
492 for y in (min_y..=start_y).rev() {
494 let rel_y = (y - min_y) as usize;
495 let state = chunk
496 .get_relative_block(local_x, rel_y, local_z)
497 .unwrap_or(BlockStateId(0));
498 if !state.is_air() {
499 break;
500 }
501 chunk.set_relative_block(local_x, rel_y, local_z, self.default_block);
502 }
503
504 start_y + 1
506 }
507
508 #[expect(
513 clippy::too_many_arguments,
514 reason = "keeps the vanilla frozenOceanExtension inputs explicit"
515 )]
516 pub fn collect_frozen_ocean_extension_writes(
517 &self,
518 biome_id: u16,
519 block_x: i32,
520 block_z: i32,
521 height: i32,
522 min_surface_level: i32,
523 min_y: i32,
524 column: &[BlockStateId],
525 writes: &mut Vec<(usize, BlockStateId)>,
526 ) {
527 let iceberg = f64::min(
528 (self
529 .iceberg_surface_noise
530 .get_value(f64::from(block_x), 0.0, f64::from(block_z))
531 * 8.25)
532 .abs(),
533 self.iceberg_pillar_noise.get_value(
534 f64::from(block_x) * 1.28,
535 0.0,
536 f64::from(block_z) * 1.28,
537 ) * 15.0,
538 );
539
540 if iceberg <= 1.8 {
541 return;
542 }
543
544 let iceberg_roof = (self.iceberg_pillar_roof_noise.get_value(
545 f64::from(block_x) * 1.17,
546 0.0,
547 f64::from(block_z) * 1.17,
548 ) * 1.5)
549 .abs();
550
551 let mut top = f64::min(iceberg * iceberg * 1.2, (iceberg_roof * 40.0).ceil() + 14.0);
552
553 if self.should_melt_frozen_ocean_iceberg_slightly(biome_id, block_x, block_z) {
554 top -= 2.0;
555 }
556
557 let extension_bottom;
558 if top > 2.0 {
559 extension_bottom = f64::from(self.sea_level) - top - 7.0;
560 top += f64::from(self.sea_level);
561 } else {
562 top = 0.0;
563 extension_bottom = 0.0;
564 }
565
566 let extension_top = top;
567 let mut random = self.noise_random.at(block_x, 0, block_z);
568 let max_snow_depth = 2 + random.next_i32_bounded(4);
569 let min_snow_height = self.sea_level + 18 + random.next_i32_bounded(10);
570 let mut snow_depth = 0;
571
572 let snow_block = vanilla_blocks::SNOW_BLOCK.default_state();
573 let packed_ice = vanilla_blocks::PACKED_ICE.default_state();
574 let air = vanilla_blocks::AIR.default_state();
575
576 let start_y = i32::max(height, top as i32 + 1);
577 for y in (min_surface_level..=start_y).rev() {
578 let rel_y = (y - min_y) as usize;
579 let state = column.get(rel_y).copied().unwrap_or(air);
580
581 let is_air = state.is_air();
582 let is_water = state.get_block() == &vanilla_blocks::WATER;
583
584 if (is_air && y < extension_top as i32 && random.next_f64() > 0.01)
585 || (is_water
586 && y > extension_bottom as i32
587 && y < self.sea_level
588 && extension_bottom != 0.0
589 && random.next_f64() > 0.15)
590 {
591 if snow_depth <= max_snow_depth && y > min_snow_height {
592 writes.push((rel_y, snow_block));
593 snow_depth += 1;
594 } else {
595 writes.push((rel_y, packed_ice));
596 }
597 }
598 }
599 }
600}
601
602impl SurfaceNoiseProvider for SurfaceSystem {
603 fn condition_noise(&self, noise_index: usize, x: i32, z: i32) -> f64 {
604 self.condition_noises[noise_index].get_value(f64::from(x), 0.0, f64::from(z))
605 }
606
607 fn condition_noise_3d(&self, noise_index: usize, x: i32, y: i32, z: i32) -> f64 {
608 self.condition_noises[noise_index].get_value(f64::from(x), f64::from(y), f64::from(z))
609 }
610
611 fn get_band(&self, x: i32, y: i32, z: i32) -> BlockStateId {
612 let offset = (self
614 .clay_bands_offset_noise
615 .get_value(f64::from(x), 0.0, f64::from(z))
616 * 4.0
617 + 0.5)
618 .floor() as i32;
619 let index = ((y + offset) % CLAY_BAND_LENGTH as i32 + CLAY_BAND_LENGTH as i32) as usize
620 % CLAY_BAND_LENGTH;
621 self.clay_bands[index]
622 }
623
624 fn cold_enough_to_snow(&self, biome_id: u16, block_x: i32, block_y: i32, block_z: i32) -> bool {
625 let mut xz = TemperatureXzCache::new(block_x, block_z);
626 SurfaceSystem::cold_enough_to_snow(self, biome_id, block_y, &mut xz)
627 }
628
629 fn vertical_gradient(
630 &self,
631 gradient_index: usize,
632 block_x: i32,
633 block_y: i32,
634 block_z: i32,
635 true_at_and_below: i32,
636 false_at_and_above: i32,
637 ) -> bool {
638 if block_y <= true_at_and_below {
639 return true;
640 }
641 if block_y >= false_at_and_above {
642 return false;
643 }
644 let probability = f64::from(false_at_and_above - block_y)
646 / f64::from(false_at_and_above - true_at_and_below);
647
648 let factory = &self.vertical_gradient_randoms[gradient_index];
651 let random_value = f64::from(factory.at(block_x, block_y, block_z).next_f32());
652 random_value < probability
653 }
654}
655
656fn create_noise(
658 splitter: &RandomSplitter,
659 id: &str,
660 params: &FxHashMap<String, NoiseParameters>,
661) -> NormalNoise {
662 let p = params
663 .get(id)
664 .unwrap_or_else(|| panic!("Missing noise parameters for {id}"));
665 NormalNoise::create(splitter, id, p.first_octave, &p.amplitudes)
666}