1use 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
14const VEININESS_THRESHOLD: f32 = 0.4;
16const EDGE_ROUNDOFF_BEGIN: f32 = 20.0;
18const MAX_EDGE_ROUNDOFF: f32 = -0.2;
20const VEIN_SOLIDNESS: f32 = 0.7;
22const MIN_RICHNESS: f32 = 0.1;
24const MAX_RICHNESS: f32 = 0.3;
26const CHANCE_OF_RAW_ORE_BLOCK: f32 = 0.02;
28const SKIP_ORE_IF_GAP_BELOW: f32 = -0.3;
30
31struct VeinType {
33 ore: BlockStateId,
34 raw_ore_block: BlockStateId,
35 filler: BlockStateId,
36 min_y: i32,
37 max_y: i32,
38}
39
40pub struct OreVeinifier {
43 ore_splitter: RandomSplitter,
44 copper: VeinType,
45 iron: VeinType,
46}
47
48impl OreVeinifier {
49 #[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 #[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 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 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 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 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 let mut rng = self.ore_splitter.at(world_x, world_y, world_z);
187
188 if rng.next_f32() > VEIN_SOLIDNESS {
190 return None;
191 }
192
193 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 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 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 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 Some(vein_type.filler)
227 }
228 } else {
229 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}