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