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