Skip to main content

steel_core/worldgen/feature/features/
geode.rs

1#![expect(
2    clippy::too_many_lines,
3    reason = "geode placement is kept linear to preserve vanilla parity"
4)]
5
6use super::super::prelude::*;
7use super::super::runner::FeatureDecorationRunner;
8use rustc_hash::FxHashMap;
9use steel_utils::locks::SyncMutex;
10
11static GEODE_NOISE_BY_SEED: LazyLock<SyncMutex<FxHashMap<i64, NormalNoise>>> =
12    LazyLock::new(|| SyncMutex::new(FxHashMap::default()));
13
14impl FeatureDecorationRunner {
15    pub(in crate::worldgen::feature) fn place_geode_feature(
16        region: &mut WorldGenRegion<'_>,
17        registry: &Registry,
18        random: &mut WorldgenRandom,
19        config: &GeodeConfiguration,
20        origin: BlockPos,
21    ) -> bool {
22        let num_points = config.distribution_points.sample(random);
23        let noise = Self::geode_noise(region.seed());
24        let crack_size_adjustment =
25            f64::from(num_points) / f64::from(config.outer_wall_distance.max());
26        let layers = &config.layers;
27        let blocks = &config.blocks;
28        let crack = &config.crack;
29        let inner_air = 1.0 / layers.filling.sqrt();
30        let innermost_block_layer = 1.0 / (layers.inner_layer + crack_size_adjustment).sqrt();
31        let inner_crust = 1.0 / (layers.middle_layer + crack_size_adjustment).sqrt();
32        let outer_crust = 1.0 / (layers.outer_layer + crack_size_adjustment).sqrt();
33        let crack_size = 1.0
34            / (crack.base_crack_size
35                + random.next_f64() / 2.0
36                + if num_points > 3 {
37                    crack_size_adjustment
38                } else {
39                    0.0
40                })
41            .sqrt();
42        let should_generate_crack = random.next_f32() < crack.generate_crack_chance as f32;
43
44        let Some(points) =
45            Self::geode_distribution_points(region, random, registry, config, origin, num_points)
46        else {
47            return false;
48        };
49        let crack_points =
50            Self::geode_crack_points(random, origin, num_points, should_generate_crack);
51
52        let mut potential_crystal_placements = Vec::new();
53        let min = origin.offset(
54            config.min_gen_offset,
55            config.min_gen_offset,
56            config.min_gen_offset,
57        );
58        let max = origin.offset(
59            config.max_gen_offset,
60            config.max_gen_offset,
61            config.max_gen_offset,
62        );
63        Self::for_each_vanilla_between_closed(min, max, |pos| {
64            let noise_offset =
65                noise.get_value(f64::from(pos.x()), f64::from(pos.y()), f64::from(pos.z()))
66                    * config.noise_multiplier;
67            let dist_sum_shell =
68                Self::geode_distance_sum(pos, &points, noise_offset, |offset| offset);
69            let dist_sum_crack = Self::geode_distance_sum(pos, &crack_points, noise_offset, |_| {
70                crack.crack_point_offset
71            });
72
73            if dist_sum_shell >= outer_crust {
74                if should_generate_crack
75                    && dist_sum_crack >= crack_size
76                    && dist_sum_shell < inner_air
77                {
78                    Self::safe_set_geode_block(
79                        region,
80                        registry,
81                        pos,
82                        vanilla_blocks::AIR.default_state(),
83                        &blocks.cannot_replace,
84                    );
85                    Self::schedule_geode_adjacent_fluid_ticks(region, pos);
86                } else if dist_sum_shell >= inner_air {
87                    let state = Self::sample_block_state_provider(
88                        region,
89                        registry,
90                        random,
91                        &blocks.filling_provider,
92                        pos,
93                    );
94                    Self::safe_set_geode_block(
95                        region,
96                        registry,
97                        pos,
98                        state,
99                        &blocks.cannot_replace,
100                    );
101                } else if dist_sum_shell >= innermost_block_layer {
102                    let use_alternate_layer =
103                        random.next_f32() < config.use_alternate_layer0_chance as f32;
104                    let provider = if use_alternate_layer {
105                        &blocks.alternate_inner_layer_provider
106                    } else {
107                        &blocks.inner_layer_provider
108                    };
109                    let state =
110                        Self::sample_block_state_provider(region, registry, random, provider, pos);
111                    Self::safe_set_geode_block(
112                        region,
113                        registry,
114                        pos,
115                        state,
116                        &blocks.cannot_replace,
117                    );
118
119                    if (!config.placements_require_layer0_alternate || use_alternate_layer)
120                        && random.next_f32() < config.use_potential_placements_chance as f32
121                    {
122                        potential_crystal_placements.push(pos);
123                    }
124                } else if dist_sum_shell >= inner_crust {
125                    let state = Self::sample_block_state_provider(
126                        region,
127                        registry,
128                        random,
129                        &blocks.middle_layer_provider,
130                        pos,
131                    );
132                    Self::safe_set_geode_block(
133                        region,
134                        registry,
135                        pos,
136                        state,
137                        &blocks.cannot_replace,
138                    );
139                } else if dist_sum_shell >= outer_crust {
140                    let state = Self::sample_block_state_provider(
141                        region,
142                        registry,
143                        random,
144                        &blocks.outer_layer_provider,
145                        pos,
146                    );
147                    Self::safe_set_geode_block(
148                        region,
149                        registry,
150                        pos,
151                        state,
152                        &blocks.cannot_replace,
153                    );
154                }
155            }
156        });
157
158        Self::place_geode_crystals(
159            region,
160            registry,
161            random,
162            blocks,
163            &potential_crystal_placements,
164        );
165        true
166    }
167
168    fn geode_noise(seed: i64) -> NormalNoise {
169        let mut cache = GEODE_NOISE_BY_SEED.lock();
170        if let Some(noise) = cache.get(&seed) {
171            return noise.clone();
172        }
173
174        let mut random_source = RandomSource::Legacy(LegacyRandom::from_seed(seed as u64));
175        let noise = NormalNoise::create_from_random(&mut random_source, -4, &[1.0]);
176        cache.insert(seed, noise.clone());
177        noise
178    }
179
180    fn geode_distribution_points(
181        region: &WorldGenRegion<'_>,
182        random: &mut WorldgenRandom,
183        registry: &Registry,
184        config: &GeodeConfiguration,
185        origin: BlockPos,
186        num_points: i32,
187    ) -> Option<Vec<(BlockPos, i32)>> {
188        let mut points = Vec::new();
189        let mut invalid_points = 0;
190        for _ in 0..num_points {
191            let point = origin.offset(
192                config.outer_wall_distance.sample(random),
193                config.outer_wall_distance.sample(random),
194                config.outer_wall_distance.sample(random),
195            );
196            let state = region.block_state(point);
197            if state.is_air()
198                || registry
199                    .blocks
200                    .is_in_tag(state.get_block(), &config.blocks.invalid_blocks)
201            {
202                invalid_points += 1;
203                if invalid_points > config.invalid_blocks_threshold {
204                    return None;
205                }
206            }
207
208            points.push((point, config.point_offset.sample(random)));
209        }
210
211        Some(points)
212    }
213
214    fn geode_crack_points(
215        random: &mut WorldgenRandom,
216        origin: BlockPos,
217        num_points: i32,
218        should_generate_crack: bool,
219    ) -> Vec<(BlockPos, i32)> {
220        if !should_generate_crack {
221            return Vec::new();
222        }
223
224        let crack_offset = num_points * 2 + 1;
225        match random.next_i32_bounded(4) {
226            0 => vec![
227                (origin.offset(crack_offset, 7, 0), 0),
228                (origin.offset(crack_offset, 5, 0), 0),
229                (origin.offset(crack_offset, 1, 0), 0),
230            ],
231            1 => vec![
232                (origin.offset(0, 7, crack_offset), 0),
233                (origin.offset(0, 5, crack_offset), 0),
234                (origin.offset(0, 1, crack_offset), 0),
235            ],
236            2 => vec![
237                (origin.offset(crack_offset, 7, crack_offset), 0),
238                (origin.offset(crack_offset, 5, crack_offset), 0),
239                (origin.offset(crack_offset, 1, crack_offset), 0),
240            ],
241            _ => vec![
242                (origin.offset(0, 7, 0), 0),
243                (origin.offset(0, 5, 0), 0),
244                (origin.offset(0, 1, 0), 0),
245            ],
246        }
247    }
248
249    fn geode_distance_sum(
250        pos: BlockPos,
251        points: &[(BlockPos, i32)],
252        noise_offset: f64,
253        offset: impl Fn(i32) -> i32,
254    ) -> f64 {
255        points
256            .iter()
257            .map(|(point, point_offset)| {
258                1.0 / (Self::block_pos_distance_squared(pos, *point)
259                    + f64::from(offset(*point_offset)))
260                .sqrt()
261                    + noise_offset
262            })
263            .sum()
264    }
265
266    fn block_pos_distance_squared(left: BlockPos, right: BlockPos) -> f64 {
267        let dx = f64::from(left.x() - right.x());
268        let dy = f64::from(left.y() - right.y());
269        let dz = f64::from(left.z() - right.z());
270        dx * dx + dy * dy + dz * dz
271    }
272
273    fn safe_set_geode_block(
274        region: &mut WorldGenRegion<'_>,
275        registry: &Registry,
276        pos: BlockPos,
277        state: BlockStateId,
278        cannot_replace: &Identifier,
279    ) {
280        let current = region.block_state(pos);
281        if !registry
282            .blocks
283            .is_in_tag(current.get_block(), cannot_replace)
284        {
285            let _ = region.set_block_state(pos, state, UpdateFlags::UPDATE_CLIENTS);
286        }
287    }
288
289    fn schedule_geode_adjacent_fluid_ticks(region: &WorldGenRegion<'_>, pos: BlockPos) {
290        for direction in Self::VANILLA_DIRECTION_VALUES {
291            let adjacent = pos.relative(direction);
292            let fluid_state = get_fluid_state_from_block(region.block_state(adjacent));
293            if !fluid_state.is_empty() {
294                let _ = region.schedule_fluid_tick_default(adjacent, fluid_state.fluid_id, 0);
295            }
296        }
297    }
298
299    fn place_geode_crystals(
300        region: &mut WorldGenRegion<'_>,
301        registry: &Registry,
302        random: &mut WorldgenRandom,
303        blocks: &GeodeBlockSettings,
304        potential_crystal_placements: &[BlockPos],
305    ) {
306        for crystal_pos in potential_crystal_placements {
307            if blocks.inner_placements.is_empty() {
308                continue;
309            }
310            let Ok(bound) = i32::try_from(blocks.inner_placements.len()) else {
311                panic!(
312                    "geode inner placement count {} exceeds i32 range",
313                    blocks.inner_placements.len()
314                );
315            };
316            let index = random.next_i32_bounded(bound) as usize;
317            let mut state = Self::block_state_from_data(registry, &blocks.inner_placements[index]);
318
319            for direction in Self::VANILLA_DIRECTION_VALUES {
320                if state.try_get_value(&BlockStateProperties::FACING).is_some() {
321                    state = state.set_value(&BlockStateProperties::FACING, direction);
322                }
323
324                let place_pos = crystal_pos.relative(direction);
325                let place_state = region.block_state(place_pos);
326                if state
327                    .try_get_value(&BlockStateProperties::WATERLOGGED)
328                    .is_some()
329                {
330                    let waterlogged = place_state.get_block() == &vanilla_blocks::WATER
331                        && get_fluid_state_from_block(place_state).is_source();
332                    state = state.set_value(&BlockStateProperties::WATERLOGGED, waterlogged);
333                }
334
335                if Self::can_grow_amethyst_cluster_at(place_state) {
336                    Self::safe_set_geode_block(
337                        region,
338                        registry,
339                        place_pos,
340                        state,
341                        &blocks.cannot_replace,
342                    );
343                    break;
344                }
345            }
346        }
347    }
348
349    fn can_grow_amethyst_cluster_at(state: BlockStateId) -> bool {
350        state.is_air()
351            || (state.get_block() == &vanilla_blocks::WATER
352                && get_fluid_state_from_block(state).is_source())
353    }
354}