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, TerrainPhase};
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: f32,
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: f32::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 = f64::from(
207 self.surface_noise
208 .get_value(f64::from(x), 0.0, f64::from(z)),
209 );
210 let jitter = self.noise_random.at(x, 0, z).next_f64() * 0.25;
211 (noise_value * 2.75 + 3.0 + jitter) as i32
212 }
213
214 #[must_use]
216 pub fn get_surface_secondary(&self, x: i32, z: i32) -> f64 {
217 f64::from(
218 self.surface_secondary_noise
219 .get_value(f64::from(x), 0.0, f64::from(z)),
220 )
221 }
222
223 #[inline]
228 fn frozen_large_x7(&self, xz: &mut TemperatureXzCache) -> f64 {
229 if !xz.frozen_large_x7.is_nan() {
230 return xz.frozen_large_x7;
231 }
232 let v = f64::from(
233 self.frozen_temperature_noise
234 .get_value(f64::from(xz.block_x) * 0.05, f64::from(xz.block_z) * 0.05)
235 * 7.0,
236 );
237 xz.frozen_large_x7 = v;
238 v
239 }
240
241 #[inline]
243 fn frozen_edge(&self, xz: &mut TemperatureXzCache) -> f64 {
244 if !xz.frozen_edge.is_nan() {
245 return xz.frozen_edge;
246 }
247 let v = f64::from(
248 self.biome_info_noise
249 .get_value(f64::from(xz.block_x) * 0.2, f64::from(xz.block_z) * 0.2),
250 );
251 xz.frozen_edge = v;
252 v
253 }
254
255 #[inline]
257 fn frozen_small(&self, xz: &mut TemperatureXzCache) -> f64 {
258 if !xz.frozen_small.is_nan() {
259 return xz.frozen_small;
260 }
261 let v = f64::from(
262 self.biome_info_noise
263 .get_value(f64::from(xz.block_x) * 0.09, f64::from(xz.block_z) * 0.09),
264 );
265 xz.frozen_small = v;
266 v
267 }
268
269 #[inline]
271 fn height_temp_noise_x8(&self, xz: &mut TemperatureXzCache) -> f32 {
272 if !xz.height_temp_noise_x8.is_nan() {
273 return xz.height_temp_noise_x8;
274 }
275 let v = self.temperature_noise.get_value(
276 f64::from(xz.block_x as f32 / 8.0),
277 f64::from(xz.block_z as f32 / 8.0),
278 ) * 8.0;
279 xz.height_temp_noise_x8 = v;
280 v
281 }
282
283 fn get_temperature(&self, biome_id: u16, block_y: i32, xz: &mut TemperatureXzCache) -> f32 {
294 let biome = REGISTRY
295 .biomes
296 .by_id(biome_id as usize)
297 .expect("invalid biome id");
298 let base_temp = biome.temperature;
299
300 let modified_temp = match biome.temperature_modifier {
302 TemperatureModifier::None => base_temp,
303 TemperatureModifier::Frozen => {
304 let combined = self.frozen_large_x7(xz) + self.frozen_edge(xz);
305 if combined < 0.3 {
306 if self.frozen_small(xz) < 0.8 {
307 0.2 } else {
309 base_temp
310 }
311 } else {
312 base_temp
313 }
314 }
315 };
316
317 let snow_level = self.sea_level + 17;
319 if block_y > snow_level {
320 let v = self.height_temp_noise_x8(xz);
321 modified_temp - (v + block_y as f32 - snow_level as f32) * 0.05 / 40.0
322 } else {
323 modified_temp
324 }
325 }
326
327 #[must_use]
333 pub fn cold_enough_to_snow(
334 &self,
335 biome_id: u16,
336 block_y: i32,
337 xz: &mut TemperatureXzCache,
338 ) -> bool {
339 self.get_temperature(biome_id, block_y, xz) < 0.15
340 }
341
342 fn should_melt_frozen_ocean_iceberg_slightly(
346 &self,
347 biome_id: u16,
348 block_x: i32,
349 block_z: i32,
350 ) -> bool {
351 let mut xz = TemperatureXzCache::new(block_x, block_z);
352 self.get_temperature(biome_id, self.sea_level, &mut xz) > 0.1
353 }
354
355 fn generate_bands(random: &mut RandomSource) -> [BlockStateId; CLAY_BAND_LENGTH] {
359 let terracotta = vanilla_blocks::TERRACOTTA.default_state();
360 let orange = vanilla_blocks::ORANGE_TERRACOTTA.default_state();
361 let yellow = vanilla_blocks::YELLOW_TERRACOTTA.default_state();
362 let brown = vanilla_blocks::BROWN_TERRACOTTA.default_state();
363 let red = vanilla_blocks::RED_TERRACOTTA.default_state();
364 let white = vanilla_blocks::WHITE_TERRACOTTA.default_state();
365 let light_gray = vanilla_blocks::LIGHT_GRAY_TERRACOTTA.default_state();
366
367 let mut bands = [terracotta; CLAY_BAND_LENGTH];
368
369 let mut i = 0usize;
372 while i < CLAY_BAND_LENGTH {
373 i += random.next_i32_bounded(5) as usize + 1;
374 if i < CLAY_BAND_LENGTH {
375 bands[i] = orange;
376 }
377 i += 1;
378 }
379
380 Self::make_bands(random, &mut bands, 1, yellow);
381 Self::make_bands(random, &mut bands, 2, brown);
382 Self::make_bands(random, &mut bands, 1, red);
383
384 let white_count = random.next_i32_between(9, 15);
386 let mut placed = 0;
387 let mut start = 0usize;
388 while placed < white_count && start < CLAY_BAND_LENGTH {
389 bands[start] = white;
390 if start > 1 && random.next_bool() {
391 bands[start - 1] = light_gray;
392 }
393 if start + 1 < CLAY_BAND_LENGTH && random.next_bool() {
394 bands[start + 1] = light_gray;
395 }
396 placed += 1;
397 start += random.next_i32_bounded(16) as usize + 4;
398 }
399
400 bands
401 }
402
403 fn make_bands(
405 random: &mut RandomSource,
406 bands: &mut [BlockStateId; CLAY_BAND_LENGTH],
407 base_width: i32,
408 state: BlockStateId,
409 ) {
410 let band_count = random.next_i32_between(6, 15);
411 for _ in 0..band_count {
412 let width = (base_width + random.next_i32_bounded(3)) as usize;
413 let start = random.next_i32_bounded(CLAY_BAND_LENGTH as i32) as usize;
414 for p in 0..width {
415 if start + p >= CLAY_BAND_LENGTH {
416 break;
417 }
418 bands[start + p] = state;
419 }
420 }
421 }
422}
423
424impl SurfaceSystem {
425 #[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<'_, TerrainPhase>,
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 (f64::from(self.badlands_surface_noise.get_value(
445 f64::from(block_x),
446 0.0,
447 f64::from(block_z),
448 )) * 8.25)
449 .abs(),
450 f64::from(
451 self.badlands_pillar_noise.get_value(
452 f64::from(block_x) * 0.2,
453 0.0,
454 f64::from(block_z) * 0.2,
455 ) * 15.0_f32,
456 ),
457 );
458
459 if pillar_buffer <= 0.0 {
460 return height;
461 }
462
463 let pillar_floor = (f64::from(self.badlands_pillar_roof_noise.get_value(
464 f64::from(block_x) * 0.75,
465 0.0,
466 f64::from(block_z) * 0.75,
467 )) * 1.5)
468 .abs();
469
470 let extension_top = 64.0
471 + f64::min(
472 pillar_buffer * pillar_buffer * 2.5,
473 (pillar_floor * 50.0).ceil() + 24.0,
474 );
475 let start_y = extension_top.floor() as i32;
476
477 if height > start_y {
478 return height;
479 }
480
481 for y in (min_y..=start_y).rev() {
483 let rel_y = (y - min_y) as usize;
484 let state = chunk
485 .get_relative_block(local_x, rel_y, local_z)
486 .unwrap_or(BlockStateId(0));
487 if state == self.default_block {
488 break;
489 }
490 if state.get_block().config.liquid {
491 return height; }
493 }
494
495 for y in (min_y..=start_y).rev() {
497 let rel_y = (y - min_y) as usize;
498 let state = chunk
499 .get_relative_block(local_x, rel_y, local_z)
500 .unwrap_or(BlockStateId(0));
501 if !state.is_air() {
502 break;
503 }
504 chunk.set_relative_block(local_x, rel_y, local_z, self.default_block);
505 }
506
507 start_y + 1
509 }
510
511 #[expect(
516 clippy::too_many_arguments,
517 reason = "keeps the vanilla frozenOceanExtension inputs explicit"
518 )]
519 pub fn collect_frozen_ocean_extension_writes(
520 &self,
521 biome_id: u16,
522 block_x: i32,
523 block_z: i32,
524 height: i32,
525 min_surface_level: i32,
526 min_y: i32,
527 column: &[BlockStateId],
528 writes: &mut Vec<(usize, BlockStateId)>,
529 ) {
530 let iceberg = f64::min(
531 (f64::from(self.iceberg_surface_noise.get_value(
532 f64::from(block_x),
533 0.0,
534 f64::from(block_z),
535 )) * 8.25)
536 .abs(),
537 f64::from(
538 self.iceberg_pillar_noise.get_value(
539 f64::from(block_x) * 1.28,
540 0.0,
541 f64::from(block_z) * 1.28,
542 ) * 15.0_f32,
543 ),
544 );
545
546 if iceberg <= 1.8 {
547 return;
548 }
549
550 let iceberg_roof = (f64::from(self.iceberg_pillar_roof_noise.get_value(
551 f64::from(block_x) * 1.17,
552 0.0,
553 f64::from(block_z) * 1.17,
554 )) * 1.5)
555 .abs();
556
557 let mut top = f64::min(iceberg * iceberg * 1.2, (iceberg_roof * 40.0).ceil() + 14.0);
558
559 if self.should_melt_frozen_ocean_iceberg_slightly(biome_id, block_x, block_z) {
560 top -= 2.0;
561 }
562
563 let extension_bottom;
564 if top > 2.0 {
565 extension_bottom = f64::from(self.sea_level) - top - 7.0;
566 top += f64::from(self.sea_level);
567 } else {
568 top = 0.0;
569 extension_bottom = 0.0;
570 }
571
572 let extension_top = top;
573 let mut random = self.noise_random.at(block_x, 0, block_z);
574 let max_snow_depth = 2 + random.next_i32_bounded(4);
575 let min_snow_height = self.sea_level + 18 + random.next_i32_bounded(10);
576 let mut snow_depth = 0;
577
578 let snow_block = vanilla_blocks::SNOW_BLOCK.default_state();
579 let packed_ice = vanilla_blocks::PACKED_ICE.default_state();
580 let air = vanilla_blocks::AIR.default_state();
581
582 let start_y = i32::max(height, top as i32 + 1);
583 for y in (min_surface_level..=start_y).rev() {
584 let rel_y = (y - min_y) as usize;
585 let state = column.get(rel_y).copied().unwrap_or(air);
586
587 let is_air = state.is_air();
588 let is_water = state.get_block() == &vanilla_blocks::WATER;
589
590 if (is_air && y < extension_top as i32 && random.next_f64() > 0.01)
591 || (is_water
592 && y > extension_bottom as i32
593 && y < self.sea_level
594 && extension_bottom != 0.0
595 && random.next_f64() > 0.15)
596 {
597 if snow_depth <= max_snow_depth && y > min_snow_height {
598 writes.push((rel_y, snow_block));
599 snow_depth += 1;
600 } else {
601 writes.push((rel_y, packed_ice));
602 }
603 }
604 }
605 }
606}
607
608impl SurfaceNoiseProvider for SurfaceSystem {
609 fn condition_noise(&self, noise_index: usize, x: i32, z: i32) -> f64 {
610 f64::from(self.condition_noises[noise_index].get_value(f64::from(x), 0.0, f64::from(z)))
611 }
612
613 fn condition_noise_3d(&self, noise_index: usize, x: i32, y: i32, z: i32) -> f64 {
614 f64::from(self.condition_noises[noise_index].get_value(
615 f64::from(x),
616 f64::from(y),
617 f64::from(z),
618 ))
619 }
620
621 fn get_band(&self, x: i32, y: i32, z: i32) -> BlockStateId {
622 let offset = (f64::from(self.clay_bands_offset_noise.get_value(
624 f64::from(x),
625 0.0,
626 f64::from(z),
627 )) * 4.0
628 + 0.5)
629 .floor() as i32;
630 let index = ((y + offset) % CLAY_BAND_LENGTH as i32 + CLAY_BAND_LENGTH as i32) as usize
631 % CLAY_BAND_LENGTH;
632 self.clay_bands[index]
633 }
634
635 fn cold_enough_to_snow(&self, biome_id: u16, block_x: i32, block_y: i32, block_z: i32) -> bool {
636 let mut xz = TemperatureXzCache::new(block_x, block_z);
637 SurfaceSystem::cold_enough_to_snow(self, biome_id, block_y, &mut xz)
638 }
639
640 fn vertical_gradient(
641 &self,
642 gradient_index: usize,
643 block_x: i32,
644 block_y: i32,
645 block_z: i32,
646 true_at_and_below: i32,
647 false_at_and_above: i32,
648 ) -> bool {
649 if block_y <= true_at_and_below {
650 return true;
651 }
652 if block_y >= false_at_and_above {
653 return false;
654 }
655 let probability = f64::from(false_at_and_above - block_y)
657 / f64::from(false_at_and_above - true_at_and_below);
658
659 let factory = &self.vertical_gradient_randoms[gradient_index];
662 let random_value = f64::from(factory.at(block_x, block_y, block_z).next_f32());
663 random_value < probability
664 }
665}
666
667fn create_noise(
669 splitter: &RandomSplitter,
670 id: &str,
671 params: &FxHashMap<String, NoiseParameters>,
672) -> NormalNoise {
673 let p = params
674 .get(id)
675 .unwrap_or_else(|| panic!("Missing noise parameters for {id}"));
676 p.create(splitter, id)
677}