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: f64 = 0.4;
16/// Within this many blocks of the vein type's Y boundary, the threshold tightens.
17const EDGE_ROUNDOFF_BEGIN: f64 = 20.0;
18/// Maximum tightening applied at the very edge of the Y range.
19const MAX_EDGE_ROUNDOFF: f64 = -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: f64 = 0.1;
24/// Maximum richness (at veininess >= 0.6).
25const MAX_RICHNESS: f64 = 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: f64 = -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    /// Check if this solid block should be replaced with an ore vein block,
93    /// using trilinearly interpolated vein density values.
94    ///
95    /// `interpolated` contains all interpolated channel values from the noise
96    /// chunk. `combine_vein_toggle` and `combine_vein_ridged` extract the
97    /// vein-specific channels and apply outer operations.
98    ///
99    /// `vein_gap` has no `Interpolated` marker so is evaluated directly.
100    pub fn compute_interpolated<N: DimensionNoises>(
101        &self,
102        noises: &N,
103        cache: &mut N::ColumnCache,
104        interpolated: &[f64],
105        world_x: i32,
106        world_y: i32,
107        world_z: i32,
108    ) -> Option<BlockStateId> {
109        let vein_toggle = if N::vein_interp_enabled() {
110            noises.combine_vein_toggle(cache, interpolated, 0, world_y, 0)
111        } else {
112            cache.ensure(world_x, world_z, noises);
113            noises.router_vein_toggle(cache, world_x, world_y, world_z)
114        };
115
116        // Select vein type based on sign of vein_toggle
117        let vein_type = if vein_toggle > 0.0 {
118            &self.copper
119        } else {
120            &self.iron
121        };
122
123        let veininess = vein_toggle.abs();
124
125        // Check Y range
126        let dist_from_top = vein_type.max_y - world_y;
127        let dist_from_bottom = world_y - vein_type.min_y;
128        if dist_from_bottom < 0 || dist_from_top < 0 {
129            return None;
130        }
131
132        // Edge roundoff: tighten threshold near Y boundaries
133        let dist_from_edge = dist_from_top.min(dist_from_bottom);
134        let edge_roundoff = map_clamped(
135            f64::from(dist_from_edge),
136            0.0,
137            EDGE_ROUNDOFF_BEGIN,
138            MAX_EDGE_ROUNDOFF,
139            0.0,
140        );
141
142        if veininess + edge_roundoff < VEININESS_THRESHOLD {
143            return None;
144        }
145
146        // Per-block positional random
147        let mut rng = self.ore_splitter.at(world_x, world_y, world_z);
148
149        // Random solidness skip (30% chance to skip)
150        if rng.next_f32() > VEIN_SOLIDNESS {
151            return None;
152        }
153
154        // Ridged noise: uses interpolation if available
155        let vein_ridged = if N::vein_interp_enabled() {
156            noises.combine_vein_ridged(cache, interpolated, 0, world_y, 0)
157        } else {
158            cache.ensure(world_x, world_z, noises);
159            noises.router_vein_ridged(cache, world_x, world_y, world_z)
160        };
161        if vein_ridged >= 0.0 {
162            return None;
163        }
164
165        // Compute richness from veininess
166        let richness = map_clamped(
167            veininess,
168            VEININESS_THRESHOLD,
169            0.6,
170            MIN_RICHNESS,
171            MAX_RICHNESS,
172        );
173
174        if (f64::from(rng.next_f32())) < richness {
175            // vein_gap has no Interpolated marker — evaluate directly
176            cache.ensure(world_x, world_z, noises);
177            let vein_gap = noises.router_vein_gap(cache, world_x, world_y, world_z);
178            if vein_gap > SKIP_ORE_IF_GAP_BELOW {
179                // Place ore (2% chance of raw ore block)
180                if rng.next_f32() < CHANCE_OF_RAW_ORE_BLOCK {
181                    Some(vein_type.raw_ore_block)
182                } else {
183                    Some(vein_type.ore)
184                }
185            } else {
186                // Below gap threshold: filler block
187                Some(vein_type.filler)
188            }
189        } else {
190            // Below richness threshold: filler block
191            Some(vein_type.filler)
192        }
193    }
194}