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}