Skip to main content

steel_core/worldgen/carver/
cave.rs

1//! Cave carver (overworld + nether variants).
2//!
3//! Mirrors vanilla's `CaveWorldCarver` + `NetherWorldCarver`. Single entry
4//! point [`CarveRun::carve_cave`] dispatched off a [`CaveKind`] — vanilla's
5//! overrides for nether (cave bound, thickness multiplier, y scale,
6//! per-block placement) are captured as kind-specific constants so the
7//! tunnel recursion logic stays shared.
8
9use std::f32::consts::{FRAC_PI_2, PI, TAU};
10
11use steel_math::trig;
12use steel_registry::carver::CaveCarverConfiguration;
13use steel_utils::random::{Random, legacy_random::LegacyRandom};
14use steel_utils::{BlockPos, ChunkPos};
15use steel_worldgen::density::DimensionNoises;
16
17use crate::worldgen::carver::{
18    CarveParams, CarveRun, CarveSkipChecker, CarverStyle, cached_replaceable_states, can_reach,
19    horizontal_tunnel_radius,
20};
21
22/// Which cave carver flavor to run.
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum CaveKind {
25    /// `minecraft:cave` / `minecraft:cave_extra_underground`.
26    Overworld,
27    /// `minecraft:nether_cave`.
28    Nether,
29}
30
31impl CaveKind {
32    /// Vanilla `CaveWorldCarver.getCaveBound` (15) or `NetherWorldCarver`'s
33    /// override (10).
34    const fn cave_bound(self) -> i32 {
35        match self {
36            Self::Overworld => 15,
37            Self::Nether => 10,
38        }
39    }
40
41    /// Vanilla `CaveWorldCarver.getYScale` (1.0) or `NetherWorldCarver`'s
42    /// override (5.0).
43    const fn y_scale(self) -> f64 {
44        match self {
45            Self::Overworld => 1.0,
46            Self::Nether => 5.0,
47        }
48    }
49
50    const fn style(self) -> CarverStyle {
51        match self {
52            Self::Overworld => CarverStyle::Overworld,
53            Self::Nether => CarverStyle::Nether,
54        }
55    }
56
57    /// Vanilla `getThickness`. Nether has a completely separate formula — it
58    /// skips the `nextInt(10) == 0` branch and doubles a 2-draw base value.
59    fn thickness(self, random: &mut impl Random) -> f32 {
60        match self {
61            Self::Overworld => {
62                // CaveWorldCarver.getThickness:
63                //   thickness = nextFloat()*2 + nextFloat();
64                //   if (nextInt(10) == 0) thickness *= nextFloat()*nextFloat()*3 + 1;
65                let mut thickness = random.next_f32() * 2.0 + random.next_f32();
66                if random.next_i32_bounded(10) == 0 {
67                    thickness *= random.next_f32() * random.next_f32() * 3.0 + 1.0;
68                }
69                thickness
70            }
71            Self::Nether => {
72                // NetherWorldCarver.getThickness override:
73                //   return (nextFloat()*2 + nextFloat()) * 2;
74                (random.next_f32() * 2.0 + random.next_f32()) * 2.0
75            }
76        }
77    }
78}
79
80/// Vanilla `WorldCarver.getRange()` — range in chunks. 4 each direction.
81const CARVER_RANGE: i32 = 4;
82/// Vanilla `SectionPos.sectionToBlockCoord(getRange() * 2 - 1)` = 112.
83const MAX_TUNNEL_DISTANCE: i32 = (CARVER_RANGE * 2 - 1) * 16;
84
85/// Position + rotation state that evolves along a tunnel's length.
86#[derive(Debug, Clone, Copy)]
87struct TunnelState {
88    x: f64,
89    y: f64,
90    z: f64,
91    /// Yaw.
92    horizontal_rotation: f32,
93    /// Pitch.
94    vertical_rotation: f32,
95}
96
97/// Static per-tunnel configuration passed through `create_tunnel` recursion
98/// unchanged between iterations.
99#[derive(Debug, Clone, Copy)]
100struct TunnelParams {
101    tunnel_seed: i64,
102    horizontal_radius_multiplier: f64,
103    vertical_radius_multiplier: f64,
104    thickness: f32,
105    step: i32,
106    dist: i32,
107    y_scale: f64,
108}
109
110impl<N, F> CarveRun<'_, '_, N, F>
111where
112    N: DimensionNoises,
113    F: FnMut(BlockPos) -> u16,
114{
115    /// Runs one cave-carver pass rooted in `source_pos`. `random` must have
116    /// been seeded by the caller via
117    /// `LegacyRandom::set_large_feature_seed(seed + carver_index, cx, cz)`
118    /// and the `isStartChunk` probability check must have already passed.
119    ///
120    /// Mirrors vanilla's `CaveWorldCarver.carve` / `NetherWorldCarver.carve`
121    /// (which inherits the cave variant).
122    pub fn carve_cave(
123        &mut self,
124        config: &CaveCarverConfiguration,
125        kind: CaveKind,
126        source_pos: ChunkPos,
127        random: &mut LegacyRandom,
128    ) {
129        // Triple-nested `random.nextInt(random.nextInt(...)+1)+1` gives a
130        // heavily right-skewed distribution of starts per chunk. Split into
131        // locals so the Java-style nesting doesn't overlap `&mut random`.
132        let bound = kind.cave_bound();
133        let inner = random.next_i32_bounded(bound);
134        let mid = random.next_i32_bounded(inner + 1);
135        let cave_count = random.next_i32_bounded(mid + 1);
136
137        let source_min_x = source_pos.0.x * 16;
138        let source_min_z = source_pos.0.y * 16;
139
140        let lava_level_y = config
141            .base
142            .lava_level
143            .resolve_y(self.ctx.min_y, self.ctx.gen_depth);
144        let params = CarveParams {
145            replaceable_tag: &config.base.replaceable_tag,
146            replaceable_states: cached_replaceable_states(&config.base.replaceable_tag),
147            lava_level_y,
148            style: kind.style(),
149        };
150
151        for _ in 0..cave_count {
152            let x = f64::from(source_min_x + random.next_i32_bounded(16));
153            let y = f64::from(
154                config
155                    .base
156                    .y
157                    .sample(random, self.ctx.min_y, self.ctx.gen_depth),
158            );
159            let z = f64::from(source_min_z + random.next_i32_bounded(16));
160
161            let horizontal_radius_multiplier =
162                f64::from(config.horizontal_radius_multiplier.sample(random));
163            let vertical_radius_multiplier =
164                f64::from(config.vertical_radius_multiplier.sample(random));
165            let floor_level = f64::from(config.floor_level.sample(random));
166
167            // Vanilla `CaveWorldCarver.shouldSkip`: skip blocks below the
168            // noisy floor OR outside the unit sphere in ellipsoid-local
169            // coords (xd²+yd²+zd² ≥ 1). Without the sphere test we'd carve
170            // cylinders, not ellipsoids.
171            let skip_checker = move |xd: f64, yd: f64, zd: f64, _world_y: i32| {
172                yd <= floor_level || xd * xd + yd * yd + zd * zd >= 1.0
173            };
174
175            let mut tunnels = 1i32;
176            if random.next_i32_bounded(4) == 0 {
177                let y_scale = f64::from(config.base.y_scale.sample(random));
178                let thickness = 1.0 + random.next_f32() * 6.0;
179                self.create_room(&params, x, y, z, thickness, y_scale, &skip_checker);
180                tunnels += random.next_i32_bounded(4);
181            }
182
183            for _ in 0..tunnels {
184                let state = TunnelState {
185                    x,
186                    y,
187                    z,
188                    horizontal_rotation: random.next_f32() * TAU,
189                    vertical_rotation: (random.next_f32() - 0.5) / 4.0,
190                };
191                let tunnel = TunnelParams {
192                    tunnel_seed: 0, // filled below to preserve vanilla draw order
193                    horizontal_radius_multiplier,
194                    vertical_radius_multiplier,
195                    thickness: kind.thickness(random),
196                    step: 0,
197                    dist: MAX_TUNNEL_DISTANCE - random.next_i32_bounded(MAX_TUNNEL_DISTANCE / 4),
198                    y_scale: kind.y_scale(),
199                };
200                // `tunnel_seed = nextLong()` draws 2 i32s — must be last to
201                // match vanilla's arg evaluation order.
202                let tunnel = TunnelParams {
203                    tunnel_seed: random.next_i64(),
204                    ..tunnel
205                };
206                self.create_tunnel(&params, state, tunnel, skip_checker);
207            }
208        }
209    }
210
211    /// Vanilla `CaveWorldCarver.createRoom`. Single ellipsoid at the tunnel
212    /// origin, offset by +1 on X.
213    #[expect(
214        clippy::too_many_arguments,
215        reason = "mirrors vanilla CaveWorldCarver.createRoom"
216    )]
217    fn create_room<S: CarveSkipChecker>(
218        &mut self,
219        params: &CarveParams<'_>,
220        x: f64,
221        y: f64,
222        z: f64,
223        thickness: f32,
224        y_scale: f64,
225        skip_checker: S,
226    ) {
227        // Vanilla: `1.5 + Mth.sin((float)(Math.PI / 2)) * thickness`. The
228        // argument is a float (π/2 cast to f32), looked up in the SIN table;
229        // the result equals 1.0f exactly, so the table detour doesn't
230        // matter here.
231        let horizontal_radius =
232            1.5 + f64::from(trig::sin(f64::from(FRAC_PI_2))) * f64::from(thickness);
233        let vertical_radius = horizontal_radius * y_scale;
234        self.carve_ellipsoid(
235            params,
236            x + 1.0,
237            y,
238            z,
239            horizontal_radius,
240            vertical_radius,
241            skip_checker,
242        );
243    }
244
245    /// Vanilla `CaveWorldCarver.createTunnel`. Steps along a curve, carving
246    /// an ellipsoid per step, with occasional mid-tunnel splits.
247    fn create_tunnel<S>(
248        &mut self,
249        params: &CarveParams<'_>,
250        mut state: TunnelState,
251        tunnel: TunnelParams,
252        skip_checker: S,
253    ) where
254        S: CarveSkipChecker + Copy,
255    {
256        let mut random = LegacyRandom::from_seed(tunnel.tunnel_seed as u64);
257        let split_point = random.next_i32_bounded(tunnel.dist / 2) + tunnel.dist / 4;
258        let steep = random.next_i32_bounded(6) == 0;
259        let mut y_rota: f32 = 0.0;
260        let mut x_rota: f32 = 0.0;
261
262        for current_step in tunnel.step..tunnel.dist {
263            // Vanilla: `Mth.sin((float)Math.PI * currentStep / dist) *
264            // thickness`. The `(float)Math.PI * currentStep / dist` term
265            // keeps float precision through to the `Mth.sin` argument before
266            // widening to double.
267            let progress_arg = PI * current_step as f32 / tunnel.dist as f32;
268            let horizontal_radius = horizontal_tunnel_radius(progress_arg, tunnel.thickness);
269            let vertical_radius = horizontal_radius * tunnel.y_scale;
270            let cos_x = trig::cos(f64::from(state.vertical_rotation));
271            state.x += f64::from(trig::cos(f64::from(state.horizontal_rotation)) * cos_x);
272            state.y += f64::from(trig::sin(f64::from(state.vertical_rotation)));
273            state.z += f64::from(trig::sin(f64::from(state.horizontal_rotation)) * cos_x);
274            state.vertical_rotation *= if steep { 0.92 } else { 0.7 };
275            state.vertical_rotation += x_rota * 0.1;
276            state.horizontal_rotation += y_rota * 0.1;
277            x_rota *= 0.9;
278            y_rota *= 0.75;
279            x_rota += (random.next_f32() - random.next_f32()) * random.next_f32() * 2.0;
280            y_rota += (random.next_f32() - random.next_f32()) * random.next_f32() * 4.0;
281
282            if current_step == split_point && tunnel.thickness > 1.0 {
283                // Vanilla evaluates args left-to-right: `nextLong()` (seed)
284                // is arg 5, `nextFloat() * 0.5 + 0.5` (thickness) is arg 11
285                // — so the seed is drawn before the thickness.
286                let sub_seed_a = random.next_i64();
287                let sub_thickness_a = random.next_f32() * 0.5 + 0.5;
288                let sub_state_a = TunnelState {
289                    horizontal_rotation: state.horizontal_rotation - FRAC_PI_2,
290                    vertical_rotation: state.vertical_rotation / 3.0,
291                    ..state
292                };
293                let sub_seed_b = random.next_i64();
294                let sub_thickness_b = random.next_f32() * 0.5 + 0.5;
295                let sub_state_b = TunnelState {
296                    horizontal_rotation: state.horizontal_rotation + FRAC_PI_2,
297                    vertical_rotation: state.vertical_rotation / 3.0,
298                    ..state
299                };
300                let sub_tunnel_a = TunnelParams {
301                    tunnel_seed: sub_seed_a,
302                    thickness: sub_thickness_a,
303                    step: current_step,
304                    y_scale: 1.0,
305                    ..tunnel
306                };
307                let sub_tunnel_b = TunnelParams {
308                    tunnel_seed: sub_seed_b,
309                    thickness: sub_thickness_b,
310                    step: current_step,
311                    y_scale: 1.0,
312                    ..tunnel
313                };
314                self.create_tunnel(params, sub_state_a, sub_tunnel_a, skip_checker);
315                self.create_tunnel(params, sub_state_b, sub_tunnel_b, skip_checker);
316                return;
317            }
318
319            if random.next_i32_bounded(4) == 0 {
320                continue;
321            }
322
323            if !can_reach(
324                self.chunk_min_x,
325                self.chunk_min_z,
326                state.x,
327                state.z,
328                current_step,
329                tunnel.dist,
330                tunnel.thickness,
331            ) {
332                return;
333            }
334
335            self.carve_ellipsoid(
336                params,
337                state.x,
338                state.y,
339                state.z,
340                horizontal_radius * tunnel.horizontal_radius_multiplier,
341                vertical_radius * tunnel.vertical_radius_multiplier,
342                skip_checker,
343            );
344        }
345    }
346}