Skip to main content

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}