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