Skip to main content

steel_core/worldgen/feature/features/
ore.rs

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        // Matches vanilla OreFeature's BitSet index layout.
551        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}