1use 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
82pub trait StructureBiomeProvider {
84 fn possible_biomes(&self) -> FxHashSet<Identifier>;
86
87 fn ring_position_cache_key(&self) -> Option<String> {
92 None
93 }
94
95 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
143pub struct FixedStructureBiomeProvider {
145 biome: BiomeRef,
146}
147
148impl FixedStructureBiomeProvider {
149 #[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
194pub 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
207pub 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 #[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 #[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#[derive(Debug, Clone)]
254pub struct StructureLocatePlan {
255 seed: i64,
256 placements: Vec<StructureLocatePlacement>,
257}
258
259#[derive(Debug, Clone)]
261pub struct StructureLocatePlacement {
262 placement: StructurePlacement,
263 ring_positions: Option<Vec<ChunkPos>>,
264 structures: Vec<Identifier>,
265}
266
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
269pub struct StructureLocateCandidate {
270 pub chunk_pos: ChunkPos,
272 pub locate_pos: BlockPos,
274 scan_id: usize,
275 ring_distance_pos: BlockPos,
276}
277
278impl StructureLocatePlan {
279 #[must_use]
281 pub const fn is_empty(&self) -> bool {
282 self.placements.is_empty()
283 }
284
285 #[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 #[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 #[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 #[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 #[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#[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 #[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 #[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 #[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 #[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 #[must_use]
802 pub const fn template_pools(&self) -> &FxHashMap<Identifier, TemplatePoolData> {
803 &self.template_pools
804 }
805
806 #[must_use]
808 pub const fn templates(&self) -> &FxHashMap<Identifier, TemplateData> {
809 &self.templates
810 }
811
812 #[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 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 use std::panic;
1142 use std::thread;
1143
1144 let result = thread::Builder::new()
1148 .stack_size(16 * 1024 * 1024)
1149 .spawn(vanilla_assets_cover_vanilla_structure_sets_inner)
1150 .expect("Failed to spawn test thread")
1151 .join();
1152
1153 if let Err(payload) = result {
1154 panic::resume_unwind(payload);
1155 }
1156 }
1157
1158 fn vanilla_assets_cover_vanilla_structure_sets_inner() {
1159 init_vanilla_registry();
1160 let biome_provider = FixedStructureBiomeProvider::new(&vanilla_biomes::PLAINS);
1161 let thread_pool = rayon::ThreadPoolBuilder::default()
1162 .build()
1163 .expect("Couldn't create a new thread pool.");
1164 let _ = StructureGenerator::vanilla_with_structure_sets(
1165 0,
1166 None,
1167 &biome_provider,
1168 load_vanilla_structure_sets(),
1169 &thread_pool,
1170 );
1171 }
1172
1173 #[test]
1174 fn ring_cache_keys_entries_by_placement_inputs() {
1175 use std::panic;
1176 use std::thread;
1177
1178 let result = thread::Builder::new()
1182 .stack_size(16 * 1024 * 1024)
1183 .spawn(ring_cache_keys_entries_by_placement_inputs_inner)
1184 .expect("Failed to spawn test thread")
1185 .join();
1186
1187 if let Err(payload) = result {
1188 panic::resume_unwind(payload);
1189 }
1190 }
1191
1192 fn ring_cache_keys_entries_by_placement_inputs_inner() {
1193 init_vanilla_registry();
1194 let world_dir = temp_world_dir("placement-inputs");
1195 let biome_provider = FixedStructureBiomeProvider::new(&vanilla_biomes::PLAINS);
1196 let thread_pool = rayon::ThreadPoolBuilder::new()
1197 .num_threads(2)
1198 .build()
1199 .expect("ring cache test thread pool should build");
1200 let (stronghold_key, stronghold_set) = vanilla_stronghold_set();
1201 let far_key = Identifier::new("test", "far_strongholds");
1202 let mut far_set = stronghold_set.clone();
1203 let PlacementKind::ConcentricRings { distance, .. } = &mut far_set.placement.kind else {
1204 panic!("vanilla stronghold set should use concentric rings");
1205 };
1206 *distance *= 2;
1207 let sets = vec![
1208 (stronghold_key.clone(), stronghold_set),
1209 (far_key.clone(), far_set),
1210 ];
1211
1212 let first = StructureGenerator::vanilla_with_structure_sets(
1213 1,
1214 Some(&world_dir),
1215 &biome_provider,
1216 sets.clone(),
1217 &thread_pool,
1218 );
1219 assert_ne!(
1220 first.ring_positions[&stronghold_key],
1221 first.ring_positions[&far_key]
1222 );
1223
1224 let second = StructureGenerator::vanilla_with_structure_sets(
1225 2,
1226 Some(&world_dir),
1227 &biome_provider,
1228 sets,
1229 &thread_pool,
1230 );
1231 assert_ne!(
1232 first.ring_positions[&stronghold_key],
1233 second.ring_positions[&stronghold_key]
1234 );
1235
1236 let cache: RingPositionCache = SavedDataManager::new(Some(&world_dir))
1237 .sync_load_wincode(saved_data_names::STRUCTURE_RINGS)
1238 .expect("ring cache should load")
1239 .expect("ring cache should exist");
1240 assert_eq!(cache.entries.len(), 2);
1241 assert!(cache.entries.iter().all(|entry| entry.key.ring_seed == 2));
1242 assert!(
1243 cache
1244 .entries
1245 .iter()
1246 .any(|entry| entry.key.structure_set == stronghold_key)
1247 );
1248 assert!(
1249 cache
1250 .entries
1251 .iter()
1252 .any(|entry| entry.key.structure_set == far_key)
1253 );
1254 assert!(world_dir.join("data").join("structure_rings.bin").exists());
1255 }
1256
1257 #[test]
1258 fn biome_source_cache_key_includes_seed() {
1259 let overworld_one = BiomeSourceKind::overworld(1);
1260 let overworld_two = BiomeSourceKind::overworld(2);
1261 let nether_one = BiomeSourceKind::nether(1);
1262 let nether_two = BiomeSourceKind::nether(2);
1263 let end_one = BiomeSourceKind::end(1);
1264 let end_two = BiomeSourceKind::end(2);
1265
1266 assert_ne!(
1267 overworld_one.ring_position_cache_key(),
1268 overworld_two.ring_position_cache_key()
1269 );
1270 assert_ne!(
1271 nether_one.ring_position_cache_key(),
1272 nether_two.ring_position_cache_key()
1273 );
1274 assert_ne!(
1275 end_one.ring_position_cache_key(),
1276 end_two.ring_position_cache_key()
1277 );
1278 }
1279
1280 #[test]
1281 #[should_panic(expected = "non-positive spacing")]
1282 fn constructor_rejects_invalid_random_spread_spacing() {
1283 let biome_provider = FixedStructureBiomeProvider::new(&vanilla_biomes::PLAINS);
1284 let sets = vec![(
1285 Identifier::new("test", "invalid"),
1286 StructureSet {
1287 structures: vec![StructureSelectionEntry {
1288 structure: Identifier::new("test", "placeholder"),
1289 weight: 1,
1290 }],
1291 placement: StructurePlacement {
1292 salt: 0,
1293 frequency: 1.0,
1294 frequency_reduction_method: FrequencyReductionMethod::Default,
1295 exclusion_zone: None,
1296 locate_offset: IVec3::ZERO,
1297 kind: PlacementKind::RandomSpread {
1298 spacing: 0,
1299 separation: 0,
1300 spread_type: SpreadType::Linear,
1301 },
1302 },
1303 },
1304 )];
1305 let thread_pool = rayon::ThreadPoolBuilder::default()
1306 .build()
1307 .expect("Couldn't create a new thread pool.");
1308
1309 let _ = StructureGenerator::with_assets(
1310 0,
1311 None,
1312 &biome_provider,
1313 sets,
1314 StructureGeneratorAssets::new(
1315 FxHashMap::default(),
1316 FxHashMap::default(),
1317 FxHashMap::default(),
1318 ),
1319 &thread_pool,
1320 );
1321 }
1322
1323 #[test]
1324 fn exclusion_zone_checks_other_set_interactions() {
1325 let a_key = Identifier::new("test", "a");
1326 let b_key = Identifier::new("test", "b");
1327 let c_key = Identifier::new("test", "c");
1328 let generator = generator_with_sets(vec![
1329 (
1330 a_key.clone(),
1331 test_structure_set(every_chunk_placement(Some(b_key.clone()))),
1332 ),
1333 (
1334 b_key.clone(),
1335 test_structure_set(every_chunk_placement(Some(c_key.clone()))),
1336 ),
1337 (c_key, test_structure_set(every_chunk_placement(None))),
1338 ]);
1339
1340 assert!(generator.is_structure_chunk_for_set(&a_key, 0, 0, &mut Vec::new()));
1341 assert!(!generator.is_structure_chunk_for_set(&b_key, 0, 0, &mut Vec::new()));
1342 }
1343
1344 #[test]
1345 #[should_panic(expected = "Circular structure exclusion zone")]
1346 fn circular_exclusion_zones_fail_loudly() {
1347 let a_key = Identifier::new("test", "a");
1348 let b_key = Identifier::new("test", "b");
1349 let generator = generator_with_sets(vec![
1350 (
1351 a_key.clone(),
1352 test_structure_set(every_chunk_placement(Some(b_key.clone()))),
1353 ),
1354 (
1355 b_key,
1356 test_structure_set(every_chunk_placement(Some(a_key.clone()))),
1357 ),
1358 ]);
1359
1360 let _ = generator.is_structure_chunk_for_set(&a_key, 0, 0, &mut Vec::new());
1361 }
1362
1363 #[test]
1364 fn random_spread_candidates_follow_vanilla_shell_order() {
1365 let plan = random_spread_plan(IVec3::ZERO);
1366 let origin = BlockPos::new(8, 64, 8);
1367 let candidates = plan.random_spread_candidates_at_radius(origin, 1);
1368
1369 let expected: Vec<ChunkPos> = (-1..=1)
1370 .flat_map(|x| {
1371 (-1..=1).filter_map(move |z| {
1372 let is_edge = x == -1 || x == 1 || z == -1 || z == 1;
1373 is_edge.then(|| {
1374 StructurePlacement::get_potential_structure_chunk(
1375 0,
1376 10_387_312,
1377 x * 32,
1378 z * 32,
1379 32,
1380 8,
1381 SpreadType::Linear,
1382 )
1383 })
1384 })
1385 })
1386 .collect();
1387
1388 let actual: Vec<ChunkPos> = candidates
1389 .iter()
1390 .map(|candidate| candidate.chunk_pos)
1391 .collect();
1392 assert_eq!(actual, expected);
1393 }
1394
1395 #[test]
1396 fn random_spread_candidates_use_locate_offset() {
1397 let plan = random_spread_plan(IVec3::new(9, 0, 9));
1398 let origin = BlockPos::new(0, 64, 0);
1399 let candidate = plan.random_spread_candidates_at_radius(origin, 0)[0];
1400
1401 assert_eq!(
1402 candidate.locate_pos,
1403 BlockPos::new(
1404 candidate.chunk_pos.0.x * 16 + 9,
1405 0,
1406 candidate.chunk_pos.0.y * 16 + 9
1407 )
1408 );
1409 }
1410
1411 #[test]
1412 fn locate_candidates_retain_only_structures_for_their_placement_scan() {
1413 let first_structure = Identifier::new("test", "first");
1414 let second_structure = Identifier::new("test", "second");
1415 let third_structure = Identifier::new("test", "third");
1416 let mut first_set = test_structure_set(every_chunk_placement(None));
1417 first_set.structures[0].structure = first_structure.clone();
1418 first_set.structures.push(StructureSelectionEntry {
1419 structure: third_structure.clone(),
1420 weight: 1,
1421 });
1422 let mut second_placement = every_chunk_placement(None);
1423 second_placement.salt = 1;
1424 let mut second_set = test_structure_set(second_placement);
1425 second_set.structures[0].structure = second_structure.clone();
1426 let generator = generator_with_sets(vec![
1427 (Identifier::new("test", "first_set"), first_set),
1428 (Identifier::new("test", "second_set"), second_set),
1429 ]);
1430 let Some(plan) = generator.locate_plan_for_structures(&[
1431 third_structure.clone(),
1432 first_structure.clone(),
1433 second_structure.clone(),
1434 ]) else {
1435 panic!("requested structures should produce a locate plan");
1436 };
1437
1438 let candidates = plan.random_spread_candidates_at_radius(BlockPos::new(0, 64, 0), 0);
1439 assert_eq!(candidates.len(), 2);
1440 assert_eq!(
1441 plan.structures_for_candidate(candidates[0]),
1442 Some([third_structure, first_structure].as_slice())
1443 );
1444 assert_eq!(
1445 plan.structures_for_candidate(candidates[1]),
1446 Some([second_structure].as_slice())
1447 );
1448 }
1449
1450 #[test]
1451 fn ring_candidates_are_ordered_by_vanilla_distance_probe() {
1452 let plan = StructureLocatePlan {
1453 seed: 0,
1454 placements: vec![StructureLocatePlacement {
1455 placement: StructurePlacement {
1456 salt: 0,
1457 frequency: 1.0,
1458 frequency_reduction_method: FrequencyReductionMethod::Default,
1459 exclusion_zone: None,
1460 locate_offset: IVec3::ZERO,
1461 kind: PlacementKind::ConcentricRings {
1462 distance: 32,
1463 spread: 3,
1464 count: 2,
1465 preferred_biomes: Vec::new(),
1466 },
1467 },
1468 ring_positions: Some(vec![ChunkPos::new(10, 0), ChunkPos::new(1, 0)]),
1469 structures: vec![Identifier::new("test", "placeholder")],
1470 }],
1471 };
1472
1473 let candidates = plan.ring_candidates(BlockPos::new(0, 64, 0));
1474 assert_eq!(candidates[0].chunk_pos, ChunkPos::new(1, 0));
1475 assert_eq!(candidates[1].chunk_pos, ChunkPos::new(10, 0));
1476 }
1477}