steel_registry/carver.rs
1//! Configured carver registry.
2//!
3//! Mirrors vanilla's `ConfiguredWorldCarver` — each entry is a
4//! `CarverType` (the carver kind — cave / nether_cave / canyon) paired with
5//! its configuration. Configured carvers are referenced by biomes via the
6//! `carvers` field on [`Biome`](crate::biome::Biome) and sampled during the
7//! `CARVERS` chunk generation stage.
8
9use std::sync::OnceLock;
10
11use rustc_hash::FxHashMap;
12use steel_utils::Identifier;
13use steel_utils::random::{Random, legacy_random::LegacyRandom};
14use steel_utils::value_providers::{FloatProvider, HeightProvider, VerticalAnchor};
15
16/// Shared per-carver configuration fields present on every carver type.
17///
18/// Mirrors vanilla's `CarverConfiguration`. The `replaceable` block set is
19/// stored as a tag identifier (e.g. `minecraft:overworld_carver_replaceables`)
20/// and resolved against `BlockRegistry::is_in_tag` at carve time.
21#[derive(Debug, Clone)]
22pub struct CarverConfiguration {
23 /// Per-chunk start probability (in `[0, 1]`).
24 pub probability: f32,
25 /// Carver origin Y coordinate provider.
26 pub y: HeightProvider,
27 /// Vertical-stretch multiplier for carved ellipsoids.
28 pub y_scale: FloatProvider,
29 /// Any block carved at or below this Y becomes lava instead of air.
30 pub lava_level: VerticalAnchor,
31 /// Tag of blocks the carver is allowed to replace.
32 pub replaceable_tag: Identifier,
33 // TODO: debug_settings parsed but ignored — only active when
34 // `SharedConstants.DEBUG_CARVERS` is true, which is never the case in
35 // a production build. Wire through if we ever want the cave visualiser.
36}
37
38/// Cave/nether-cave configuration.
39///
40/// Mirrors vanilla's `CaveCarverConfiguration`.
41#[derive(Debug, Clone)]
42pub struct CaveCarverConfiguration {
43 /// Base configuration.
44 pub base: CarverConfiguration,
45 /// Per-tunnel horizontal-radius stretch factor.
46 pub horizontal_radius_multiplier: FloatProvider,
47 /// Per-tunnel vertical-radius stretch factor.
48 pub vertical_radius_multiplier: FloatProvider,
49 /// Shapes the cave floor — values in `[-1, 1]`. Blocks where the normalized
50 /// vertical offset `yd` is below this value are skipped.
51 pub floor_level: FloatProvider,
52}
53
54/// Canyon shape parameters — controls tunnel shape and width-per-height
55/// variation.
56///
57/// Mirrors vanilla's `CanyonCarverConfiguration.CanyonShapeConfiguration`.
58#[derive(Debug, Clone)]
59pub struct CanyonShapeConfiguration {
60 /// Fraction of the max carving distance used as the actual tunnel length.
61 pub distance_factor: FloatProvider,
62 /// Overall tunnel thickness.
63 pub thickness: FloatProvider,
64 /// Lower values = fresher width noise each step; higher = smoother.
65 pub width_smoothness: i32,
66 /// Per-step horizontal-radius multiplier.
67 pub horizontal_radius_factor: FloatProvider,
68 /// Baseline vertical-radius multiplier applied along the whole tunnel.
69 pub vertical_radius_default_factor: f32,
70 /// Extra vertical-radius multiplier that peaks at the tunnel midpoint.
71 pub vertical_radius_center_factor: f32,
72}
73
74impl CanyonShapeConfiguration {
75 /// Mirrors vanilla `CanyonWorldCarver.initWidthFactors` — fresh squared
76 /// width factor at every `width_smoothness`-th Y level, otherwise
77 /// repeating the previous value.
78 #[must_use]
79 pub fn init_width_factors(&self, gen_depth: i32, random: &mut LegacyRandom) -> Vec<f32> {
80 let depth = gen_depth as usize;
81 let mut factors = vec![0.0_f32; depth];
82 let mut current = 1.0_f32;
83 for (y_index, slot) in factors.iter_mut().enumerate() {
84 if y_index == 0 || random.next_i32_bounded(self.width_smoothness) == 0 {
85 current = 1.0 + random.next_f32() * random.next_f32();
86 }
87 *slot = current * current;
88 }
89 factors
90 }
91
92 /// Mirrors vanilla `CanyonWorldCarver.updateVerticalRadius` — applies the
93 /// shape's default/center factors plus a `Mth.randomBetween(0.75, 1.0)`
94 /// jitter.
95 #[must_use]
96 pub fn update_vertical_radius(
97 &self,
98 random: &mut LegacyRandom,
99 vertical_radius: f64,
100 distance: f32,
101 current_step: f32,
102 ) -> f64 {
103 // Vanilla: `Mth.abs(0.5F - currentStep/distance)` — float arithmetic.
104 let vertical_multiplier = 1.0_f32 - (0.5 - current_step / distance).abs() * 2.0;
105 let factor = self.vertical_radius_default_factor
106 + self.vertical_radius_center_factor * vertical_multiplier;
107 // `Mth.randomBetween(random, 0.75F, 1.0F)` = 0.75 + nextFloat()*0.25.
108 let jitter = 0.75 + random.next_f32() * 0.25;
109 f64::from(factor) * vertical_radius * f64::from(jitter)
110 }
111}
112
113/// Canyon (ravine) configuration.
114///
115/// Mirrors vanilla's `CanyonCarverConfiguration`.
116#[derive(Debug, Clone)]
117pub struct CanyonCarverConfiguration {
118 /// Base configuration.
119 pub base: CarverConfiguration,
120 /// Per-tunnel vertical rotation amount.
121 pub vertical_rotation: FloatProvider,
122 /// Shape configuration.
123 pub shape: CanyonShapeConfiguration,
124}
125
126/// Which carver algorithm a [`ConfiguredCarver`] uses, along with its
127/// fully-resolved configuration.
128#[derive(Debug, Clone)]
129pub enum ConfiguredCarverKind {
130 /// Branching cave tunnels (`CaveWorldCarver`).
131 Cave(CaveCarverConfiguration),
132 /// Nether variant of caves (`NetherWorldCarver`) — no aquifer lookups,
133 /// and a fixed lava-or-cave-air substance decision.
134 NetherCave(CaveCarverConfiguration),
135 /// Long narrow ravines (`CanyonWorldCarver`).
136 Canyon(CanyonCarverConfiguration),
137}
138
139/// A fully-configured carver, as referenced by biomes via their
140/// `carvers` field.
141///
142/// Mirrors vanilla's `ConfiguredWorldCarver<?>`.
143#[derive(Debug)]
144pub struct ConfiguredCarver {
145 /// Registry key (e.g. `minecraft:cave`, `minecraft:canyon`).
146 pub key: Identifier,
147 /// Which carver algorithm this is + its configuration.
148 pub kind: ConfiguredCarverKind,
149 /// Cached registry ID, set during registration for O(1) lookup on hot
150 /// paths.
151 pub id: OnceLock<usize>,
152}
153
154impl ConfiguredCarver {
155 /// The base (shared) configuration for this carver.
156 #[must_use]
157 pub const fn base(&self) -> &CarverConfiguration {
158 match &self.kind {
159 ConfiguredCarverKind::Cave(c) | ConfiguredCarverKind::NetherCave(c) => &c.base,
160 ConfiguredCarverKind::Canyon(c) => &c.base,
161 }
162 }
163}
164
165/// Read-only reference to a registered [`ConfiguredCarver`].
166pub type ConfiguredCarverRef = &'static ConfiguredCarver;
167
168/// Registry of configured carvers, keyed by namespaced identifier.
169pub struct ConfiguredCarverRegistry {
170 carvers_by_id: Vec<ConfiguredCarverRef>,
171 carvers_by_key: FxHashMap<Identifier, usize>,
172 allows_registering: bool,
173}
174
175impl ConfiguredCarverRegistry {
176 /// Creates an empty registry.
177 #[must_use]
178 pub fn new() -> Self {
179 Self {
180 carvers_by_id: Vec::new(),
181 carvers_by_key: FxHashMap::default(),
182 allows_registering: true,
183 }
184 }
185
186 /// Registers a carver and returns its numeric ID.
187 pub fn register(&mut self, entry: ConfiguredCarverRef) -> usize {
188 assert!(
189 self.allows_registering,
190 "Cannot register ConfiguredCarver after registry has been frozen"
191 );
192 let id = self.carvers_by_id.len();
193 let cached = entry.id.get_or_init(|| id);
194 assert_eq!(*cached, id, "carver registered with conflicting id");
195 self.carvers_by_id.push(entry);
196 self.carvers_by_key.insert(entry.key.clone(), id);
197 id
198 }
199
200 /// Iterates over all registered carvers with their IDs.
201 pub fn iter(&self) -> impl Iterator<Item = (usize, ConfiguredCarverRef)> + '_ {
202 self.carvers_by_id
203 .iter()
204 .enumerate()
205 .map(|(id, &entry)| (id, entry))
206 }
207}
208
209impl Default for ConfiguredCarverRegistry {
210 fn default() -> Self {
211 Self::new()
212 }
213}
214
215crate::impl_registry_ext!(
216 ConfiguredCarverRegistry,
217 ConfiguredCarver,
218 carvers_by_id,
219 carvers_by_key
220);
221
222impl crate::RegistryEntry for ConfiguredCarver {
223 fn key(&self) -> &Identifier {
224 &self.key
225 }
226
227 fn try_id(&self) -> Option<usize> {
228 self.id.get().copied()
229 }
230}