1use 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#[derive(Debug, Clone, Copy)]
39pub struct PreliminarySurfaceCorners {
40 pub nw: i32,
42 pub ne: i32,
44 pub sw: i32,
46 pub se: i32,
48}
49
50#[derive(Debug, Clone, Copy)]
54pub struct SourceChunk {
55 pub pos: ChunkPos,
57 pub biome: BiomeRef,
59}
60
61pub struct CarvingContext<'a, N: DimensionNoises> {
65 pub min_y: i32,
67 pub gen_depth: i32,
69 pub surface_system: &'a SurfaceSystem,
71 pub aquifer: &'a mut Aquifer<N>,
73 pub default_block_id: BlockStateId,
76 pub psl_corners: PreliminarySurfaceCorners,
80 pub chunk_min_x: i32,
82 pub chunk_min_z: i32,
84}
85
86impl<N: DimensionNoises> CarvingContext<'_, N> {
87 #[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 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 #[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 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#[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#[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 #[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#[must_use]
224pub fn cached_replaceable_states(tag: &Identifier) -> Option<&'static CarverReplaceableStates> {
225 CARVER_REPLACEABLE_STATES.get(tag)
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub enum CarverStyle {
234 Overworld,
236 Nether,
238}
239
240#[derive(Debug, Clone, Copy)]
243pub struct CarverBlockIds {
244 pub air: BlockStateId,
246 pub cave_air: BlockStateId,
248 pub lava: BlockStateId,
250 pub grass_block: BlockStateId,
252 pub mycelium: BlockStateId,
254 pub dirt: BlockStateId,
256}
257
258impl CarverBlockIds {
259 #[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 #[must_use]
288 pub const fn is_air_like(&self, state: BlockStateId) -> bool {
289 state.0 == self.air.0 || state.0 == self.cave_air.0
292 }
293}
294
295pub trait CarveSkipChecker {
299 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
311pub struct CarveParams<'a> {
315 pub replaceable_tag: &'a Identifier,
317 pub replaceable_states: Option<&'static CarverReplaceableStates>,
319 pub lava_level_y: i32,
322 pub style: CarverStyle,
324}
325
326#[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
334enum CarveState {
336 Place(BlockStateId),
338 Skip,
340}
341
342pub struct CarveRun<'a, 'b, N, F>
347where
348 N: DimensionNoises,
349 F: FnMut(BlockPos) -> u16,
350{
351 pub ctx: &'a mut CarvingContext<'b, N>,
353 pub noises: &'a N,
355 pub chunk: GenerationChunk<'a, CarversPhase>,
357 pub chunk_min_x: i32,
359 pub chunk_min_z: i32,
361 pub biome_getter: &'a mut F,
363 pub mask: &'a mut CarvingMask,
365 pub ids: CarverBlockIds,
367}
368
369impl<N, F> CarveRun<'_, '_, N, F>
370where
371 N: DimensionNoises,
372 F: FnMut(BlockPos) -> u16,
373{
374 #[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 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 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 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 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 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 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#[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#[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}