Skip to main content

steel_worldgen/noise/
ore_veinifier.rs

1//! Ore vein generation during terrain fill.
2//!
3//! Matches vanilla's `OreVeinifier`. Evaluates the three vein density functions
4//! (`vein_toggle`, `vein_ridged`, `vein_gap`) per solid block to decide whether
5//! to replace stone with copper/iron ore, raw ore blocks, or filler (granite/tuff).
6
7use steel_math::map_clamped;
8use steel_registry::{REGISTRY, vanilla_blocks};
9use steel_utils::BlockStateId;
10use steel_utils::random::name_hash::NameHash;
11use steel_utils::random::{PositionalRandom, Random, RandomSplitter};
12use steel_worldgen::density::{ColumnCache, DimensionNoises};
13
14/// Veininess magnitude must exceed this (after edge roundoff) to place any vein block.
15const VEININESS_THRESHOLD: f32 = 0.4;
16/// Within this many blocks of the vein type's Y boundary, the threshold tightens.
17const EDGE_ROUNDOFF_BEGIN: f32 = 20.0;
18/// Maximum tightening applied at the very edge of the Y range.
19const MAX_EDGE_ROUNDOFF: f32 = -0.2;
20/// Probability of NOT skipping a vein block (nextFloat must be <= this).
21const VEIN_SOLIDNESS: f32 = 0.7;
22/// Minimum richness (at veininess = 0.4).
23const MIN_RICHNESS: f32 = 0.1;
24/// Maximum richness (at veininess >= 0.6).
25const MAX_RICHNESS: f32 = 0.3;
26/// Probability of placing a raw ore block instead of ore.
27const CHANCE_OF_RAW_ORE_BLOCK: f32 = 0.02;
28/// Vein gap noise must be above this to place ore (otherwise filler).
29const SKIP_ORE_IF_GAP_BELOW: f32 = -0.3;
30
31/// A vein type with its Y range and block variants.
32struct VeinType {
33    ore: BlockStateId,
34    raw_ore_block: BlockStateId,
35    filler: BlockStateId,
36    min_y: i32,
37    max_y: i32,
38}
39
40/// Ore vein generator. Holds cached block state IDs and the positional random
41/// splitter used for per-block randomness.
42pub struct OreVeinifier {
43    ore_splitter: RandomSplitter,
44    copper: VeinType,
45    iron: VeinType,
46}
47
48impl OreVeinifier {
49    /// Create a new ore veinifier from the seed's positional splitter.
50    ///
51    /// The `splitter` should be the same one used to create `OverworldNoises`
52    /// (i.e. from `Xoroshiro::from_seed(seed).next_positional()`).
53    #[must_use]
54    pub fn new(splitter: &RandomSplitter) -> Self {
55        const ORE_HASH: NameHash = NameHash::new("minecraft:ore");
56        let mut ore_rng = splitter.with_hash_of(&ORE_HASH);
57        let ore_splitter = ore_rng.next_positional();
58
59        let copper = VeinType {
60            ore: REGISTRY
61                .blocks
62                .get_default_state_id(&vanilla_blocks::COPPER_ORE),
63            raw_ore_block: REGISTRY
64                .blocks
65                .get_default_state_id(&vanilla_blocks::RAW_COPPER_BLOCK),
66            filler: REGISTRY
67                .blocks
68                .get_default_state_id(&vanilla_blocks::GRANITE),
69            min_y: 0,
70            max_y: 50,
71        };
72
73        let iron = VeinType {
74            ore: REGISTRY
75                .blocks
76                .get_default_state_id(&vanilla_blocks::DEEPSLATE_IRON_ORE),
77            raw_ore_block: REGISTRY
78                .blocks
79                .get_default_state_id(&vanilla_blocks::RAW_IRON_BLOCK),
80            filler: REGISTRY.blocks.get_default_state_id(&vanilla_blocks::TUFF),
81            min_y: -60,
82            max_y: -8,
83        };
84
85        Self {
86            ore_splitter,
87            copper,
88            iron,
89        }
90    }
91
92    /// Applies one `OreVeinRule` from the material-rule datapack.
93    #[expect(
94        clippy::too_many_arguments,
95        reason = "matches the Vanilla ore-vein rule inputs"
96    )]
97    #[must_use]
98    pub fn try_apply_material_rule(
99        &self,
100        x: i32,
101        y: i32,
102        z: i32,
103        density: f32,
104        richness: f32,
105        filler_gap: impl FnOnce() -> f32,
106        ore: BlockStateId,
107        raw_ore: BlockStateId,
108        filler: BlockStateId,
109        raw_ore_chance: f32,
110    ) -> Option<BlockStateId> {
111        if density <= 0.0 {
112            return None;
113        }
114
115        let mut random = self.ore_splitter.at(x, y, z);
116        if random.next_f32() > density {
117            return None;
118        }
119
120        if random.next_f32() < richness && filler_gap() < 0.0 {
121            if random.next_f32() < raw_ore_chance {
122                Some(raw_ore)
123            } else {
124                Some(ore)
125            }
126        } else {
127            Some(filler)
128        }
129    }
130
131    /// Check if this solid block should be replaced with an ore vein block,
132    /// using trilinearly interpolated vein density values.
133    ///
134    /// `interpolated` contains all interpolated channel values from the noise
135    /// chunk. `combine_vein_toggle` and `combine_vein_ridged` extract the
136    /// vein-specific channels and apply outer operations.
137    ///
138    /// `vein_gap` has no `Interpolated` marker so is evaluated directly.
139    pub fn compute_interpolated<N: DimensionNoises>(
140        &self,
141        noises: &N,
142        cache: &mut N::ColumnCache,
143        interpolated: &[f32],
144        world_x: i32,
145        world_y: i32,
146        world_z: i32,
147    ) -> Option<BlockStateId> {
148        let vein_toggle = if N::vein_interp_enabled() {
149            noises.combine_vein_toggle(cache, interpolated, 0, world_y, 0)
150        } else {
151            cache.ensure(world_x, world_z, noises);
152            noises.router_vein_toggle(cache, world_x, world_y, world_z)
153        };
154
155        // Select vein type based on sign of vein_toggle
156        let vein_type = if vein_toggle > 0.0 {
157            &self.copper
158        } else {
159            &self.iron
160        };
161
162        let veininess = vein_toggle.abs();
163
164        // Check Y range
165        let dist_from_top = vein_type.max_y - world_y;
166        let dist_from_bottom = world_y - vein_type.min_y;
167        if dist_from_bottom < 0 || dist_from_top < 0 {
168            return None;
169        }
170
171        // Edge roundoff: tighten threshold near Y boundaries
172        let dist_from_edge = dist_from_top.min(dist_from_bottom);
173        let edge_roundoff = map_clamped(
174            dist_from_edge as f32,
175            0.0,
176            EDGE_ROUNDOFF_BEGIN,
177            MAX_EDGE_ROUNDOFF,
178            0.0,
179        );
180
181        if veininess + edge_roundoff < VEININESS_THRESHOLD {
182            return None;
183        }
184
185        // Per-block positional random
186        let mut rng = self.ore_splitter.at(world_x, world_y, world_z);
187
188        // Random solidness skip (30% chance to skip)
189        if rng.next_f32() > VEIN_SOLIDNESS {
190            return None;
191        }
192
193        // Ridged noise: uses interpolation if available
194        let vein_ridged = if N::vein_interp_enabled() {
195            noises.combine_vein_ridged(cache, interpolated, 0, world_y, 0)
196        } else {
197            cache.ensure(world_x, world_z, noises);
198            noises.router_vein_ridged(cache, world_x, world_y, world_z)
199        };
200        if vein_ridged >= 0.0 {
201            return None;
202        }
203
204        // Compute richness from veininess
205        let richness = map_clamped(
206            veininess,
207            VEININESS_THRESHOLD,
208            0.6,
209            MIN_RICHNESS,
210            MAX_RICHNESS,
211        );
212
213        if rng.next_f32() < richness {
214            // vein_gap has no Interpolated marker — evaluate directly
215            cache.ensure(world_x, world_z, noises);
216            let vein_gap = noises.router_vein_gap(cache, world_x, world_y, world_z);
217            if vein_gap > SKIP_ORE_IF_GAP_BELOW {
218                // Place ore (2% chance of raw ore block)
219                if rng.next_f32() < CHANCE_OF_RAW_ORE_BLOCK {
220                    Some(vein_type.raw_ore_block)
221                } else {
222                    Some(vein_type.ore)
223                }
224            } else {
225                // Below gap threshold: filler block
226                Some(vein_type.filler)
227            }
228        } else {
229            // Below richness threshold: filler block
230            Some(vein_type.filler)
231        }
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::OreVeinifier;
238    use crate::random::{Random, xoroshiro::Xoroshiro};
239    use steel_registry::{init_vanilla_registry, vanilla_blocks};
240
241    #[test]
242    fn rejected_material_rules_do_not_sample_filler_gap() {
243        init_vanilla_registry();
244        let splitter = Xoroshiro::from_seed(13579).next_positional();
245        let veinifier = OreVeinifier::new(&splitter);
246        let ore = vanilla_blocks::COPPER_ORE.default_state();
247        let raw = vanilla_blocks::RAW_COPPER_BLOCK.default_state();
248        let filler = vanilla_blocks::GRANITE.default_state();
249        for (density, richness, expected) in [(0.0, 1.0, None), (1.0, 0.0, Some(filler))] {
250            assert_eq!(
251                veinifier.try_apply_material_rule(
252                    12,
253                    20,
254                    -34,
255                    density,
256                    richness,
257                    || panic!("Vanilla rejects before sampling the gap"),
258                    ore,
259                    raw,
260                    filler,
261                    0.02,
262                ),
263                expected,
264            );
265        }
266    }
267}