Skip to main content

steel_core/worldgen/carver/
cave.rs

1//! Cave carver.
2//!
3//! Mirrors vanilla's `CaveWorldCarver` — also used for `minecraft:nether_cave`,
4//! since vanilla merged `NetherWorldCarver` into this same algorithm; only
5//! the configured values differ now.
6
7use std::f32::consts::{FRAC_PI_2, PI, TAU};
8
9use steel_math::trig;
10use steel_registry::carver::CaveCarverConfiguration;
11use steel_utils::random::{Random, legacy_random::LegacyRandom};
12use steel_utils::{BlockPos, ChunkPos};
13use steel_worldgen::density::DimensionNoises;
14
15use crate::worldgen::carver::{CarveRun, CarveSkipChecker, can_reach, horizontal_tunnel_radius};
16
17/// Vanilla `WorldCarver.getRange()` — range in chunks. 4 each direction.
18const CARVER_RANGE: i32 = 4;
19/// Vanilla `SectionPos.sectionToBlockCoord(getRange() * 2 - 1)` = 112.
20const MAX_TUNNEL_DISTANCE: i32 = (CARVER_RANGE * 2 - 1) * 16;
21
22/// Position + rotation state that evolves along a tunnel's length.
23#[derive(Debug, Clone, Copy)]
24struct TunnelState {
25    x: f64,
26    y: f64,
27    z: f64,
28    /// Yaw.
29    horizontal_rotation: f32,
30    /// Pitch.
31    vertical_rotation: f32,
32}
33
34/// Static per-tunnel configuration passed through `create_tunnel` recursion
35/// unchanged between iterations.
36#[derive(Debug, Clone, Copy)]
37struct TunnelParams {
38    tunnel_seed: i64,
39    horizontal_radius_multiplier: f64,
40    vertical_radius_multiplier: f64,
41    thickness: f32,
42    step: i32,
43    dist: i32,
44    y_scale: f64,
45}
46
47impl<N, F> CarveRun<'_, '_, N, F>
48where
49    N: DimensionNoises,
50    F: FnMut(BlockPos) -> u16,
51{
52    /// Runs one cave-carver pass rooted in `source_pos`. `random` must have
53    /// been seeded by the caller via
54    /// `LegacyRandom::set_large_feature_seed(seed + carver_index, cx, cz)`
55    /// and the `isStartChunk` probability check must have already passed.
56    ///
57    /// Mirrors vanilla's `CaveWorldCarver.carve`.
58    pub fn carve_cave(
59        &mut self,
60        config: &CaveCarverConfiguration,
61        source_pos: ChunkPos,
62        random: &mut LegacyRandom,
63    ) {
64        let cave_count = config.count.sample(random);
65
66        let source_min_x = source_pos.0.x * 16;
67        let source_min_z = source_pos.0.y * 16;
68
69        for _ in 0..cave_count {
70            let x = f64::from(source_min_x + random.next_i32_bounded(16));
71            let y = f64::from(config.base.y.sample(
72                random,
73                self.ctx.min_y,
74                self.ctx.gen_depth,
75                self.ctx.sea_level,
76            ));
77            let z = f64::from(source_min_z + random.next_i32_bounded(16));
78
79            let horizontal_radius_multiplier =
80                f64::from(config.horizontal_radius_multiplier.sample(random));
81            let vertical_radius_multiplier =
82                f64::from(config.vertical_radius_multiplier.sample(random));
83            let start_vertical_radius_multiplier =
84                f64::from(config.start_vertical_radius_multiplier.sample(random));
85            let floor_level = f64::from(config.floor_level.sample(random));
86
87            // Vanilla `CaveWorldCarver.shouldSkip`: skip blocks below the
88            // noisy floor OR outside the unit sphere in ellipsoid-local
89            // coords (xd²+yd²+zd² ≥ 1). Without the sphere test we'd carve
90            // cylinders, not ellipsoids.
91            let skip_checker = move |xd: f64, yd: f64, zd: f64, _world_y: i32| {
92                yd <= floor_level || xd * xd + yd * yd + zd * zd >= 1.0
93            };
94
95            let mut tunnels = 1i32;
96            if random.next_i32_bounded(4) == 0 {
97                let y_scale = f64::from(config.room_vertical_radius_multiplier.sample(random));
98                let thickness = 1.0 + random.next_f32() * 6.0;
99                self.create_room(x, y, z, thickness, y_scale, &skip_checker);
100                tunnels += random.next_i32_bounded(4);
101            }
102
103            for _ in 0..tunnels {
104                let state = TunnelState {
105                    x,
106                    y,
107                    z,
108                    horizontal_rotation: random.next_f32() * TAU,
109                    vertical_rotation: (random.next_f32() - 0.5) / 4.0,
110                };
111                let tunnel = TunnelParams {
112                    tunnel_seed: 0, // filled below to preserve vanilla draw order
113                    horizontal_radius_multiplier,
114                    vertical_radius_multiplier,
115                    thickness: get_thickness(config, random),
116                    step: 0,
117                    dist: MAX_TUNNEL_DISTANCE - random.next_i32_bounded(MAX_TUNNEL_DISTANCE / 4),
118                    y_scale: start_vertical_radius_multiplier,
119                };
120                // `tunnel_seed = nextLong()` draws 2 i32s — must be last to
121                // match vanilla's arg evaluation order.
122                let tunnel = TunnelParams {
123                    tunnel_seed: random.next_i64(),
124                    ..tunnel
125                };
126                self.create_tunnel(state, tunnel, skip_checker);
127            }
128        }
129    }
130
131    /// Vanilla `CaveWorldCarver.createRoom`. Single ellipsoid at the tunnel
132    /// origin, offset by +1 on X.
133    fn create_room<S: CarveSkipChecker>(
134        &mut self,
135        x: f64,
136        y: f64,
137        z: f64,
138        thickness: f32,
139        y_scale: f64,
140        skip_checker: S,
141    ) {
142        // Vanilla: `1.5 + Mth.sin((float)(Math.PI / 2)) * thickness`. The
143        // argument is a float (π/2 cast to f32), looked up in the SIN table;
144        // the result equals 1.0f exactly, so the table detour doesn't
145        // matter here.
146        let horizontal_radius = 1.5 + f64::from(trig::sin(FRAC_PI_2)) * f64::from(thickness);
147        let vertical_radius = horizontal_radius * y_scale;
148        self.carve_ellipsoid(
149            x + 1.0,
150            y,
151            z,
152            horizontal_radius,
153            vertical_radius,
154            skip_checker,
155        );
156    }
157
158    /// Vanilla `CaveWorldCarver.createTunnel`. Steps along a curve, carving
159    /// an ellipsoid per step, with occasional mid-tunnel splits.
160    fn create_tunnel<S>(&mut self, mut state: TunnelState, tunnel: TunnelParams, skip_checker: S)
161    where
162        S: CarveSkipChecker + Copy,
163    {
164        let mut random = LegacyRandom::from_seed(tunnel.tunnel_seed as u64);
165        let split_point = random.next_i32_bounded(tunnel.dist / 2) + tunnel.dist / 4;
166        let steep = random.next_i32_bounded(6) == 0;
167        let mut y_rota: f32 = 0.0;
168        let mut x_rota: f32 = 0.0;
169
170        for current_step in tunnel.step..tunnel.dist {
171            // Vanilla: `Mth.sin((float)Math.PI * currentStep / dist) *
172            // thickness`. The `(float)Math.PI * currentStep / dist` term
173            // keeps float precision through to the `Mth.sin` argument before
174            // widening to double.
175            let progress_arg = PI * current_step as f32 / tunnel.dist as f32;
176            let horizontal_radius = horizontal_tunnel_radius(progress_arg, tunnel.thickness);
177            let vertical_radius = horizontal_radius * tunnel.y_scale;
178            let cos_x = trig::cos(state.vertical_rotation);
179            state.x += f64::from(trig::cos(state.horizontal_rotation) * cos_x);
180            state.y += f64::from(trig::sin(state.vertical_rotation));
181            state.z += f64::from(trig::sin(state.horizontal_rotation) * cos_x);
182            state.vertical_rotation *= if steep { 0.92 } else { 0.7 };
183            state.vertical_rotation += x_rota * 0.1;
184            state.horizontal_rotation += y_rota * 0.1;
185            x_rota *= 0.9;
186            y_rota *= 0.75;
187            x_rota += (random.next_f32() - random.next_f32()) * random.next_f32() * 2.0;
188            y_rota += (random.next_f32() - random.next_f32()) * random.next_f32() * 4.0;
189
190            if current_step == split_point && tunnel.thickness > 1.0 {
191                // Vanilla evaluates args left-to-right: `nextLong()` (seed)
192                // is arg 5, `nextFloat() * 0.5 + 0.5` (thickness) is arg 11
193                // — so the seed is drawn before the thickness.
194                let sub_seed_a = random.next_i64();
195                let sub_thickness_a = random.next_f32() * 0.5 + 0.5;
196                let sub_state_a = TunnelState {
197                    horizontal_rotation: state.horizontal_rotation - FRAC_PI_2,
198                    vertical_rotation: state.vertical_rotation / 3.0,
199                    ..state
200                };
201                let sub_seed_b = random.next_i64();
202                let sub_thickness_b = random.next_f32() * 0.5 + 0.5;
203                let sub_state_b = TunnelState {
204                    horizontal_rotation: state.horizontal_rotation + FRAC_PI_2,
205                    vertical_rotation: state.vertical_rotation / 3.0,
206                    ..state
207                };
208                let sub_tunnel_a = TunnelParams {
209                    tunnel_seed: sub_seed_a,
210                    thickness: sub_thickness_a,
211                    step: current_step,
212                    y_scale: 1.0,
213                    ..tunnel
214                };
215                let sub_tunnel_b = TunnelParams {
216                    tunnel_seed: sub_seed_b,
217                    thickness: sub_thickness_b,
218                    step: current_step,
219                    y_scale: 1.0,
220                    ..tunnel
221                };
222                self.create_tunnel(sub_state_a, sub_tunnel_a, skip_checker);
223                self.create_tunnel(sub_state_b, sub_tunnel_b, skip_checker);
224                return;
225            }
226
227            if random.next_i32_bounded(4) == 0 {
228                continue;
229            }
230
231            if !can_reach(
232                self.chunk_min_x,
233                self.chunk_min_z,
234                state.x,
235                state.z,
236                current_step,
237                tunnel.dist,
238                tunnel.thickness,
239            ) {
240                return;
241            }
242
243            self.carve_ellipsoid(
244                state.x,
245                state.y,
246                state.z,
247                horizontal_radius * tunnel.horizontal_radius_multiplier,
248                vertical_radius * tunnel.vertical_radius_multiplier,
249                skip_checker,
250            );
251        }
252    }
253}
254
255/// Vanilla `CaveWorldCarver.getThickness`.
256fn get_thickness(config: &CaveCarverConfiguration, random: &mut LegacyRandom) -> f32 {
257    let mut thickness = config.thickness.sample(random);
258    if config.weird_thickness_bias && random.next_i32_bounded(10) == 0 {
259        thickness *= random.next_f32() * random.next_f32() * 3.0 + 1.0;
260    }
261    thickness
262}