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}