Skip to main content

steel_core/worldgen/carver/
mod.rs

1//! World-carving: runtime types for running configured carvers during the
2//! `CARVERS` chunk stage.
3//!
4//! Mirrors vanilla's `net.minecraft.world.level.levelgen.carver` package. The
5//! [`CarvingContext`] bundles the dimension-level state; a [`CarveRun`]
6//! bundles the per-chunk references that every carver method threads
7//! through.
8
9use std::{
10    cell::Cell,
11    sync::{Arc, LazyLock},
12};
13
14use glam::IVec3;
15use smallvec::SmallVec;
16use steel_math::lerp2;
17use steel_math::trig;
18use steel_registry::REGISTRY;
19use steel_registry::biome::BiomeRef;
20use steel_registry::blocks::block_state_ext::BlockStateExt;
21use steel_utils::ChunkPos;
22use steel_utils::{BlockPos, BlockStateId, Identifier};
23use steel_worldgen::density::DimensionNoises;
24use steel_worldgen::surface::{SurfaceConditionNoiseCache, SurfaceRuleContext};
25
26use crate::chunk::heightmap::Heightmap;
27use crate::worldgen::generator::{GenerationChunk, TerrainPhase};
28use crate::worldgen::surface::SurfaceSystem;
29use steel_worldgen::noise::OreVeinifier;
30use steel_worldgen::noise::{Aquifer, AquiferResult};
31
32pub mod canyon;
33pub mod cave;
34mod mask;
35
36pub use mask::CarvingMask;
37
38/// The four preliminary-surface-level samples at a chunk's block corners, in
39/// world Y. Indexed by local `(x, z)` corner as `(0,0)`, `(16,0)`, `(0,16)`,
40/// `(16,16)`.
41#[derive(Debug, Clone, Copy)]
42pub struct PreliminarySurfaceCorners {
43    /// Corner at `(chunk_min_x, chunk_min_z)`.
44    pub nw: i32,
45    /// Corner at `(chunk_min_x + 16, chunk_min_z)`.
46    pub ne: i32,
47    /// Corner at `(chunk_min_x, chunk_min_z + 16)`.
48    pub sw: i32,
49    /// Corner at `(chunk_min_x + 16, chunk_min_z + 16)`.
50    pub se: i32,
51}
52
53/// A source chunk's position and carver-list biome — the unit of work in the
54/// 17×17 `apply_carvers` loop. Each entry feeds one or more carver
55/// invocations from the biome's `carvers` list.
56#[derive(Debug, Clone, Copy)]
57pub struct SourceChunk {
58    /// Chunk position of the carver's origin.
59    pub pos: ChunkPos,
60    /// Biome providing the source chunk's carver list.
61    pub biome: BiomeRef,
62}
63
64/// Runtime context for a single `apply_carvers` invocation on one chunk.
65///
66/// Mirrors vanilla's `CarvingContext` and borrows the Aquifer retained from Noise.
67pub struct CarvingContext<'a, N: DimensionNoises> {
68    /// Density functions for material-rule evaluation.
69    pub noises: &'a N,
70    /// Dimension minimum Y (inclusive).
71    pub min_y: i32,
72    /// Dimension vertical extent in blocks (`max_y = min_y + gen_depth - 1`).
73    pub gen_depth: i32,
74    /// Dimension sea level used by relative vertical anchors.
75    pub sea_level: i32,
76    /// Surface system (biome-specific surface noise + clay bands).
77    pub surface_system: &'a SurfaceSystem,
78    /// Aquifer for this chunk, retained from Noise or reconstructed after a disk reload.
79    pub aquifer: &'a mut Aquifer<N>,
80    /// Default solid block for this dimension (stone / netherrack /
81    /// `end_stone`).
82    pub default_block_id: BlockStateId,
83    /// Preliminary surface levels at the 4 corners of this chunk, used for
84    /// bilinear interpolation of `min_surface_level` during top-material
85    /// lookup.
86    pub psl_corners: PreliminarySurfaceCorners,
87    /// Chunk NW block X — anchors `psl_corners`.
88    pub chunk_min_x: i32,
89    /// Chunk NW block Z — anchors `psl_corners`.
90    pub chunk_min_z: i32,
91    /// Density/richness values prefilled for material ore rules during noise fill.
92    pub material_ore_vein_values: Arc<[f32]>,
93    /// Positional random source and block states for material ore rules.
94    pub ore_veinifier: Option<&'a OreVeinifier>,
95    /// Material-rule column cache for direct filler-gap sampling.
96    pub material_cache: N::ColumnCache,
97}
98
99impl<N: DimensionNoises> CarvingContext<'_, N> {
100    /// Bilinear interpolation of the 4 preliminary-surface-level corners at
101    /// the given in-chunk position. Matches vanilla's
102    /// `SurfaceRules.Context.updateXZ` path.
103    #[must_use]
104    pub fn min_surface_level(&self, block_x: i32, block_z: i32) -> i32 {
105        let local_x = (block_x - self.chunk_min_x).clamp(0, 16);
106        let local_z = (block_z - self.chunk_min_z).clamp(0, 16);
107        // Vanilla: (float)(blockX & 15) / 16.0F — float intermediate is exact for 0-15
108        let t_x = f64::from(local_x as u8) / 16.0;
109        let t_z = f64::from(local_z as u8) / 16.0;
110        let c = self.psl_corners;
111        let interp = lerp2(
112            t_x,
113            t_z,
114            f64::from(c.nw),
115            f64::from(c.ne),
116            f64::from(c.sw),
117            f64::from(c.se),
118        );
119        interp.floor() as i32
120    }
121
122    /// Runs surface rules at a single position to pick the "top material"
123    /// block (grass / podzol / mycelium / sand / ...). Called by the carver
124    /// when it uncovers dirt beneath a grass block so the exposed surface gets
125    /// rewritten to the biome-appropriate surface block.
126    ///
127    /// Mirrors vanilla's `SurfaceSystem.topMaterial` (the `@Deprecated`
128    /// carver-specific variant). Vanilla hardcodes
129    /// `stone_depth_above = stone_depth_below = 1` here, and the water height
130    /// depends on whether the carved block was replaced with a fluid.
131    #[must_use]
132    pub fn top_material(
133        &mut self,
134        biome_id: u16,
135        block_x: i32,
136        block_y: i32,
137        block_z: i32,
138        steep: bool,
139        under_fluid: bool,
140    ) -> Option<BlockStateId> {
141        let value_count = N::material_ore_vein_value_count();
142        let mut ore_vein_results = SmallVec::<[Option<BlockStateId>; 2]>::new();
143        ore_vein_results.resize(value_count / 2, None);
144        if let Some(ore_veinifier) = self.ore_veinifier {
145            let local_x = (block_x - self.chunk_min_x) as usize;
146            let local_z = (block_z - self.chunk_min_z) as usize;
147            let relative_y = (block_y - self.min_y) as usize;
148            let offset = (relative_y * 16 * 16 + local_z * 16 + local_x) * value_count;
149            if let Some(values) = self
150                .material_ore_vein_values
151                .get(offset..offset + value_count)
152            {
153                N::fill_prefilled_material_ore_vein_results(
154                    self.noises,
155                    &mut self.material_cache,
156                    ore_veinifier,
157                    values,
158                    block_x,
159                    block_y,
160                    block_z,
161                    &mut ore_vein_results,
162                );
163            }
164        }
165
166        // Surface noise inputs (same helpers build_surface uses per column).
167        let surface_depth = self.surface_system.get_surface_depth(block_x, block_z);
168        let surface_secondary = self.surface_system.get_surface_secondary(block_x, block_z);
169        let min_surface_level = self.min_surface_level(block_x, block_z) + surface_depth - 8;
170
171        let water_height = if under_fluid { block_y + 1 } else { i32::MIN };
172        let condition_noise_values: SmallVec<[Cell<f64>; 8]> = N::surface_noise_ids()
173            .iter()
174            .map(|_| Cell::new(0.0))
175            .collect();
176        let condition_noise_initialized: SmallVec<[Cell<bool>; 8]> = N::surface_noise_ids()
177            .iter()
178            .map(|_| Cell::new(false))
179            .collect();
180        let condition_noise_cache =
181            SurfaceConditionNoiseCache::new(&condition_noise_values, &condition_noise_initialized);
182
183        let mut ctx = SurfaceRuleContext::new(
184            block_x,
185            block_z,
186            surface_depth,
187            surface_secondary,
188            min_surface_level,
189            steep,
190            block_y,
191            1,
192            1,
193            water_height,
194            Some(biome_id),
195            None,
196            self.surface_system,
197            &condition_noise_cache,
198            N::surface_rule_block_states(),
199        );
200
201        ctx = ctx.with_ore_vein_results(&ore_vein_results);
202        N::try_apply_surface_rule(&mut ctx)
203    }
204}
205
206/// Vanilla's global `#minecraft:uncarvable` block tag
207/// (`applyCarvingMask`'s `!blockState.is(BlockTags.UNCARVABLE)` check).
208/// Every carver shares this one tag now, not a per-carver one.
209pub const UNCARVABLE_TAG: Identifier = Identifier::vanilla_static("uncarvable");
210
211/// Per-state membership cache for [`UNCARVABLE_TAG`], resolved once into a
212/// state-id table to avoid repeated tag lookups in the carve loop.
213#[derive(Debug)]
214pub struct CarverReplaceableStates {
215    states: Box<[bool]>,
216}
217
218impl CarverReplaceableStates {
219    fn build() -> Self {
220        let states = REGISTRY
221            .blocks
222            .state_to_block_lookup
223            .iter()
224            .map(|&block| block.has_tag(&UNCARVABLE_TAG))
225            .collect();
226        Self { states }
227    }
228
229    /// Returns whether `state` belongs to `#minecraft:uncarvable`.
230    #[inline]
231    #[must_use]
232    pub fn contains(&self, state: BlockStateId) -> bool {
233        self.states.get(state.0 as usize).copied().unwrap_or(false)
234    }
235}
236
237static UNCARVABLE_STATES: LazyLock<CarverReplaceableStates> =
238    LazyLock::new(CarverReplaceableStates::build);
239
240/// A block may be carved unless it's in `#minecraft:uncarvable`. No separate
241/// air check — air isn't in the tag, so it's replaced like anything else.
242#[must_use]
243pub fn can_replace_block(state: BlockStateId) -> bool {
244    !UNCARVABLE_STATES.contains(state)
245}
246
247/// Well-known block state IDs a carver needs. Cached once per `apply_carvers`
248/// call so the carver loop doesn't hit the registry in its hot path.
249#[derive(Debug, Clone, Copy)]
250pub struct CarverBlockIds {
251    /// `minecraft:air`.
252    pub air: BlockStateId,
253    /// `minecraft:cave_air`.
254    pub cave_air: BlockStateId,
255    /// `minecraft:lava` (fluid block state).
256    pub lava: BlockStateId,
257    /// `minecraft:grass_block` default state.
258    pub grass_block: BlockStateId,
259    /// `minecraft:mycelium` default state.
260    pub mycelium: BlockStateId,
261    /// `minecraft:dirt` default state.
262    pub dirt: BlockStateId,
263}
264
265impl CarverBlockIds {
266    /// Looks up the well-known block state IDs once from the registry.
267    #[must_use]
268    pub fn load() -> Self {
269        static IDS: LazyLock<CarverBlockIds> = LazyLock::new(CarverBlockIds::load_uncached);
270        *IDS
271    }
272
273    fn load_uncached() -> Self {
274        use steel_registry::{REGISTRY, vanilla_blocks};
275        Self {
276            air: REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR),
277            cave_air: REGISTRY
278                .blocks
279                .get_default_state_id(&vanilla_blocks::CAVE_AIR),
280            lava: REGISTRY.blocks.get_default_state_id(&vanilla_blocks::LAVA),
281            grass_block: REGISTRY
282                .blocks
283                .get_default_state_id(&vanilla_blocks::GRASS_BLOCK),
284            mycelium: REGISTRY
285                .blocks
286                .get_default_state_id(&vanilla_blocks::MYCELIUM),
287            dirt: REGISTRY.blocks.get_default_state_id(&vanilla_blocks::DIRT),
288        }
289    }
290
291    /// Returns whether the given state is one of the air variants this
292    /// carver uses (i.e. not a fluid). Used by the top-material flow to
293    /// decide `under_fluid`.
294    #[must_use]
295    pub const fn is_air_like(&self, state: BlockStateId) -> bool {
296        // BlockStateId is a `#[repr(transparent)]` wrapper around u16.
297        // Hand-written equality keeps this function `const`.
298        state.0 == self.air.0 || state.0 == self.cave_air.0
299    }
300}
301
302/// Predicate called inside the carver's Y scan to decide whether a block is
303/// outside the carved shape for a given ellipsoid (cave floor cutoff, canyon
304/// width-by-height, etc). Matches vanilla's `WorldCarver.CarveSkipChecker`.
305pub trait CarveSkipChecker {
306    /// `xd`, `yd`, `zd` are the ellipsoid-normalized offsets from the carver
307    /// origin to this block's center (see `CarveRun::carve_ellipsoid`);
308    /// `world_y` is the absolute Y coordinate of the current block.
309    fn should_skip(&mut self, xd: f64, yd: f64, zd: f64, world_y: i32) -> bool;
310}
311
312impl<F: FnMut(f64, f64, f64, i32) -> bool> CarveSkipChecker for F {
313    fn should_skip(&mut self, xd: f64, yd: f64, zd: f64, world_y: i32) -> bool {
314        self(xd, yd, zd, world_y)
315    }
316}
317
318/// Vanilla cave/canyon tunnel radius calculation.
319#[inline]
320#[must_use]
321pub(super) fn horizontal_tunnel_radius(progress_arg: f32, thickness: f32) -> f64 {
322    let radius_offset = trig::sin(progress_arg) * thickness;
323    1.5 + f64::from(radius_offset)
324}
325
326/// The references every carver method needs. Bundled so `carve_ellipsoid`,
327/// `carve_block`, `create_tunnel`, `create_room`, `carve_cave`,
328/// `carve_canyon`, and `do_carve` can all be `&mut self` methods instead of
329/// repeating the same 7–8 arguments.
330pub struct CarveRun<'a, 'b, N, F>
331where
332    N: DimensionNoises,
333    F: FnMut(BlockPos) -> u16,
334{
335    /// Dimension-level context (aquifer, surface system, bounds, psl).
336    pub ctx: &'a mut CarvingContext<'b, N>,
337    /// Noise generators for this dimension.
338    pub noises: &'a N,
339    /// Chunk being carved into.
340    pub chunk: GenerationChunk<'a, TerrainPhase>,
341    /// Chunk NW block X (cached; `ctx.chunk_min_x` mirrors this).
342    pub chunk_min_x: i32,
343    /// Chunk NW block Z (cached; `ctx.chunk_min_z` mirrors this).
344    pub chunk_min_z: i32,
345    /// Biome lookup (vanilla `BiomeManager.getBiome`-style, fuzzed).
346    pub biome_getter: &'a mut F,
347    /// Carving mask for this Terrain operation.
348    pub mask: &'a mut CarvingMask,
349    /// Block IDs cached once per carver session.
350    pub ids: CarverBlockIds,
351}
352
353impl<N, F> CarveRun<'_, '_, N, F>
354where
355    N: DimensionNoises,
356    F: FnMut(BlockPos) -> u16,
357{
358    /// Carve every block inside the given ellipsoid that falls in this chunk.
359    ///
360    /// Returns `true` if at least one block was carved.
361    #[expect(
362        clippy::similar_names,
363        reason = "min_x_idx / min_z_idx / max_x_idx / max_z_idx mirror vanilla"
364    )]
365    pub fn carve_ellipsoid<S: CarveSkipChecker>(
366        &mut self,
367        x: f64,
368        y: f64,
369        z: f64,
370        horizontal_radius: f64,
371        vertical_radius: f64,
372        mut skip_checker: S,
373    ) -> bool {
374        let middle_x = f64::from(self.chunk_min_x) + 8.0;
375        let middle_z = f64::from(self.chunk_min_z) + 8.0;
376        let max_delta = 16.0 + horizontal_radius * 2.0;
377        if (x - middle_x).abs() > max_delta || (z - middle_z).abs() > max_delta {
378            return false;
379        }
380
381        let min_x_idx = ((x - horizontal_radius).floor() as i32 - self.chunk_min_x - 1).max(0);
382        let max_x_idx = ((x + horizontal_radius).floor() as i32 - self.chunk_min_x).min(15);
383        let min_y = ((y - vertical_radius).floor() as i32 - 1).max(self.ctx.min_y + 1);
384        // Vanilla: `chunk.isUpgrading() ? 0 : 7`. No chunk upgrade path yet,
385        // so always 7 — matches extractor config.
386        let protected_blocks_on_top = 7;
387        let max_y = ((y + vertical_radius).floor() as i32 + 1)
388            .min(self.ctx.min_y + self.ctx.gen_depth - 1 - protected_blocks_on_top);
389        let min_z_idx = ((z - horizontal_radius).floor() as i32 - self.chunk_min_z - 1).max(0);
390        let max_z_idx = ((z + horizontal_radius).floor() as i32 - self.chunk_min_z).min(15);
391
392        let mut carved = false;
393
394        for x_idx in min_x_idx..=max_x_idx {
395            let world_x = self.chunk_min_x + x_idx;
396            let xd = (f64::from(world_x) + 0.5 - x) / horizontal_radius;
397
398            for z_idx in min_z_idx..=max_z_idx {
399                let world_z = self.chunk_min_z + z_idx;
400                let zd = (f64::from(world_z) + 0.5 - z) / horizontal_radius;
401                if xd * xd + zd * zd >= 1.0 {
402                    continue;
403                }
404
405                // Scan top-down; range is exclusive of min_y (matches vanilla's
406                // `worldY > minY`).
407                for world_y in (min_y + 1..=max_y).rev() {
408                    let yd = (f64::from(world_y) - 0.5 - y) / vertical_radius;
409                    if skip_checker.should_skip(xd, yd, zd, world_y) {
410                        continue;
411                    }
412                    self.mask.set(x_idx, world_y, z_idx);
413                    carved = true;
414                }
415            }
416        }
417
418        carved
419    }
420
421    /// Applies the shared output mask after every carver has marked its geometry.
422    pub fn apply_carving_mask(&mut self) {
423        self.mask.visit(|local_x, local_z, bottom_y, top_y| {
424            let world_x = self.chunk_min_x + local_x;
425            let world_z = self.chunk_min_z + local_z;
426            let mut has_grass = false;
427
428            for world_y in (bottom_y..=top_y).rev() {
429                let pos = BlockPos::new(world_x, world_y, world_z);
430                let existing = self.chunk.get_block_state(pos);
431                if !can_replace_block(existing) {
432                    continue;
433                }
434                if existing == self.ids.grass_block || existing == self.ids.mycelium {
435                    has_grass = true;
436                }
437
438                let state = match self.ctx.aquifer.compute_substance(
439                    self.noises,
440                    world_x,
441                    world_y,
442                    world_z,
443                    0.0,
444                ) {
445                    AquiferResult::Solid => continue,
446                    AquiferResult::Fluid(state) => state,
447                    AquiferResult::Air => self.ids.air,
448                };
449                self.chunk.set_block_state(pos, state);
450                if self.ctx.aquifer.should_schedule_fluid_update() && state.has_fluid() {
451                    self.chunk.mark_pos_for_postprocessing(pos);
452                }
453
454                if !has_grass {
455                    continue;
456                }
457                let below_pos = BlockPos::new(world_x, world_y - 1, world_z);
458                if self.chunk.get_block_state(below_pos) != self.ids.dirt {
459                    continue;
460                }
461                let steep = Self::steep_material_condition(self.chunk, world_x, world_z);
462                let biome_id =
463                    (self.biome_getter)(BlockPos(IVec3::new(world_x, world_y - 1, world_z)));
464                if let Some(top) = self.ctx.top_material(
465                    biome_id,
466                    world_x,
467                    world_y - 1,
468                    world_z,
469                    steep,
470                    state.has_fluid(),
471                ) {
472                    self.chunk.set_block_state(below_pos, top);
473                    if top.has_fluid() {
474                        self.chunk.mark_pos_for_postprocessing(below_pos);
475                    }
476                }
477            }
478        });
479    }
480
481    fn steep_material_condition(
482        chunk: GenerationChunk<'_, TerrainPhase>,
483        world_x: i32,
484        world_z: i32,
485    ) -> bool {
486        let Some(steep) = chunk.with_world_surface_heightmap(|worldgen_surface| {
487            steep_material_condition(worldgen_surface, world_x, world_z)
488        }) else {
489            log::error!("WorldSurfaceWg heightmap missing during carver top-material lookup");
490            return false;
491        };
492        steep
493    }
494}
495
496/// Vanilla's `SurfaceRules.steep()` condition. It is asymmetric: only
497/// south-vs-north and west-vs-east deltas are checked.
498#[must_use]
499fn steep_material_condition(worldgen_surface: &Heightmap, block_x: i32, block_z: i32) -> bool {
500    let local_x = (block_x & 15) as usize;
501    let local_z = (block_z & 15) as usize;
502
503    let z_north = local_z.saturating_sub(1);
504    let z_south = (local_z + 1).min(15);
505    let h_north = worldgen_surface.get_highest_taken(local_x, z_north);
506    let h_south = worldgen_surface.get_highest_taken(local_x, z_south);
507    if h_south >= h_north + 4 {
508        return true;
509    }
510
511    let x_west = local_x.saturating_sub(1);
512    let x_east = (local_x + 1).min(15);
513    let h_west = worldgen_surface.get_highest_taken(x_west, local_z);
514    let h_east = worldgen_surface.get_highest_taken(x_east, local_z);
515    h_west >= h_east + 4
516}
517
518/// Vanilla's `WorldCarver.canReach` — prunes carver steps that can't touch
519/// any block in the given chunk (used by cave/canyon tunnel loops before
520/// carving an ellipsoid).
521#[must_use]
522pub fn can_reach(
523    chunk_min_x: i32,
524    chunk_min_z: i32,
525    x: f64,
526    z: f64,
527    current_step: i32,
528    total_steps: i32,
529    thickness: f32,
530) -> bool {
531    let x_mid = f64::from(chunk_min_x) + 8.0;
532    let z_mid = f64::from(chunk_min_z) + 8.0;
533    let xd = x - x_mid;
534    let zd = z - z_mid;
535    let remaining = f64::from(total_steps - current_step);
536    let rr = f64::from(thickness + 2.0_f32 + 16.0_f32);
537    xd * xd + zd * zd - remaining * remaining <= rr * rr
538}
539
540#[cfg(test)]
541mod tests {
542    use crate::chunk::heightmap::{Heightmap, HeightmapType};
543    use steel_worldgen::density_functions::overworld::OverworldNoiseSettings;
544
545    use super::steep_material_condition;
546
547    fn flat_world_surface(highest_taken: i32) -> Heightmap {
548        let mut heightmap = Heightmap::new(
549            HeightmapType::WorldSurfaceWg,
550            0,
551            OverworldNoiseSettings::HEIGHT,
552        );
553        for x in 0..16 {
554            for z in 0..16 {
555                heightmap.set_height(x, z, highest_taken + 1);
556            }
557        }
558        heightmap
559    }
560
561    #[test]
562    fn steep_material_condition_matches_vanilla_asymmetry() {
563        let mut heightmap = flat_world_surface(OverworldNoiseSettings::SEA_LEVEL);
564        heightmap.set_height(5, 4, 61);
565        heightmap.set_height(5, 6, 65);
566        assert!(steep_material_condition(&heightmap, 5, 5));
567
568        let mut heightmap = flat_world_surface(OverworldNoiseSettings::SEA_LEVEL);
569        heightmap.set_height(5, 4, 65);
570        heightmap.set_height(5, 6, 61);
571        assert!(!steep_material_condition(&heightmap, 5, 5));
572
573        let mut heightmap = flat_world_surface(OverworldNoiseSettings::SEA_LEVEL);
574        heightmap.set_height(4, 5, 65);
575        heightmap.set_height(6, 5, 61);
576        assert!(steep_material_condition(&heightmap, 5, 5));
577
578        let mut heightmap = flat_world_surface(OverworldNoiseSettings::SEA_LEVEL);
579        heightmap.set_height(4, 5, 61);
580        heightmap.set_height(6, 5, 65);
581        assert!(!steep_material_condition(&heightmap, 5, 5));
582    }
583}