steel_worldgen/noise/end_islands.rs
1//! End islands terrain generation algorithm.
2//!
3//! Matches vanilla's `DensityFunctions.EndIslandDensityFunction`. Generates the
4//! characteristic floating island pattern of The End by combining a distance-based
5//! falloff from the origin with simplex-noise-driven island placement.
6//!
7//! The noise seed is always 0 (world-seed-independent), initialized with
8//! `LegacyRandomSource(0)` + `consumeCount(17292)`.
9//!
10//! Result range: `[-0.84375, 0.5625]`.
11
12use crate::random::Random;
13use crate::random::legacy_random::LegacyRandom;
14
15use super::SimplexNoise;
16
17/// Threshold for simplex noise below which an island is spawned.
18///
19/// Vanilla uses `-0.9F` (float literal) in a `double < float` comparison, which
20/// promotes the float to double. `(double)(-0.9f)` ≈ `-0.8999999761581421`,
21/// NOT the exact double `-0.9`. We must match this f32→f64 promotion.
22const ISLAND_THRESHOLD: f64 = -0.9_f32 as f64;
23
24/// End islands density function.
25///
26/// Unlike overworld/nether density functions which are transpiled into native Rust,
27/// this is used directly at runtime because it's a self-contained leaf algorithm
28/// (simplex noise + neighbor loop) with no density function tree to transpile.
29#[derive(Debug, Clone)]
30pub struct EndIslands {
31 island_noise: SimplexNoise,
32}
33
34impl EndIslands {
35 /// Create a new `EndIslands` with the given world seed.
36 ///
37 /// Matches vanilla's `RandomState.NoiseWiringHelper.wrapNew()` which creates
38 /// `EndIslandDensityFunction(worldSeed)`, NOT seed 0. The JSON codec defaults
39 /// to seed 0, but `RandomState` replaces it with the world seed.
40 #[must_use]
41 pub fn new(seed: u64) -> Self {
42 let mut rng = LegacyRandom::from_seed(seed);
43 rng.consume_count(17292);
44 let island_noise = SimplexNoise::new(&mut rng);
45 Self { island_noise }
46 }
47
48 /// Sample the density value at block coordinates.
49 ///
50 /// Converts block coordinates to section coordinates internally (divides by 8).
51 #[must_use]
52 pub fn sample(&self, block_x: f64, _block_y: f64, block_z: f64) -> f64 {
53 let block_x = block_x as i32;
54 let block_z = block_z as i32;
55 // Widen to f64 BEFORE subtracting 8.0, matching Java's `float - 8.0` (double literal)
56 // where the float is promoted to double first.
57 (f64::from(Self::get_height_value(
58 &self.island_noise,
59 block_x / 8,
60 block_z / 8,
61 )) - 8.0)
62 / 128.0
63 }
64
65 /// Compute the height value at section coordinates.
66 ///
67 /// Matches vanilla's `EndIslandDensityFunction.getHeightValue()`.
68 /// Takes section coordinates (block position / 8).
69 fn get_height_value(island_noise: &SimplexNoise, section_x: i32, section_z: i32) -> f32 {
70 let chunk_x = section_x / 2;
71 let chunk_z = section_z / 2;
72 let sub_section_x = section_x % 2;
73 let sub_section_z = section_z % 2;
74
75 // Distance-based falloff from the origin.
76 // Vanilla does integer multiply THEN casts to float: `Mth.sqrt(sectionX * sectionX + ...)`.
77 // Integer overflow wraps in Java; we use wrapping_mul/wrapping_add to match.
78 let dist_sq = section_x
79 .wrapping_mul(section_x)
80 .wrapping_add(section_z.wrapping_mul(section_z));
81 let dist = (dist_sq as f32).sqrt();
82 let mut doffs = (100.0_f32 - dist * 8.0).clamp(-100.0, 80.0);
83
84 // Check 25×25 neighborhood for island contributions
85 for xo in -12..=12 {
86 for zo in -12..=12 {
87 let total_chunk_x = i64::from(chunk_x) + i64::from(xo);
88 let total_chunk_z = i64::from(chunk_z) + i64::from(zo);
89
90 if total_chunk_x * total_chunk_x + total_chunk_z * total_chunk_z > 4096
91 && island_noise.get_value_2d(total_chunk_x as f64, total_chunk_z as f64)
92 < ISLAND_THRESHOLD
93 {
94 let island_size = ((total_chunk_x as f32).abs() * 3439.0
95 + (total_chunk_z as f32).abs() * 147.0)
96 % 13.0
97 + 9.0;
98 let xd = sub_section_x as f32 - (xo * 2) as f32;
99 let zd = sub_section_z as f32 - (zo * 2) as f32;
100 let new_doffs =
101 (100.0_f32 - (xd * xd + zd * zd).sqrt() * island_size).clamp(-100.0, 80.0);
102 // Must NOT use f32::max here — Rust's max returns the non-NaN
103 // argument, while Java's Math.max propagates NaN. When the initial
104 // distance overflows i32, doffs becomes NaN and must stay NaN.
105 if new_doffs > doffs {
106 doffs = new_doffs;
107 }
108 }
109 }
110 }
111
112 doffs
113 }
114}