1use 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#[derive(Debug, Clone, Copy)]
42pub struct PreliminarySurfaceCorners {
43 pub nw: i32,
45 pub ne: i32,
47 pub sw: i32,
49 pub se: i32,
51}
52
53#[derive(Debug, Clone, Copy)]
57pub struct SourceChunk {
58 pub pos: ChunkPos,
60 pub biome: BiomeRef,
62}
63
64pub struct CarvingContext<'a, N: DimensionNoises> {
68 pub noises: &'a N,
70 pub min_y: i32,
72 pub gen_depth: i32,
74 pub sea_level: i32,
76 pub surface_system: &'a SurfaceSystem,
78 pub aquifer: &'a mut Aquifer<N>,
80 pub default_block_id: BlockStateId,
83 pub psl_corners: PreliminarySurfaceCorners,
87 pub chunk_min_x: i32,
89 pub chunk_min_z: i32,
91 pub material_ore_vein_values: Arc<[f32]>,
93 pub ore_veinifier: Option<&'a OreVeinifier>,
95 pub material_cache: N::ColumnCache,
97}
98
99impl<N: DimensionNoises> CarvingContext<'_, N> {
100 #[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 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 #[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 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
206pub const UNCARVABLE_TAG: Identifier = Identifier::vanilla_static("uncarvable");
210
211#[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 #[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#[must_use]
243pub fn can_replace_block(state: BlockStateId) -> bool {
244 !UNCARVABLE_STATES.contains(state)
245}
246
247#[derive(Debug, Clone, Copy)]
250pub struct CarverBlockIds {
251 pub air: BlockStateId,
253 pub cave_air: BlockStateId,
255 pub lava: BlockStateId,
257 pub grass_block: BlockStateId,
259 pub mycelium: BlockStateId,
261 pub dirt: BlockStateId,
263}
264
265impl CarverBlockIds {
266 #[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 #[must_use]
295 pub const fn is_air_like(&self, state: BlockStateId) -> bool {
296 state.0 == self.air.0 || state.0 == self.cave_air.0
299 }
300}
301
302pub trait CarveSkipChecker {
306 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#[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
326pub struct CarveRun<'a, 'b, N, F>
331where
332 N: DimensionNoises,
333 F: FnMut(BlockPos) -> u16,
334{
335 pub ctx: &'a mut CarvingContext<'b, N>,
337 pub noises: &'a N,
339 pub chunk: GenerationChunk<'a, TerrainPhase>,
341 pub chunk_min_x: i32,
343 pub chunk_min_z: i32,
345 pub biome_getter: &'a mut F,
347 pub mask: &'a mut CarvingMask,
349 pub ids: CarverBlockIds,
351}
352
353impl<N, F> CarveRun<'_, '_, N, F>
354where
355 N: DimensionNoises,
356 F: FnMut(BlockPos) -> u16,
357{
358 #[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 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 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 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#[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#[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}