1use super::super::instrumentation::OreFeatureProfile;
2use super::super::prelude::*;
3use super::super::runner::FeatureDecorationRunner;
4use smallvec::SmallVec;
5use std::f32::consts::PI;
6use std::time::Instant;
7use steel_math::trig;
8use steel_utils::PackedSectionBlockPos;
9use steel_worldgen::state_resolver::WorldgenStateResolver;
10
11impl FeatureDecorationRunner {
12 pub(in crate::worldgen::feature) fn place_ore_feature(
13 region: &mut WorldGenRegion<'_>,
14 registry: &Registry,
15 random: &mut WorldgenRandom,
16 config: &OreConfiguration,
17 origin: BlockPos,
18 ) -> bool {
19 if config.size <= 0 {
20 return false;
21 }
22 let direction = random.next_f32() * PI;
23 let spread_xz = config.size as f32 / 8.0;
24 let spread_xz_ceil = spread_xz.ceil() as i32;
25 let max_radius = f32::midpoint(config.size as f32 / 16.0 * 2.0, 1.0).ceil() as i32;
26 let sin = f64::from(direction).sin();
27 let cos = f64::from(direction).cos();
28 let x0 = f64::from(origin.x()) + sin * f64::from(spread_xz);
29 let x1 = f64::from(origin.x()) - sin * f64::from(spread_xz);
30 let z0 = f64::from(origin.z()) + cos * f64::from(spread_xz);
31 let z1 = f64::from(origin.z()) - cos * f64::from(spread_xz);
32 let y0 = f64::from(origin.y() + random.next_i32_bounded(3) - 2);
33 let y1 = f64::from(origin.y() + random.next_i32_bounded(3) - 2);
34 let x_start = origin.x() - spread_xz_ceil - max_radius;
35 let y_start = origin.y() - 2 - max_radius;
36 let z_start = origin.z() - spread_xz_ceil - max_radius;
37 let size_xz = 2 * (spread_xz_ceil + max_radius);
38 let size_y = 2 * (2 + max_radius);
39
40 for x_probe in x_start..=x_start + size_xz {
41 for z_probe in z_start..=z_start + size_xz {
42 if y_start <= region.height_at(HeightmapType::OceanFloorWg, x_probe, z_probe) {
43 return Self::do_place_ore(
44 region, registry, random, config, x0, x1, z0, z1, y0, y1, x_start, y_start,
45 z_start, size_xz, size_y,
46 );
47 }
48 }
49 }
50
51 false
52 }
53
54 #[expect(
55 clippy::too_many_arguments,
56 clippy::too_many_lines,
57 reason = "mirrors vanilla ore vein placement inputs"
58 )]
59 pub(in crate::worldgen::feature) fn do_place_ore(
60 region: &mut WorldGenRegion<'_>,
61 registry: &Registry,
62 random: &mut WorldgenRandom,
63 config: &OreConfiguration,
64 x0: f64,
65 x1: f64,
66 z0: f64,
67 z1: f64,
68 y0: f64,
69 y1: f64,
70 x_start: i32,
71 y_start: i32,
72 z_start: i32,
73 size_xz: i32,
74 size_y: i32,
75 ) -> bool {
76 let Ok(size) = usize::try_from(config.size) else {
77 return false;
78 };
79 let mut vein_nodes = SmallVec::<[[f64; 4]; 32]>::from_elem([0.0; 4], size);
80
81 for i in 0..size {
82 let step = i as f32 / config.size as f32;
83 let size_factor = random.next_f64() * f64::from(config.size) / 16.0;
84 let radius_wave = trig::sin(f64::from(PI * step)) + 1.0;
85 let radius = f64::from(radius_wave) * size_factor + 1.0;
86 vein_nodes[i] = [
87 lerp(f64::from(step), x0, x1),
88 lerp(f64::from(step), y0, y1),
89 lerp(f64::from(step), z0, z1),
90 radius / 2.0,
91 ];
92 }
93
94 for i1 in 0..size.saturating_sub(1) {
95 if vein_nodes[i1][3] <= 0.0 {
96 continue;
97 }
98
99 for i2 in i1 + 1..size {
100 if vein_nodes[i2][3] <= 0.0 {
101 continue;
102 }
103
104 let dx = vein_nodes[i1][0] - vein_nodes[i2][0];
105 let dy = vein_nodes[i1][1] - vein_nodes[i2][1];
106 let dz = vein_nodes[i1][2] - vein_nodes[i2][2];
107 let dr = vein_nodes[i1][3] - vein_nodes[i2][3];
108 if dr * dr > dx * dx + dy * dy + dz * dz {
109 if dr > 0.0 {
110 vein_nodes[i2][3] = -1.0;
111 } else {
112 vein_nodes[i1][3] = -1.0;
113 }
114 }
115 }
116 }
117
118 let Some(search_volume) = OreSearchVolume::new(size_xz, size_y) else {
119 return false;
120 };
121 let profile = OreFeatureProfile::new(config.size);
122 let mut placed = 0_u64;
123 let mut tested = OreTestedPositions::with_capacity(search_volume.tested_position_count);
124 let targets = ResolvedOreTargets::from_config(registry, config);
125 let batch_no_air_exposure = config.discard_chance_on_air_exposure <= 0.0;
126 let mut pending_no_air_sections = SmallVec::<[PendingOreSection; 8]>::new();
127 let min_y = region.min_y();
128 let height = region.height();
129
130 {
131 let mut sections = region.bulk_section_access_for_ore(profile.stats());
132 let candidate_started_at = profile.stats().map(|_| Instant::now());
133
134 placed += if profile.stats().is_some() {
135 Self::collect_ore_candidates::<true>(
136 &mut sections,
137 registry,
138 random,
139 config,
140 &targets,
141 search_volume,
142 vein_nodes,
143 x_start,
144 y_start,
145 z_start,
146 min_y,
147 height,
148 batch_no_air_exposure,
149 &mut tested,
150 &mut pending_no_air_sections,
151 )
152 } else {
153 Self::collect_ore_candidates::<false>(
154 &mut sections,
155 registry,
156 random,
157 config,
158 &targets,
159 search_volume,
160 vein_nodes,
161 x_start,
162 y_start,
163 z_start,
164 min_y,
165 height,
166 batch_no_air_exposure,
167 &mut tested,
168 &mut pending_no_air_sections,
169 )
170 };
171 if let Some(started_at) = candidate_started_at
172 && let Some(stats) = profile.stats()
173 {
174 stats
175 .borrow_mut()
176 .record_candidate_time(started_at.elapsed());
177 }
178
179 if batch_no_air_exposure {
180 let batch_apply_started_at = profile.stats().map(|_| Instant::now());
181 for pending_section in &pending_no_air_sections {
182 placed += sections.replace_ore_target_block_states_in_section(
183 pending_section.key.chunk_x,
184 pending_section.key.chunk_z,
185 pending_section.key.section_index,
186 &pending_section.positions,
187 |block_state| targets.matching_replacement(registry, block_state),
188 );
189 }
190 if let Some(started_at) = batch_apply_started_at
191 && let Some(stats) = profile.stats()
192 {
193 stats
194 .borrow_mut()
195 .record_batch_apply_time(started_at.elapsed());
196 }
197 }
198 }
199
200 profile.finish(placed);
201 placed > 0
202 }
203
204 #[expect(
205 clippy::too_many_arguments,
206 reason = "keeps the vanilla ore candidate loop monomorphized without hiding state"
207 )]
208 fn collect_ore_candidates<const PROFILE: bool>(
209 sections: &mut WorldGenBulkSectionAccess<'_, '_, '_>,
210 registry: &Registry,
211 random: &mut WorldgenRandom,
212 config: &OreConfiguration,
213 targets: &ResolvedOreTargets,
214 search_volume: OreSearchVolume,
215 vein_nodes: SmallVec<[[f64; 4]; 32]>,
216 x_start: i32,
217 y_start: i32,
218 z_start: i32,
219 min_y: i32,
220 height: i32,
221 batch_no_air_exposure: bool,
222 tested: &mut OreTestedPositions,
223 pending_no_air_sections: &mut SmallVec<[PendingOreSection; 8]>,
224 ) -> u64 {
225 let mut placed = 0_u64;
226
227 for node in vein_nodes {
228 let radius = node[3];
229 if radius < 0.0 {
230 continue;
231 }
232
233 let x_min = fast_floor(node[0] - radius).max(x_start);
234 let z_min = fast_floor(node[2] - radius).max(z_start);
235 let x_max = fast_floor(node[0] + radius).max(x_min);
236 let z_max = fast_floor(node[2] + radius).max(z_min);
237 let raw_y_min = fast_floor(node[1] - radius).max(y_start);
238 let raw_y_max = fast_floor(node[1] + radius).max(raw_y_min);
239 let y_min = raw_y_min.max(min_y);
240 let y_max = raw_y_max.min(min_y + height - 1);
241 if y_min > y_max {
242 continue;
243 }
244
245 for x in x_min..=x_max {
246 let x_offset = i64::from(x) - i64::from(x_start);
247 let x_distance = (f64::from(x) + 0.5 - node[0]) / radius;
248 let x_distance_squared = x_distance * x_distance;
249 if x_distance_squared >= 1.0 {
250 continue;
251 }
252
253 for y in y_min..=y_max {
254 let y_offset = i64::from(y) - i64::from(y_start);
255 let y_distance = (f64::from(y) + 0.5 - node[1]) / radius;
256 let x_y_distance_squared = x_distance_squared + y_distance * y_distance;
257 if x_y_distance_squared >= 1.0 {
258 continue;
259 }
260
261 for z in z_min..=z_max {
262 let z_offset = i64::from(z) - i64::from(z_start);
263 let z_distance = (f64::from(z) + 0.5 - node[2]) / radius;
264 if x_y_distance_squared + z_distance * z_distance >= 1.0 {
265 continue;
266 }
267
268 if PROFILE {
269 sections.record_ore_candidate_position();
270 }
271 let Some(tested_index) =
272 search_volume.index_from_offsets(x_offset, y_offset, z_offset)
273 else {
274 continue;
275 };
276 if tested.insert(tested_index) {
277 if PROFILE {
278 sections.record_ore_unique_position();
279 }
280 if batch_no_air_exposure {
281 let section_key =
282 PendingOreSectionKey::from_in_height_coords(min_y, x, y, z);
283 let Some(pos) = PackedSectionBlockPos::from_local_xyz(
284 (x & 15) as u8,
285 (y & 15) as u8,
286 (z & 15) as u8,
287 ) else {
288 panic!("masked ore section-local position escaped section");
289 };
290 push_pending_ore_position(
291 pending_no_air_sections,
292 section_key,
293 pos,
294 );
295 } else {
296 let pos = BlockPos::new(x, y, z);
297 if sections.can_write_to_pos(pos) {
298 if PROFILE {
299 sections.record_ore_write_allowed_position();
300 }
301 if Self::try_place_ore_block_in_bulk(
302 sections, registry, random, config, targets, pos,
303 ) {
304 placed += 1;
305 }
306 }
307 }
308 }
309 }
310 }
311 }
312 }
313
314 placed
315 }
316
317 pub(in crate::worldgen::feature) fn place_scattered_ore_feature(
318 region: &mut WorldGenRegion<'_>,
319 registry: &Registry,
320 random: &mut WorldgenRandom,
321 config: &OreConfiguration,
322 origin: BlockPos,
323 ) -> bool {
324 assert!(
325 config.size >= 0,
326 "scattered ore size {} is negative",
327 config.size
328 );
329
330 let targets = ResolvedOreTargets::from_config(registry, config);
331 let tries = random.next_i32_bounded(config.size + 1);
332 for i in 0..tries {
333 let max_distance = i.min(7);
334 let pos = origin.offset(
335 Self::random_scattered_ore_offset(random, max_distance),
336 Self::random_scattered_ore_offset(random, max_distance),
337 Self::random_scattered_ore_offset(random, max_distance),
338 );
339 let _ =
340 Self::try_place_resolved_ore_block(region, registry, random, config, &targets, pos);
341 }
342
343 true
344 }
345
346 pub(in crate::worldgen::feature) fn random_scattered_ore_offset(
347 random: &mut WorldgenRandom,
348 max_distance: i32,
349 ) -> i32 {
350 Self::java_round_f32((random.next_f32() - random.next_f32()) * max_distance as f32)
351 }
352
353 pub(in crate::worldgen::feature) fn java_round_f32(value: f32) -> i32 {
354 (value + 0.5).floor() as i32
355 }
356
357 fn try_place_resolved_ore_block(
358 region: &mut WorldGenRegion<'_>,
359 registry: &Registry,
360 random: &mut WorldgenRandom,
361 config: &OreConfiguration,
362 targets: &ResolvedOreTargets,
363 pos: BlockPos,
364 ) -> bool {
365 let block_state = region.block_state(pos);
366 let block_id = ResolvedOreTargets::block_id_for_state(registry, block_state);
367 for target in targets.iter() {
368 if Self::can_place_resolved_ore(region, registry, random, config, target, pos, block_id)
369 {
370 return region.set_block_state(pos, target.state, UpdateFlags::UPDATE_CLIENTS);
371 }
372 }
373
374 false
375 }
376
377 fn try_place_ore_block_in_bulk(
378 sections: &mut WorldGenBulkSectionAccess<'_, '_, '_>,
379 registry: &Registry,
380 random: &mut WorldgenRandom,
381 config: &OreConfiguration,
382 targets: &ResolvedOreTargets,
383 pos: BlockPos,
384 ) -> bool {
385 if config.discard_chance_on_air_exposure <= 0.0 {
386 return sections.replace_ore_target_block_state(pos, |block_state| {
387 targets.matching_replacement(registry, block_state)
388 });
389 }
390
391 let block_state = sections.ore_target_block_state(pos);
392 let block_id = ResolvedOreTargets::block_id_for_state(registry, block_state);
393 for target in targets.iter() {
394 if Self::can_place_resolved_ore_in_bulk(
395 sections, registry, random, config, target, pos, block_id,
396 ) {
397 return sections.set_block_state(pos, target.state);
398 }
399 }
400
401 false
402 }
403
404 fn can_place_resolved_ore(
405 region: &WorldGenRegion<'_>,
406 registry: &Registry,
407 random: &mut WorldgenRandom,
408 config: &OreConfiguration,
409 target: &ResolvedOreTarget,
410 pos: BlockPos,
411 block_id: usize,
412 ) -> bool {
413 if !target.matches_block_id(block_id) {
414 return false;
415 }
416
417 if Self::should_skip_air_check(random, config.discard_chance_on_air_exposure) {
418 return true;
419 }
420
421 !Self::is_adjacent_to_air(region, registry, pos)
422 }
423
424 fn can_place_resolved_ore_in_bulk(
425 sections: &mut WorldGenBulkSectionAccess<'_, '_, '_>,
426 registry: &Registry,
427 random: &mut WorldgenRandom,
428 config: &OreConfiguration,
429 target: &ResolvedOreTarget,
430 pos: BlockPos,
431 block_id: usize,
432 ) -> bool {
433 if !target.matches_block_id(block_id) {
434 return false;
435 }
436
437 if Self::should_skip_air_check(random, config.discard_chance_on_air_exposure) {
438 return true;
439 }
440
441 !Self::is_adjacent_to_air_in_bulk(sections, registry, pos)
442 }
443
444 pub(in crate::worldgen::feature) fn should_skip_air_check(
445 random: &mut WorldgenRandom,
446 discard_chance_on_air_exposure: f32,
447 ) -> bool {
448 if discard_chance_on_air_exposure <= 0.0 {
449 true
450 } else if discard_chance_on_air_exposure >= 1.0 {
451 false
452 } else {
453 random.next_f32() >= discard_chance_on_air_exposure
454 }
455 }
456
457 pub(in crate::worldgen::feature) fn is_adjacent_to_air(
458 region: &WorldGenRegion<'_>,
459 registry: &Registry,
460 pos: BlockPos,
461 ) -> bool {
462 Direction::ALL.into_iter().any(|direction| {
463 let neighbor = region.block_state(pos.relative(direction));
464 Self::is_air_block_state(registry, neighbor)
465 })
466 }
467
468 pub(in crate::worldgen::feature) fn is_adjacent_to_air_in_bulk(
469 sections: &mut WorldGenBulkSectionAccess<'_, '_, '_>,
470 registry: &Registry,
471 pos: BlockPos,
472 ) -> bool {
473 Direction::ALL.into_iter().any(|direction| {
474 let neighbor = sections.ore_neighbor_block_state(pos.relative(direction));
475 Self::is_air_block_state(registry, neighbor)
476 })
477 }
478
479 pub(in crate::worldgen::feature) fn is_air_block_state(
480 registry: &Registry,
481 state: BlockStateId,
482 ) -> bool {
483 let Some(block) = registry.blocks.by_state_id(state) else {
484 panic!("feature received invalid block state id {}", state.0);
485 };
486 block.config.is_air
487 }
488}
489
490struct OreTestedPositions {
491 words: SmallVec<[u64; 16]>,
492}
493
494#[derive(Clone, Copy, PartialEq, Eq)]
495struct PendingOreSectionKey {
496 chunk_x: i32,
497 chunk_z: i32,
498 section_index: usize,
499}
500
501struct PendingOreSection {
502 key: PendingOreSectionKey,
503 positions: SmallVec<[PackedSectionBlockPos; 256]>,
504}
505
506struct ResolvedOreTargets {
507 targets: SmallVec<[ResolvedOreTarget; 2]>,
508}
509
510struct ResolvedOreTarget {
511 matcher: ResolvedOreRuleTest,
512 state: BlockStateId,
513}
514
515enum ResolvedOreRuleTest {
516 Block(usize),
517 Tag(SmallVec<[usize; 8]>),
518}
519
520#[derive(Clone, Copy)]
521struct OreSearchVolume {
522 size_xz: i64,
523 size_xz_y: i64,
524 tested_position_count: usize,
525}
526
527impl OreSearchVolume {
528 fn new(size_xz: i32, size_y: i32) -> Option<Self> {
529 let size_xz = i64::from(size_xz);
530 let size_y = i64::from(size_y);
531 if size_xz <= 0 || size_y <= 0 {
532 return None;
533 }
534
535 let size_xz_y = size_xz.checked_mul(size_y)?;
536 let tested_position_count = usize::try_from(size_xz_y.checked_mul(size_xz)?).ok()?;
537 Some(Self {
538 size_xz,
539 size_xz_y,
540 tested_position_count,
541 })
542 }
543
544 #[inline]
545 fn index_from_offsets(self, x_offset: i64, y_offset: i64, z_offset: i64) -> Option<usize> {
546 if x_offset < 0 || y_offset < 0 || z_offset < 0 {
547 return None;
548 }
549
550 let index = x_offset + y_offset * self.size_xz + z_offset * self.size_xz_y;
552 usize::try_from(index).ok()
553 }
554}
555
556impl ResolvedOreTargets {
557 fn from_config(registry: &Registry, config: &OreConfiguration) -> Self {
558 let mut targets = SmallVec::with_capacity(config.targets.len());
559 for target in &config.targets {
560 let matcher = match &target.target {
561 RuleTest::BlockMatch { block } => ResolvedOreRuleTest::Block(block.id()),
562 RuleTest::TagMatch { tag } => {
563 let block_ids = registry
564 .blocks
565 .iter_tag(tag)
566 .map(steel_registry::RegistryEntry::id)
567 .collect();
568 ResolvedOreRuleTest::Tag(block_ids)
569 }
570 };
571 let state = WorldgenStateResolver::feature_block_state_from_data(
572 registry,
573 &target.state,
574 "ore feature",
575 );
576 targets.push(ResolvedOreTarget { matcher, state });
577 }
578
579 Self { targets }
580 }
581
582 fn iter(&self) -> impl Iterator<Item = &ResolvedOreTarget> {
583 self.targets.iter()
584 }
585
586 fn matching_replacement(
587 &self,
588 registry: &Registry,
589 state: BlockStateId,
590 ) -> Option<BlockStateId> {
591 let block_id = Self::block_id_for_state(registry, state);
592 self.targets
593 .iter()
594 .find_map(|target| target.matches_block_id(block_id).then_some(target.state))
595 }
596
597 fn block_id_for_state(registry: &Registry, state: BlockStateId) -> usize {
598 let Some(&block_id) = registry.blocks.state_to_block_id.get(state.0 as usize) else {
599 panic!("ore feature received invalid block state id {}", state.0);
600 };
601 block_id
602 }
603}
604
605impl ResolvedOreTarget {
606 fn matches_block_id(&self, block_id: usize) -> bool {
607 match &self.matcher {
608 ResolvedOreRuleTest::Block(target_block_id) => block_id == *target_block_id,
609 ResolvedOreRuleTest::Tag(block_ids) => block_ids.contains(&block_id),
610 }
611 }
612}
613
614impl PendingOreSectionKey {
615 const fn from_in_height_coords(min_y: i32, x: i32, y: i32, z: i32) -> Self {
616 Self {
617 chunk_x: SectionPos::block_to_section_coord(x),
618 chunk_z: SectionPos::block_to_section_coord(z),
619 section_index: ((y - min_y) / 16) as usize,
620 }
621 }
622}
623
624fn push_pending_ore_position(
625 sections: &mut SmallVec<[PendingOreSection; 8]>,
626 key: PendingOreSectionKey,
627 pos: PackedSectionBlockPos,
628) {
629 if let Some(section) = sections.last_mut()
630 && section.key == key
631 {
632 section.positions.push(pos);
633 return;
634 }
635
636 if let Some(section) = sections.iter_mut().find(|section| section.key == key) {
637 section.positions.push(pos);
638 return;
639 }
640
641 sections.push(PendingOreSection {
642 key,
643 positions: smallvec::smallvec![pos],
644 });
645}
646
647impl OreTestedPositions {
648 fn with_capacity(bit_count: usize) -> Self {
649 Self {
650 words: smallvec::smallvec![0; bit_count.div_ceil(u64::BITS as usize)],
651 }
652 }
653
654 fn insert(&mut self, index: usize) -> bool {
655 let word_index = index / u64::BITS as usize;
656 if word_index >= self.words.len() {
657 self.words.resize(word_index + 1, 0);
658 }
659
660 let mask = 1_u64 << (index % u64::BITS as usize);
661 let word = &mut self.words[word_index];
662 if *word & mask != 0 {
663 return false;
664 }
665
666 *word |= mask;
667 true
668 }
669}
670
671#[cfg(test)]
672mod tests {
673 use super::{OreSearchVolume, OreTestedPositions};
674
675 #[test]
676 fn ore_tested_position_index_matches_vanilla_layout() {
677 let volume = OreSearchVolume::new(4, 6);
678 assert_eq!(
679 volume.and_then(|volume| volume.index_from_offsets(2, 3, 1)),
680 Some(38)
681 );
682 }
683
684 #[test]
685 fn ore_tested_position_index_keeps_vanilla_inclusive_edge_layout() {
686 let volume = OreSearchVolume::new(4, 6);
687 assert_eq!(
688 volume.and_then(|volume| volume.index_from_offsets(4, 0, 0)),
689 Some(4)
690 );
691 assert_eq!(
692 volume.and_then(|volume| volume.index_from_offsets(0, 1, 0)),
693 Some(4)
694 );
695 }
696
697 #[test]
698 fn ore_tested_positions_deduplicate_and_grow() {
699 let mut tested = OreTestedPositions::with_capacity(1);
700 assert!(tested.insert(0));
701 assert!(!tested.insert(0));
702 assert!(tested.insert(130));
703 assert!(!tested.insert(130));
704 }
705}