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