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 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}