Skip to main content

steel_worldgen/structure/
generator.rs

1//! Shared structure placement/selection engine.
2
3use std::{iter, path::Path};
4
5use rustc_hash::{FxHashMap, FxHashSet};
6use steel_registry::REGISTRY;
7use steel_registry::biome::BiomeRef;
8use steel_registry::structure::StructureRef;
9use steel_registry::template_pool::{TemplateData, TemplatePoolData};
10use steel_registry::vanilla_template_pools::{vanilla_template_pools, vanilla_templates};
11use steel_utils::random::Random;
12use steel_utils::random::legacy_random::LegacyRandom;
13use steel_utils::saved_data::{SavedDataManager, names as saved_data_names};
14use steel_utils::{BlockPos, ChunkPos, Identifier, PackedChunkPos};
15use wincode::{SchemaRead, SchemaWrite};
16
17use crate::biomes::BiomeSourceKind;
18use crate::structure::desert_pyramid::DesertPyramidStructure;
19use crate::structure::end_city::EndCityStructure;
20use crate::structure::fortress::NetherFortressStructure;
21use crate::structure::igloo::IglooStructure;
22use crate::structure::jigsaw::JigsawStructure;
23use crate::structure::jungle_temple::JungleTempleStructure;
24use crate::structure::mansion::WoodlandMansionStructure;
25use crate::structure::mineshaft::MineshaftStructure;
26use crate::structure::nether_fossil::NetherFossilStructure;
27use crate::structure::ocean_monument::OceanMonumentStructure;
28use crate::structure::ocean_ruin::OceanRuinStructure;
29use crate::structure::placement::{
30    PlacementKind, StructurePlacement, StructureSelectionEntry, StructureSet,
31    generate_ring_positions, load_vanilla_structure_sets,
32};
33use crate::structure::ruined_portal::RuinedPortalStructure;
34use crate::structure::shipwreck::ShipwreckStructure;
35use crate::structure::single_piece::BuriedTreasureStructure;
36use crate::structure::stronghold::StrongholdStructure;
37use crate::structure::swamp_hut::SwampHutStructure;
38use crate::structure::{GenerationStub, Structure, StructureGenerationContext, StructureStart};
39
40const VANILLA_FLAT_RING_POSITION_SEED: i64 = 0;
41
42#[derive(SchemaWrite, SchemaRead)]
43struct RingPositionCache {
44    entries: Vec<RingPositionCacheEntry>,
45}
46
47impl RingPositionCache {
48    const fn empty() -> Self {
49        Self {
50            entries: Vec::new(),
51        }
52    }
53}
54
55#[derive(SchemaWrite, SchemaRead)]
56struct RingPositionCacheEntry {
57    key: RingPositionCacheKey,
58    positions: Vec<PackedChunkPos>,
59}
60
61impl RingPositionCacheEntry {
62    fn runtime_positions(&self) -> Vec<ChunkPos> {
63        self.positions
64            .iter()
65            .copied()
66            .map(PackedChunkPos::to_chunk_pos)
67            .collect()
68    }
69}
70
71#[derive(PartialEq, Eq, SchemaWrite, SchemaRead)]
72struct RingPositionCacheKey {
73    structure_set: Identifier,
74    ring_seed: i64,
75    biome_provider: String,
76    distance: i32,
77    spread: i32,
78    count: i32,
79    preferred_biomes: Vec<Identifier>,
80}
81
82/// Biome operations needed while building `ChunkGeneratorStructureState`.
83pub trait StructureBiomeProvider {
84    /// Every biome this provider can produce.
85    fn possible_biomes(&self) -> FxHashSet<Identifier>;
86
87    /// Stable identity for every provider input that can affect ring snapping.
88    ///
89    /// Providers without a stable identity are not eligible for ring-position
90    /// persistence and should keep the default `None`.
91    fn ring_position_cache_key(&self) -> Option<String> {
92        None
93    }
94
95    /// Vanilla's `BiomeSource.findBiomeHorizontal(findClosest=false, skipSteps=1)`.
96    fn find_biome_horizontal(
97        &self,
98        origin_x: i32,
99        origin_z: i32,
100        search_radius: i32,
101        allowed: &dyn Fn(&Identifier) -> bool,
102        rng: &mut LegacyRandom,
103    ) -> Option<(i32, i32)>;
104}
105
106impl StructureBiomeProvider for BiomeSourceKind {
107    fn possible_biomes(&self) -> FxHashSet<Identifier> {
108        BiomeSourceKind::possible_biomes(self)
109    }
110
111    fn ring_position_cache_key(&self) -> Option<String> {
112        let key = match self {
113            Self::Overworld(source) => {
114                format!("multi_noise/minecraft:overworld/{}", source.seed())
115            }
116            Self::Nether(source) => {
117                format!("multi_noise/minecraft:the_nether/{}", source.seed())
118            }
119            Self::End(source) => format!("minecraft:the_end/{}", source.seed()),
120        };
121        Some(key)
122    }
123
124    fn find_biome_horizontal(
125        &self,
126        origin_x: i32,
127        origin_z: i32,
128        search_radius: i32,
129        allowed: &dyn Fn(&Identifier) -> bool,
130        rng: &mut LegacyRandom,
131    ) -> Option<(i32, i32)> {
132        BiomeSourceKind::find_biome_horizontal(
133            self,
134            origin_x,
135            origin_z,
136            search_radius,
137            &|biome| allowed(&biome.key),
138            rng,
139        )
140    }
141}
142
143/// Fixed-biome provider used by flat generation settings.
144pub struct FixedStructureBiomeProvider {
145    biome: BiomeRef,
146}
147
148impl FixedStructureBiomeProvider {
149    /// Creates a fixed-biome provider.
150    #[must_use]
151    pub const fn new(biome: BiomeRef) -> Self {
152        Self { biome }
153    }
154}
155
156impl StructureBiomeProvider for FixedStructureBiomeProvider {
157    fn possible_biomes(&self) -> FxHashSet<Identifier> {
158        FxHashSet::from_iter([self.biome.key.clone()])
159    }
160
161    fn ring_position_cache_key(&self) -> Option<String> {
162        Some(format!("fixed/{}", self.biome.key))
163    }
164
165    fn find_biome_horizontal(
166        &self,
167        origin_x: i32,
168        origin_z: i32,
169        search_radius: i32,
170        allowed: &dyn Fn(&Identifier) -> bool,
171        rng: &mut LegacyRandom,
172    ) -> Option<(i32, i32)> {
173        if !allowed(&self.biome.key) {
174            return None;
175        }
176
177        let noise_center_x = origin_x >> 2;
178        let noise_center_z = origin_z >> 2;
179        let noise_radius = search_radius >> 2;
180        let mut result = None;
181        let mut found = 0;
182        for z in -noise_radius..=noise_radius {
183            for x in -noise_radius..=noise_radius {
184                if result.is_none() || rng.next_i32_bounded(found + 1) == 0 {
185                    result = Some(((noise_center_x + x) << 2, (noise_center_z + z) << 2));
186                }
187                found += 1;
188            }
189        }
190        result
191    }
192}
193
194/// Runtime equivalent of vanilla's `ChunkGeneratorStructureState` plus structure
195/// implementation dispatch.
196pub struct StructureGenerator {
197    seed: i64,
198    structure_sets: Vec<(Identifier, StructureSet)>,
199    structure_set_indices: FxHashMap<Identifier, usize>,
200    structure_data: FxHashMap<Identifier, StructureRef>,
201    ring_positions: FxHashMap<Identifier, Vec<ChunkPos>>,
202    template_pools: FxHashMap<Identifier, TemplatePoolData>,
203    templates: FxHashMap<Identifier, TemplateData>,
204    structure_impls: FxHashMap<Identifier, Box<dyn Structure>>,
205}
206
207/// Runtime assets required by structure generation beyond the structure-set list.
208///
209/// Vanilla datapacks let structure sets, template pools, NBT templates, and
210/// structure implementation dispatch vary together. Use
211/// `StructureGenerator::vanilla_with_structure_sets` only when the set list is
212/// custom but all other assets are still vanilla.
213pub struct StructureGeneratorAssets {
214    template_pools: FxHashMap<Identifier, TemplatePoolData>,
215    templates: FxHashMap<Identifier, TemplateData>,
216    structure_impls: FxHashMap<Identifier, Box<dyn Structure>>,
217}
218
219impl StructureGeneratorAssets {
220    /// Creates an explicit structure asset bundle.
221    #[must_use]
222    pub fn new(
223        template_pools: FxHashMap<Identifier, TemplatePoolData>,
224        templates: FxHashMap<Identifier, TemplateData>,
225        structure_impls: FxHashMap<Identifier, Box<dyn Structure>>,
226    ) -> Self {
227        Self {
228            template_pools,
229            templates,
230            structure_impls,
231        }
232    }
233
234    /// Creates an asset bundle from generated vanilla registries and built-in
235    /// structure implementation dispatch.
236    #[must_use]
237    pub fn vanilla() -> Self {
238        let template_pools: FxHashMap<_, _> = vanilla_template_pools()
239            .into_iter()
240            .map(|pool| (pool.key.clone(), pool))
241            .collect();
242        let templates: FxHashMap<_, _> = vanilla_templates().into_iter().collect();
243
244        Self {
245            template_pools,
246            templates,
247            structure_impls: vanilla_structure_impls(),
248        }
249    }
250}
251
252/// Search plan for vanilla `/locate structure` queries.
253#[derive(Debug, Clone)]
254pub struct StructureLocatePlan {
255    seed: i64,
256    placements: Vec<StructureLocatePlacement>,
257}
258
259/// A structure placement that can produce the requested structure.
260#[derive(Debug, Clone)]
261pub struct StructureLocatePlacement {
262    placement: StructurePlacement,
263    ring_positions: Option<Vec<ChunkPos>>,
264    structures: Vec<Identifier>,
265}
266
267/// Candidate chunk and locate position for a structure search.
268#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
269pub struct StructureLocateCandidate {
270    /// Chunk that must be generated through `StructureStarts`.
271    pub chunk_pos: ChunkPos,
272    /// Position reported if the structure is present.
273    pub locate_pos: BlockPos,
274    scan_id: usize,
275    ring_distance_pos: BlockPos,
276}
277
278impl StructureLocatePlan {
279    /// Returns `true` if this plan has no placements to scan.
280    #[must_use]
281    pub const fn is_empty(&self) -> bool {
282        self.placements.is_empty()
283    }
284
285    /// Returns `true` if the plan has any random-spread placements.
286    #[must_use]
287    pub fn has_random_spread(&self) -> bool {
288        self.placements.iter().any(|placement| {
289            matches!(
290                &placement.placement.kind,
291                PlacementKind::RandomSpread { .. }
292            )
293        })
294    }
295
296    /// Requested structures associated with the candidate's placement scan.
297    #[must_use]
298    pub fn structures_for_candidate(
299        &self,
300        candidate: StructureLocateCandidate,
301    ) -> Option<&[Identifier]> {
302        self.placements
303            .get(candidate.scan_id)
304            .map(|placement| placement.structures.as_slice())
305    }
306
307    /// Ring-placement candidates ordered by vanilla's stronghold distance pre-check.
308    #[must_use]
309    pub fn ring_candidates(&self, origin: BlockPos) -> Vec<StructureLocateCandidate> {
310        let mut candidates = Vec::new();
311        for (scan_id, placement) in self.placements.iter().enumerate() {
312            let Some(ring_positions) = &placement.ring_positions else {
313                continue;
314            };
315            for &chunk_pos in ring_positions {
316                candidates.push(StructureLocateCandidate::new_ring(
317                    scan_id,
318                    placement.placement.locate_pos(chunk_pos),
319                    chunk_pos,
320                ));
321            }
322        }
323        candidates.sort_by_key(|candidate| candidate.ring_distance_squared(origin));
324        candidates
325    }
326
327    /// Random-spread candidates on the square shell at `radius` around `origin`.
328    ///
329    /// This matches vanilla's `ChunkGenerator.getNearestGeneratedStructure` scan:
330    /// each shell step moves by the placement spacing before resolving the
331    /// potential structure chunk inside that placement cell.
332    #[must_use]
333    pub fn random_spread_candidates_at_radius(
334        &self,
335        origin: BlockPos,
336        radius: i32,
337    ) -> Vec<StructureLocateCandidate> {
338        if radius < 0 {
339            return Vec::new();
340        }
341
342        let chunk_origin_x = origin.0.x >> 4;
343        let chunk_origin_z = origin.0.z >> 4;
344        let mut candidates = Vec::new();
345
346        for (scan_id, locate_placement) in self.placements.iter().enumerate() {
347            let PlacementKind::RandomSpread {
348                spacing,
349                separation,
350                spread_type,
351            } = &locate_placement.placement.kind
352            else {
353                continue;
354            };
355
356            for x in -radius..=radius {
357                let x_edge = x == -radius || x == radius;
358                for z in -radius..=radius {
359                    let z_edge = z == -radius || z == radius;
360                    if !x_edge && !z_edge {
361                        continue;
362                    }
363
364                    let sector_x = chunk_origin_x + *spacing * x;
365                    let sector_z = chunk_origin_z + *spacing * z;
366                    let chunk_pos = StructurePlacement::get_potential_structure_chunk(
367                        self.seed,
368                        locate_placement.placement.salt,
369                        sector_x,
370                        sector_z,
371                        *spacing,
372                        *separation,
373                        *spread_type,
374                    );
375                    let candidate = StructureLocateCandidate::new(
376                        scan_id,
377                        locate_placement.placement.locate_pos(chunk_pos),
378                        chunk_pos,
379                    );
380                    candidates.push(candidate);
381                }
382            }
383        }
384
385        candidates
386    }
387}
388
389impl StructureLocateCandidate {
390    const fn new(scan_id: usize, locate_pos: BlockPos, chunk_pos: ChunkPos) -> Self {
391        Self {
392            chunk_pos,
393            locate_pos,
394            scan_id,
395            ring_distance_pos: locate_pos,
396        }
397    }
398
399    const fn new_ring(scan_id: usize, locate_pos: BlockPos, chunk_pos: ChunkPos) -> Self {
400        Self {
401            chunk_pos,
402            locate_pos,
403            scan_id,
404            ring_distance_pos: BlockPos::new(chunk_pos.0.x * 16 + 8, 32, chunk_pos.0.y * 16 + 8),
405        }
406    }
407
408    /// Group id matching one structure placement scan.
409    #[must_use]
410    pub const fn scan_id(self) -> usize {
411        self.scan_id
412    }
413
414    fn ring_distance_squared(&self, origin: BlockPos) -> i64 {
415        squared_distance(self.ring_distance_pos, origin)
416    }
417}
418
419/// Squared block distance using vanilla's three-dimensional `BlockPos.distSqr`.
420#[must_use]
421pub fn squared_distance(a: BlockPos, b: BlockPos) -> i64 {
422    let dx = i64::from(a.0.x) - i64::from(b.0.x);
423    let dy = i64::from(a.0.y) - i64::from(b.0.y);
424    let dz = i64::from(a.0.z) - i64::from(b.0.z);
425    dx * dx + dy * dy + dz * dz
426}
427
428fn validate_structure_sets(structure_sets: &[(Identifier, StructureSet)]) {
429    for (set_key, set) in structure_sets {
430        assert!(
431            !set.structures.is_empty(),
432            "Structure set {set_key} must have at least one structure"
433        );
434        for (entry_index, entry) in set.structures.iter().enumerate() {
435            assert!(
436                entry.weight > 0,
437                "Structure set {set_key} entry {entry_index} has non-positive weight {}",
438                entry.weight
439            );
440        }
441        assert!(
442            !(!set.placement.frequency.is_finite()
443                || !(0.0..=1.0).contains(&set.placement.frequency)),
444            "Structure set {set_key} has invalid placement frequency {}",
445            set.placement.frequency
446        );
447        if let Some(exclusion) = &set.placement.exclusion_zone
448            && exclusion.chunk_count < 0
449        {
450            panic!(
451                "Structure set {set_key} has negative exclusion chunk_count {}",
452                exclusion.chunk_count
453            );
454        }
455
456        match &set.placement.kind {
457            PlacementKind::RandomSpread {
458                spacing,
459                separation,
460                ..
461            } => {
462                assert!(
463                    *spacing > 0,
464                    "Structure set {set_key} has non-positive spacing {spacing}"
465                );
466                assert!(
467                    *separation >= 0,
468                    "Structure set {set_key} has negative separation {separation}"
469                );
470                assert!(
471                    spacing > separation,
472                    "Structure set {set_key} has spacing {spacing} <= separation {separation}"
473                );
474            }
475            PlacementKind::ConcentricRings {
476                distance,
477                spread,
478                count,
479                ..
480            } => {
481                assert!(
482                    *distance > 0,
483                    "Structure set {set_key} has non-positive ring distance {distance}"
484                );
485                assert!(
486                    *spread > 0,
487                    "Structure set {set_key} has non-positive ring spread {spread}"
488                );
489                assert!(
490                    *count >= 0,
491                    "Structure set {set_key} has negative ring count {count}"
492                );
493            }
494        }
495    }
496}
497
498fn validate_structure_assets(
499    structure_sets: &[(Identifier, StructureSet)],
500    structure_data: &FxHashMap<Identifier, StructureRef>,
501    structure_impls: &FxHashMap<Identifier, Box<dyn Structure>>,
502) {
503    for (set_key, set) in structure_sets {
504        for entry in &set.structures {
505            let structure = structure_data.get(&entry.structure).unwrap_or_else(|| {
506                panic!(
507                    "Structure set {set_key} references unknown structure {}",
508                    entry.structure
509                )
510            });
511            assert!(
512                structure_impls.contains_key(&structure.structure_type),
513                "Structure set {set_key} references {} with unsupported structure type {}",
514                structure.key,
515                structure.structure_type
516            );
517        }
518    }
519}
520
521fn load_ring_position_cache(data_manager: &SavedDataManager) -> RingPositionCache {
522    match data_manager.sync_load_wincode::<RingPositionCache>(saved_data_names::STRUCTURE_RINGS) {
523        Ok(Some(cache)) => cache,
524        Ok(None) => RingPositionCache::empty(),
525        Err(error) => {
526            tracing::warn!("Couldn't load the structure ring cache: {error}");
527            RingPositionCache::empty()
528        }
529    }
530}
531
532struct RingPlacementInput<'a> {
533    structure_set: &'a Identifier,
534    ring_seed: i64,
535    distance: i32,
536    spread: i32,
537    count: i32,
538    preferred_biomes: &'a [Identifier],
539}
540
541impl RingPlacementInput<'_> {
542    fn cache_key(&self, biome_provider: &str) -> RingPositionCacheKey {
543        RingPositionCacheKey {
544            structure_set: self.structure_set.clone(),
545            ring_seed: self.ring_seed,
546            biome_provider: biome_provider.to_owned(),
547            distance: self.distance,
548            spread: self.spread,
549            count: self.count,
550            preferred_biomes: self.preferred_biomes.to_vec(),
551        }
552    }
553}
554
555fn ring_positions_for_placement(
556    input: RingPlacementInput<'_>,
557    biome_provider: &(impl StructureBiomeProvider + Sync),
558    thread_pool: &rayon::ThreadPool,
559    persisted: Option<(&str, &mut RingPositionCache)>,
560) -> (Vec<ChunkPos>, bool) {
561    let preferred_biomes: FxHashSet<_> = input.preferred_biomes.iter().cloned().collect();
562    let snap = |block_x: i32, block_z: i32, rng: &mut LegacyRandom| -> Option<(i32, i32)> {
563        biome_provider.find_biome_horizontal(
564            block_x,
565            block_z,
566            112,
567            &|biome| preferred_biomes.contains(biome),
568            rng,
569        )
570    };
571
572    let Some((provider_key, cache)) = persisted else {
573        return (
574            generate_ring_positions(
575                input.ring_seed,
576                input.distance,
577                input.spread,
578                input.count,
579                Some(&snap),
580                thread_pool,
581            ),
582            false,
583        );
584    };
585    let cache_key = input.cache_key(provider_key);
586    if let Some(entry) = cache.entries.iter().find(|entry| entry.key == cache_key) {
587        return (entry.runtime_positions(), false);
588    }
589
590    let positions = generate_ring_positions(
591        input.ring_seed,
592        input.distance,
593        input.spread,
594        input.count,
595        Some(&snap),
596        thread_pool,
597    );
598    cache
599        .entries
600        .retain(|entry| &entry.key.structure_set != input.structure_set);
601    cache.entries.push(RingPositionCacheEntry {
602        key: cache_key,
603        positions: positions
604            .iter()
605            .copied()
606            .map(PackedChunkPos::from)
607            .collect(),
608    });
609    (positions, true)
610}
611
612impl StructureGenerator {
613    /// Creates a structure generator over all vanilla structure sets.
614    #[must_use]
615    pub fn vanilla(
616        seed: i64,
617        world_path: Option<&Path>,
618        biome_provider: &(impl StructureBiomeProvider + Sync),
619        thread_pool: &rayon::ThreadPool,
620    ) -> Self {
621        Self::vanilla_with_structure_sets(
622            seed,
623            world_path,
624            biome_provider,
625            load_vanilla_structure_sets(),
626            thread_pool,
627        )
628    }
629
630    /// Creates a generator over an explicit structure-set list while keeping all
631    /// template pools, templates, and structure implementation dispatch vanilla.
632    #[must_use]
633    pub fn vanilla_with_structure_sets(
634        seed: i64,
635        world_path: Option<&Path>,
636        biome_provider: &(impl StructureBiomeProvider + Sync),
637        structure_sets: Vec<(Identifier, StructureSet)>,
638        thread_pool: &rayon::ThreadPool,
639    ) -> Self {
640        Self::with_assets_for_ring_seed(
641            seed,
642            seed,
643            world_path,
644            biome_provider,
645            structure_sets,
646            StructureGeneratorAssets::vanilla(),
647            thread_pool,
648        )
649    }
650
651    /// Creates a vanilla superflat structure generator.
652    ///
653    /// Vanilla superflat uses the level seed for random-spread placement and
654    /// structure selection, but always seeds concentric-ring positions with
655    /// `0L`.
656    #[must_use]
657    pub fn vanilla_flat_with_structure_sets(
658        seed: i64,
659        world_path: Option<&Path>,
660        biome_provider: &(impl StructureBiomeProvider + Sync),
661        structure_sets: Vec<(Identifier, StructureSet)>,
662        thread_pool: &rayon::ThreadPool,
663    ) -> Self {
664        Self::with_assets_for_ring_seed(
665            seed,
666            VANILLA_FLAT_RING_POSITION_SEED,
667            world_path,
668            biome_provider,
669            structure_sets,
670            StructureGeneratorAssets::vanilla(),
671            thread_pool,
672        )
673    }
674
675    /// Creates a generator from explicit structure sets and explicit runtime assets.
676    #[must_use]
677    pub fn with_assets(
678        seed: i64,
679        world_path: Option<&Path>,
680        biome_provider: &(impl StructureBiomeProvider + Sync),
681        structure_sets: Vec<(Identifier, StructureSet)>,
682        assets: StructureGeneratorAssets,
683        thread_pool: &rayon::ThreadPool,
684    ) -> Self {
685        Self::with_assets_for_ring_seed(
686            seed,
687            seed,
688            world_path,
689            biome_provider,
690            structure_sets,
691            assets,
692            thread_pool,
693        )
694    }
695
696    fn with_assets_for_ring_seed(
697        seed: i64,
698        ring_position_seed: i64,
699        world_path: Option<&Path>,
700        biome_provider: &(impl StructureBiomeProvider + Sync),
701        structure_sets: Vec<(Identifier, StructureSet)>,
702        assets: StructureGeneratorAssets,
703        thread_pool: &rayon::ThreadPool,
704    ) -> Self {
705        validate_structure_sets(&structure_sets);
706
707        let structure_data: FxHashMap<Identifier, StructureRef> = REGISTRY
708            .structures
709            .iter()
710            .map(|(_, structure)| (structure.key.clone(), structure))
711            .collect();
712        validate_structure_assets(&structure_sets, &structure_data, &assets.structure_impls);
713
714        let possible_biomes = biome_provider.possible_biomes();
715        let structure_sets: Vec<_> = structure_sets
716            .into_iter()
717            .filter(|(_, set)| {
718                set.structures.iter().any(|entry| {
719                    structure_data
720                        .get(&entry.structure)
721                        .is_some_and(|structure| {
722                            structure.allowed_biomes.is_empty()
723                                || structure
724                                    .allowed_biomes
725                                    .iter()
726                                    .any(|biome| possible_biomes.contains(biome))
727                        })
728                })
729            })
730            .collect();
731
732        let structure_set_indices: FxHashMap<Identifier, usize> = structure_sets
733            .iter()
734            .enumerate()
735            .map(|(index, (key, _))| (key.clone(), index))
736            .collect();
737
738        let data_manager = SavedDataManager::new(world_path);
739        let has_ring_placement = structure_sets
740            .iter()
741            .any(|(_, set)| matches!(&set.placement.kind, PlacementKind::ConcentricRings { .. }));
742        let mut persisted_ring_positions = if has_ring_placement {
743            biome_provider
744                .ring_position_cache_key()
745                .map(|provider_key| (provider_key, load_ring_position_cache(&data_manager)))
746        } else {
747            None
748        };
749        let mut ring_cache_dirty = false;
750        let mut ring_positions = FxHashMap::default();
751        for (key, set) in &structure_sets {
752            if let PlacementKind::ConcentricRings {
753                distance,
754                spread,
755                count,
756                preferred_biomes,
757            } = &set.placement.kind
758            {
759                let persisted = persisted_ring_positions
760                    .as_mut()
761                    .map(|(provider_key, cache)| (provider_key.as_str(), cache));
762                let (positions, cache_changed) = ring_positions_for_placement(
763                    RingPlacementInput {
764                        structure_set: key,
765                        ring_seed: ring_position_seed,
766                        distance: *distance,
767                        spread: *spread,
768                        count: *count,
769                        preferred_biomes,
770                    },
771                    biome_provider,
772                    thread_pool,
773                    persisted,
774                );
775                ring_cache_dirty |= cache_changed;
776                ring_positions.insert(key.clone(), positions);
777            }
778        }
779
780        if ring_cache_dirty
781            && let Some((_, cache)) = &persisted_ring_positions
782            && let Err(error) =
783                data_manager.sync_save_wincode(saved_data_names::STRUCTURE_RINGS, cache)
784        {
785            tracing::warn!("Couldn't save the structure ring cache: {error}");
786        }
787
788        Self {
789            seed,
790            structure_sets,
791            structure_set_indices,
792            structure_data,
793            ring_positions,
794            template_pools: assets.template_pools,
795            templates: assets.templates,
796            structure_impls: assets.structure_impls,
797        }
798    }
799
800    /// Template pool registry used by structure contexts.
801    #[must_use]
802    pub const fn template_pools(&self) -> &FxHashMap<Identifier, TemplatePoolData> {
803        &self.template_pools
804    }
805
806    /// Structure templates used by structure contexts.
807    #[must_use]
808    pub const fn templates(&self) -> &FxHashMap<Identifier, TemplateData> {
809        &self.templates
810    }
811
812    /// Builds a detached locate plan for one or more structure ids.
813    #[must_use]
814    pub fn locate_plan_for_structures(
815        &self,
816        structures: &[Identifier],
817    ) -> Option<StructureLocatePlan> {
818        let mut placements = Vec::new();
819        for (set_key, set) in &self.structure_sets {
820            let matching_structures = structures
821                .iter()
822                .filter(|structure| {
823                    set.structures
824                        .iter()
825                        .any(|entry| entry.structure == **structure)
826                })
827                .cloned()
828                .collect::<Vec<_>>();
829            if matching_structures.is_empty() {
830                continue;
831            }
832
833            placements.push(StructureLocatePlacement {
834                placement: set.placement.clone(),
835                ring_positions: self.ring_positions.get(set_key).cloned(),
836                structures: matching_structures,
837            });
838        }
839
840        (!placements.is_empty()).then_some(StructureLocatePlan {
841            seed: self.seed,
842            placements,
843        })
844    }
845
846    /// Generates structure starts for one chunk.
847    pub fn generate_starts_for_chunk(
848        &self,
849        ctx: &mut dyn StructureGenerationContext,
850        mut has_existing_start: impl FnMut(&Identifier) -> bool,
851    ) -> Vec<StructureStart> {
852        let chunk_x = ctx.chunk_x();
853        let chunk_z = ctx.chunk_z();
854        let mut generated_keys = FxHashSet::default();
855        let mut starts = Vec::new();
856
857        for (set_key, set) in &self.structure_sets {
858            if !self.is_structure_chunk_for_set(set_key, chunk_x, chunk_z, &mut Vec::new()) {
859                continue;
860            }
861
862            if set.structures.iter().any(|entry| {
863                generated_keys.contains(&entry.structure) || has_existing_start(&entry.structure)
864            }) {
865                continue;
866            }
867
868            let Some((structure, stub)) = self.select_structure(set, ctx) else {
869                continue;
870            };
871
872            let start = StructureStart::new(
873                structure.key.clone(),
874                ChunkPos::new(chunk_x, chunk_z),
875                stub.pieces,
876                structure.terrain_adjustment,
877            );
878            if !start.pieces.is_empty() {
879                generated_keys.insert(structure.key.clone());
880            }
881            starts.push(start);
882        }
883
884        starts
885    }
886
887    fn rings_for_set(&self, set_key: &Identifier) -> Option<&[ChunkPos]> {
888        self.ring_positions.get(set_key).map(Vec::as_slice)
889    }
890
891    fn is_structure_chunk_for_set(
892        &self,
893        set_key: &Identifier,
894        source_x: i32,
895        source_z: i32,
896        stack: &mut Vec<Identifier>,
897    ) -> bool {
898        if stack.iter().any(|key| key == set_key) {
899            let chain = stack
900                .iter()
901                .map(ToString::to_string)
902                .chain(iter::once(set_key.to_string()))
903                .collect::<Vec<_>>()
904                .join(" -> ");
905            panic!("Circular structure exclusion zone: {chain}");
906        }
907
908        let Some(&set_index) = self.structure_set_indices.get(set_key) else {
909            return false;
910        };
911        let (_, set) = &self.structure_sets[set_index];
912        let rings = self.rings_for_set(set_key);
913        if !set
914            .placement
915            .is_structure_chunk(self.seed, source_x, source_z, rings)
916        {
917            return false;
918        }
919
920        stack.push(set_key.clone());
921        let excluded = self.is_excluded(&set.placement, source_x, source_z, stack);
922        stack.pop();
923        !excluded
924    }
925
926    fn is_excluded(
927        &self,
928        placement: &StructurePlacement,
929        source_x: i32,
930        source_z: i32,
931        stack: &mut Vec<Identifier>,
932    ) -> bool {
933        let Some(exclusion) = &placement.exclusion_zone else {
934            return false;
935        };
936
937        for dx in (source_x - exclusion.chunk_count)..=(source_x + exclusion.chunk_count) {
938            for dz in (source_z - exclusion.chunk_count)..=(source_z + exclusion.chunk_count) {
939                if self.is_structure_chunk_for_set(&exclusion.other_set, dx, dz, stack) {
940                    return true;
941                }
942            }
943        }
944        false
945    }
946
947    fn select_structure(
948        &self,
949        set: &StructureSet,
950        ctx: &mut dyn StructureGenerationContext,
951    ) -> Option<(StructureRef, GenerationStub)> {
952        if set.structures.len() == 1 {
953            return self.try_generate_entry(&set.structures[0], ctx);
954        }
955
956        let mut rng = LegacyRandom::from_seed(0);
957        rng.set_large_feature_seed(self.seed, ctx.chunk_x(), ctx.chunk_z());
958
959        let mut remaining: Vec<&StructureSelectionEntry> = set.structures.iter().collect();
960        let mut total_weight: i32 = remaining.iter().map(|entry| entry.weight).sum();
961
962        while !remaining.is_empty() {
963            let mut choice = rng.next_i32_bounded(total_weight);
964            let mut selected_idx = 0;
965            for (idx, entry) in remaining.iter().enumerate() {
966                choice -= entry.weight;
967                if choice < 0 {
968                    selected_idx = idx;
969                    break;
970                }
971            }
972
973            let candidate = remaining[selected_idx];
974            if let Some(generated) = self.try_generate_entry(candidate, ctx) {
975                return Some(generated);
976            }
977
978            total_weight -= candidate.weight;
979            remaining.remove(selected_idx);
980        }
981
982        None
983    }
984
985    fn try_generate_entry(
986        &self,
987        entry: &StructureSelectionEntry,
988        ctx: &mut dyn StructureGenerationContext,
989    ) -> Option<(StructureRef, GenerationStub)> {
990        let Some(structure) = self.structure_data.get(&entry.structure).copied() else {
991            tracing::warn!("Missing structure registry data for {}", entry.structure);
992            return None;
993        };
994
995        if let Some(structure_impl) = self.structure_impls.get(&structure.structure_type) {
996            let mut rng = LegacyRandom::from_seed(0);
997            rng.set_large_feature_seed(self.seed, ctx.chunk_x(), ctx.chunk_z());
998            return structure_impl
999                .find_generation_point(ctx, structure, &mut rng)
1000                .map(|stub| (structure, stub));
1001        }
1002
1003        tracing::warn!(
1004            "Unknown structure type {:?} for {}, skipping structure start",
1005            structure.structure_type,
1006            structure.key
1007        );
1008        None
1009    }
1010}
1011
1012fn vanilla_structure_impls() -> FxHashMap<Identifier, Box<dyn Structure>> {
1013    let mut structures: FxHashMap<Identifier, Box<dyn Structure>> = FxHashMap::default();
1014    let mut reg = |key: &'static str, structure: Box<dyn Structure>| {
1015        structures.insert(Identifier::vanilla_static(key), structure);
1016    };
1017
1018    reg("jigsaw", Box::new(JigsawStructure));
1019    reg("nether_fossil", Box::new(NetherFossilStructure));
1020    reg("fortress", Box::new(NetherFortressStructure));
1021    reg("end_city", Box::new(EndCityStructure));
1022    reg("woodland_mansion", Box::new(WoodlandMansionStructure));
1023    reg("ocean_monument", Box::new(OceanMonumentStructure));
1024    reg("mineshaft", Box::new(MineshaftStructure));
1025    reg("desert_pyramid", Box::new(DesertPyramidStructure));
1026    reg("jungle_temple", Box::new(JungleTempleStructure));
1027    reg("swamp_hut", Box::new(SwampHutStructure));
1028    reg("buried_treasure", Box::new(BuriedTreasureStructure));
1029    reg("shipwreck", Box::new(ShipwreckStructure));
1030    reg("igloo", Box::new(IglooStructure));
1031    reg("ocean_ruin", Box::new(OceanRuinStructure));
1032    reg("stronghold", Box::new(StrongholdStructure));
1033    reg("ruined_portal", Box::new(RuinedPortalStructure));
1034
1035    structures
1036}
1037
1038#[cfg(test)]
1039mod tests {
1040    use std::{
1041        env::temp_dir,
1042        path::PathBuf,
1043        time::{SystemTime, UNIX_EPOCH},
1044    };
1045
1046    use glam::IVec3;
1047    use steel_registry::{init_vanilla_registry, vanilla_biomes};
1048
1049    use crate::structure::placement::{
1050        ExclusionZone, FrequencyReductionMethod, PlacementKind, SpreadType,
1051    };
1052
1053    use super::*;
1054
1055    fn random_spread_plan(locate_offset: IVec3) -> StructureLocatePlan {
1056        StructureLocatePlan {
1057            seed: 0,
1058            placements: vec![StructureLocatePlacement {
1059                placement: StructurePlacement {
1060                    salt: 10_387_312,
1061                    frequency: 1.0,
1062                    frequency_reduction_method: FrequencyReductionMethod::Default,
1063                    exclusion_zone: None,
1064                    locate_offset,
1065                    kind: PlacementKind::RandomSpread {
1066                        spacing: 32,
1067                        separation: 8,
1068                        spread_type: SpreadType::Linear,
1069                    },
1070                },
1071                ring_positions: None,
1072                structures: vec![Identifier::new("test", "placeholder")],
1073            }],
1074        }
1075    }
1076
1077    fn every_chunk_placement(excludes: Option<Identifier>) -> StructurePlacement {
1078        StructurePlacement {
1079            salt: 0,
1080            frequency: 1.0,
1081            frequency_reduction_method: FrequencyReductionMethod::Default,
1082            exclusion_zone: excludes.map(|other_set| ExclusionZone {
1083                other_set,
1084                chunk_count: 0,
1085            }),
1086            locate_offset: IVec3::ZERO,
1087            kind: PlacementKind::RandomSpread {
1088                spacing: 1,
1089                separation: 0,
1090                spread_type: SpreadType::Linear,
1091            },
1092        }
1093    }
1094
1095    fn test_structure_set(placement: StructurePlacement) -> StructureSet {
1096        StructureSet {
1097            structures: vec![StructureSelectionEntry {
1098                structure: Identifier::new("test", "placeholder"),
1099                weight: 1,
1100            }],
1101            placement,
1102        }
1103    }
1104
1105    fn generator_with_sets(sets: Vec<(Identifier, StructureSet)>) -> StructureGenerator {
1106        let structure_set_indices = sets
1107            .iter()
1108            .enumerate()
1109            .map(|(index, (key, _))| (key.clone(), index))
1110            .collect();
1111
1112        StructureGenerator {
1113            seed: 0,
1114            structure_sets: sets,
1115            structure_set_indices,
1116            structure_data: FxHashMap::default(),
1117            ring_positions: FxHashMap::default(),
1118            template_pools: FxHashMap::default(),
1119            templates: FxHashMap::default(),
1120            structure_impls: FxHashMap::default(),
1121        }
1122    }
1123
1124    fn temp_world_dir(test_name: &str) -> PathBuf {
1125        let unique = SystemTime::now()
1126            .duration_since(UNIX_EPOCH)
1127            .expect("system time should be after Unix epoch")
1128            .as_nanos();
1129        temp_dir().join(format!("steel-ring-cache-{test_name}-{unique}"))
1130    }
1131
1132    fn vanilla_stronghold_set() -> (Identifier, StructureSet) {
1133        load_vanilla_structure_sets()
1134            .into_iter()
1135            .find(|(key, _)| key == &Identifier::vanilla_static("strongholds"))
1136            .expect("vanilla stronghold structure set should exist")
1137    }
1138
1139    #[test]
1140    fn vanilla_assets_cover_vanilla_structure_sets() {
1141        init_vanilla_registry();
1142        let biome_provider = FixedStructureBiomeProvider::new(&vanilla_biomes::PLAINS);
1143        let thread_pool = rayon::ThreadPoolBuilder::default()
1144            .build()
1145            .expect("Couldn't create a new thread pool.");
1146        let _ = StructureGenerator::vanilla_with_structure_sets(
1147            0,
1148            None,
1149            &biome_provider,
1150            load_vanilla_structure_sets(),
1151            &thread_pool,
1152        );
1153    }
1154
1155    #[test]
1156    fn ring_cache_keys_entries_by_placement_inputs() {
1157        init_vanilla_registry();
1158        let world_dir = temp_world_dir("placement-inputs");
1159        let biome_provider = FixedStructureBiomeProvider::new(&vanilla_biomes::PLAINS);
1160        let thread_pool = rayon::ThreadPoolBuilder::new()
1161            .num_threads(2)
1162            .build()
1163            .expect("ring cache test thread pool should build");
1164        let (stronghold_key, stronghold_set) = vanilla_stronghold_set();
1165        let far_key = Identifier::new("test", "far_strongholds");
1166        let mut far_set = stronghold_set.clone();
1167        let PlacementKind::ConcentricRings { distance, .. } = &mut far_set.placement.kind else {
1168            panic!("vanilla stronghold set should use concentric rings");
1169        };
1170        *distance *= 2;
1171        let sets = vec![
1172            (stronghold_key.clone(), stronghold_set),
1173            (far_key.clone(), far_set),
1174        ];
1175
1176        let first = StructureGenerator::vanilla_with_structure_sets(
1177            1,
1178            Some(&world_dir),
1179            &biome_provider,
1180            sets.clone(),
1181            &thread_pool,
1182        );
1183        assert_ne!(
1184            first.ring_positions[&stronghold_key],
1185            first.ring_positions[&far_key]
1186        );
1187
1188        let second = StructureGenerator::vanilla_with_structure_sets(
1189            2,
1190            Some(&world_dir),
1191            &biome_provider,
1192            sets,
1193            &thread_pool,
1194        );
1195        assert_ne!(
1196            first.ring_positions[&stronghold_key],
1197            second.ring_positions[&stronghold_key]
1198        );
1199
1200        let cache: RingPositionCache = SavedDataManager::new(Some(&world_dir))
1201            .sync_load_wincode(saved_data_names::STRUCTURE_RINGS)
1202            .expect("ring cache should load")
1203            .expect("ring cache should exist");
1204        assert_eq!(cache.entries.len(), 2);
1205        assert!(cache.entries.iter().all(|entry| entry.key.ring_seed == 2));
1206        assert!(
1207            cache
1208                .entries
1209                .iter()
1210                .any(|entry| entry.key.structure_set == stronghold_key)
1211        );
1212        assert!(
1213            cache
1214                .entries
1215                .iter()
1216                .any(|entry| entry.key.structure_set == far_key)
1217        );
1218        assert!(world_dir.join("data").join("structure_rings.bin").exists());
1219    }
1220
1221    #[test]
1222    fn biome_source_cache_key_includes_seed() {
1223        let overworld_one = BiomeSourceKind::overworld(1);
1224        let overworld_two = BiomeSourceKind::overworld(2);
1225        let nether_one = BiomeSourceKind::nether(1);
1226        let nether_two = BiomeSourceKind::nether(2);
1227        let end_one = BiomeSourceKind::end(1);
1228        let end_two = BiomeSourceKind::end(2);
1229
1230        assert_ne!(
1231            overworld_one.ring_position_cache_key(),
1232            overworld_two.ring_position_cache_key()
1233        );
1234        assert_ne!(
1235            nether_one.ring_position_cache_key(),
1236            nether_two.ring_position_cache_key()
1237        );
1238        assert_ne!(
1239            end_one.ring_position_cache_key(),
1240            end_two.ring_position_cache_key()
1241        );
1242    }
1243
1244    #[test]
1245    #[should_panic(expected = "non-positive spacing")]
1246    fn constructor_rejects_invalid_random_spread_spacing() {
1247        let biome_provider = FixedStructureBiomeProvider::new(&vanilla_biomes::PLAINS);
1248        let sets = vec![(
1249            Identifier::new("test", "invalid"),
1250            StructureSet {
1251                structures: vec![StructureSelectionEntry {
1252                    structure: Identifier::new("test", "placeholder"),
1253                    weight: 1,
1254                }],
1255                placement: StructurePlacement {
1256                    salt: 0,
1257                    frequency: 1.0,
1258                    frequency_reduction_method: FrequencyReductionMethod::Default,
1259                    exclusion_zone: None,
1260                    locate_offset: IVec3::ZERO,
1261                    kind: PlacementKind::RandomSpread {
1262                        spacing: 0,
1263                        separation: 0,
1264                        spread_type: SpreadType::Linear,
1265                    },
1266                },
1267            },
1268        )];
1269        let thread_pool = rayon::ThreadPoolBuilder::default()
1270            .build()
1271            .expect("Couldn't create a new thread pool.");
1272
1273        let _ = StructureGenerator::with_assets(
1274            0,
1275            None,
1276            &biome_provider,
1277            sets,
1278            StructureGeneratorAssets::new(
1279                FxHashMap::default(),
1280                FxHashMap::default(),
1281                FxHashMap::default(),
1282            ),
1283            &thread_pool,
1284        );
1285    }
1286
1287    #[test]
1288    fn exclusion_zone_checks_other_set_interactions() {
1289        let a_key = Identifier::new("test", "a");
1290        let b_key = Identifier::new("test", "b");
1291        let c_key = Identifier::new("test", "c");
1292        let generator = generator_with_sets(vec![
1293            (
1294                a_key.clone(),
1295                test_structure_set(every_chunk_placement(Some(b_key.clone()))),
1296            ),
1297            (
1298                b_key.clone(),
1299                test_structure_set(every_chunk_placement(Some(c_key.clone()))),
1300            ),
1301            (c_key, test_structure_set(every_chunk_placement(None))),
1302        ]);
1303
1304        assert!(generator.is_structure_chunk_for_set(&a_key, 0, 0, &mut Vec::new()));
1305        assert!(!generator.is_structure_chunk_for_set(&b_key, 0, 0, &mut Vec::new()));
1306    }
1307
1308    #[test]
1309    #[should_panic(expected = "Circular structure exclusion zone")]
1310    fn circular_exclusion_zones_fail_loudly() {
1311        let a_key = Identifier::new("test", "a");
1312        let b_key = Identifier::new("test", "b");
1313        let generator = generator_with_sets(vec![
1314            (
1315                a_key.clone(),
1316                test_structure_set(every_chunk_placement(Some(b_key.clone()))),
1317            ),
1318            (
1319                b_key,
1320                test_structure_set(every_chunk_placement(Some(a_key.clone()))),
1321            ),
1322        ]);
1323
1324        let _ = generator.is_structure_chunk_for_set(&a_key, 0, 0, &mut Vec::new());
1325    }
1326
1327    #[test]
1328    fn random_spread_candidates_follow_vanilla_shell_order() {
1329        let plan = random_spread_plan(IVec3::ZERO);
1330        let origin = BlockPos::new(8, 64, 8);
1331        let candidates = plan.random_spread_candidates_at_radius(origin, 1);
1332
1333        let expected: Vec<ChunkPos> = (-1..=1)
1334            .flat_map(|x| {
1335                (-1..=1).filter_map(move |z| {
1336                    let is_edge = x == -1 || x == 1 || z == -1 || z == 1;
1337                    is_edge.then(|| {
1338                        StructurePlacement::get_potential_structure_chunk(
1339                            0,
1340                            10_387_312,
1341                            x * 32,
1342                            z * 32,
1343                            32,
1344                            8,
1345                            SpreadType::Linear,
1346                        )
1347                    })
1348                })
1349            })
1350            .collect();
1351
1352        let actual: Vec<ChunkPos> = candidates
1353            .iter()
1354            .map(|candidate| candidate.chunk_pos)
1355            .collect();
1356        assert_eq!(actual, expected);
1357    }
1358
1359    #[test]
1360    fn random_spread_candidates_use_locate_offset() {
1361        let plan = random_spread_plan(IVec3::new(9, 0, 9));
1362        let origin = BlockPos::new(0, 64, 0);
1363        let candidate = plan.random_spread_candidates_at_radius(origin, 0)[0];
1364
1365        assert_eq!(
1366            candidate.locate_pos,
1367            BlockPos::new(
1368                candidate.chunk_pos.0.x * 16 + 9,
1369                0,
1370                candidate.chunk_pos.0.y * 16 + 9
1371            )
1372        );
1373    }
1374
1375    #[test]
1376    fn locate_candidates_retain_only_structures_for_their_placement_scan() {
1377        let first_structure = Identifier::new("test", "first");
1378        let second_structure = Identifier::new("test", "second");
1379        let third_structure = Identifier::new("test", "third");
1380        let mut first_set = test_structure_set(every_chunk_placement(None));
1381        first_set.structures[0].structure = first_structure.clone();
1382        first_set.structures.push(StructureSelectionEntry {
1383            structure: third_structure.clone(),
1384            weight: 1,
1385        });
1386        let mut second_placement = every_chunk_placement(None);
1387        second_placement.salt = 1;
1388        let mut second_set = test_structure_set(second_placement);
1389        second_set.structures[0].structure = second_structure.clone();
1390        let generator = generator_with_sets(vec![
1391            (Identifier::new("test", "first_set"), first_set),
1392            (Identifier::new("test", "second_set"), second_set),
1393        ]);
1394        let Some(plan) = generator.locate_plan_for_structures(&[
1395            third_structure.clone(),
1396            first_structure.clone(),
1397            second_structure.clone(),
1398        ]) else {
1399            panic!("requested structures should produce a locate plan");
1400        };
1401
1402        let candidates = plan.random_spread_candidates_at_radius(BlockPos::new(0, 64, 0), 0);
1403        assert_eq!(candidates.len(), 2);
1404        assert_eq!(
1405            plan.structures_for_candidate(candidates[0]),
1406            Some([third_structure, first_structure].as_slice())
1407        );
1408        assert_eq!(
1409            plan.structures_for_candidate(candidates[1]),
1410            Some([second_structure].as_slice())
1411        );
1412    }
1413
1414    #[test]
1415    fn ring_candidates_are_ordered_by_vanilla_distance_probe() {
1416        let plan = StructureLocatePlan {
1417            seed: 0,
1418            placements: vec![StructureLocatePlacement {
1419                placement: StructurePlacement {
1420                    salt: 0,
1421                    frequency: 1.0,
1422                    frequency_reduction_method: FrequencyReductionMethod::Default,
1423                    exclusion_zone: None,
1424                    locate_offset: IVec3::ZERO,
1425                    kind: PlacementKind::ConcentricRings {
1426                        distance: 32,
1427                        spread: 3,
1428                        count: 2,
1429                        preferred_biomes: Vec::new(),
1430                    },
1431                },
1432                ring_positions: Some(vec![ChunkPos::new(10, 0), ChunkPos::new(1, 0)]),
1433                structures: vec![Identifier::new("test", "placeholder")],
1434            }],
1435        };
1436
1437        let candidates = plan.ring_candidates(BlockPos::new(0, 64, 0));
1438        assert_eq!(candidates[0].chunk_pos, ChunkPos::new(1, 0));
1439        assert_eq!(candidates[1].chunk_pos, ChunkPos::new(10, 0));
1440    }
1441}