Skip to main content

steel_worldgen/structure/
jigsaw.rs

1//! Jigsaw assembly. Ports vanilla's `JigsawPlacement` BFS: connects pieces via
2//! jigsaw blocks given a start pool + config. Produces typed piece state;
3//! block placement runs in a later worldgen stage.
4
5use std::cmp::{Ordering, Reverse};
6use std::collections::BinaryHeap;
7use std::{array, mem, ptr};
8
9use glam::IVec3;
10use rustc_hash::{FxHashMap, FxHashSet};
11use steel_registry::structure::{
12    JigsawConfig, LiquidSettingsData, PoolAlias, StartHeight, StructureData,
13};
14use steel_registry::template_pool::{
15    JigsawOrientation, JointType, PoolElement, Projection, TemplateData, TemplatePoolData,
16};
17use steel_utils::random::legacy_random::LegacyRandom;
18use steel_utils::random::{PositionalRandom, Random};
19use steel_utils::{BoundingBox, Identifier, Rotation};
20
21use crate::structure::box_octree::BoxOctree;
22use crate::structure::{
23    GenerationStub, Structure, StructureGenerationContext, StructurePiece, StructurePiecePayload,
24};
25
26/// A placed piece produced by jigsaw assembly.
27#[derive(Debug, Clone)]
28pub struct PlacedPiece {
29    /// Selected pool element.
30    pub element: PoolElement,
31    /// Template location (Single/LegacySingle).
32    pub template_location: Option<Identifier>,
33    /// World-space origin.
34    pub position: IVec3,
35    /// Rotation.
36    pub rotation: Rotation,
37    /// Template-sized BB (used for beardifier + world save).
38    pub bounding_box: BoundingBox,
39    /// Assembly-time BB, possibly expanded vertically by the expansion hack.
40    /// Used only during assembly — not persisted.
41    pub assembly_bb: BoundingBox,
42    /// Ground-level delta for Beardifier.
43    pub ground_level_delta: i32,
44    /// Rigid or terrain-matching.
45    pub projection: Projection,
46    /// BFS tree depth.
47    pub depth: i32,
48    /// Junctions to neighbors.
49    pub junctions: Vec<JigsawJunction>,
50}
51
52/// Typed state needed to place or compare a vanilla jigsaw piece.
53#[derive(Debug, Clone)]
54pub struct JigsawPieceData {
55    /// Selected pool element.
56    pub pool_element: PoolElement,
57    /// World-space template origin.
58    pub position: IVec3,
59    /// Template rotation.
60    pub rotation: Rotation,
61    /// Liquid handling mode for block placement.
62    pub liquid_settings: LiquidSettingsData,
63}
64
65/// Junction between two jigsaw pieces (terrain adaptation).
66#[derive(Debug, Clone)]
67pub struct JigsawJunction {
68    /// World-space source position.
69    pub source_pos: IVec3,
70    /// Y delta between source and target.
71    pub delta_y: i32,
72    /// Destination projection.
73    pub dest_projection: Projection,
74}
75
76/// Resolves pool aliases for a specific structure instance.
77pub fn resolve_aliases(
78    aliases: &[PoolAlias],
79    rng: &mut impl Random,
80) -> FxHashMap<Identifier, Identifier> {
81    let mut map = FxHashMap::default();
82    for alias in aliases {
83        match alias {
84            PoolAlias::Direct { alias, target } => {
85                map.insert(alias.clone(), target.clone());
86            }
87            PoolAlias::Random { alias, targets } => {
88                let total: i32 = targets.iter().map(|(_, w)| *w).sum();
89                if total > 0 {
90                    let mut pick = rng.next_i32_bounded(total);
91                    for (target, weight) in targets {
92                        pick -= weight;
93                        if pick < 0 {
94                            map.insert(alias.clone(), target.clone());
95                            break;
96                        }
97                    }
98                }
99            }
100            PoolAlias::RandomGroup { groups } => {
101                let total: i32 = groups.iter().map(|(_, w)| *w).sum();
102                if total > 0 {
103                    let mut pick = rng.next_i32_bounded(total);
104                    for (bindings, weight) in groups {
105                        pick -= weight;
106                        if pick < 0 {
107                            for (alias, target) in bindings {
108                                map.insert(alias.clone(), target.clone());
109                            }
110                            break;
111                        }
112                    }
113                }
114            }
115        }
116    }
117    map
118}
119
120fn sample_start_height(config: &JigsawConfig, rng: &mut impl Random) -> i32 {
121    match &config.start_height {
122        StartHeight::Constant(y) => *y,
123        StartHeight::Uniform { min, max } => rng.next_i32_between(*min, *max),
124    }
125}
126
127/// Java integer midpoint used by vanilla jigsaw placement: `(min + max) / 2`.
128const fn java_center(min: i32, max: i32) -> i32 {
129    min.wrapping_add(max) / 2
130}
131
132static SYNTHETIC_BOTTOM_JIGSAW: Identifier = Identifier::new_static("minecraft", "bottom");
133static SYNTHETIC_EMPTY_POOL: Identifier = Identifier::new_static("minecraft", "empty");
134
135type PoolTemplateCache<'a> = FxHashMap<Identifier, Vec<&'a PoolElement>>;
136type JigsawRotationCache<'a> = FxHashMap<Identifier, [Option<Vec<TransformedJigsaw<'a>>>; 4]>;
137const CANDIDATE_DEDUPE_THRESHOLD: usize = 16;
138const JIGSAW_PRIORITY_CACHE_THRESHOLD: usize = 16;
139const QUEUE_HEAP_THRESHOLD: usize = 512;
140const FREE_SPACE_OCTREE_THRESHOLD: usize = 512;
141
142struct AssemblyScratch<'a> {
143    parsed_candidates: FxHashSet<*const PoolElement>,
144    source_jigsaw_indices: Vec<usize>,
145    candidate_jigsaw_indices: Vec<usize>,
146    jigsaw_order_scratch: Vec<usize>,
147    jigsaw_priority_scratch: Vec<i32>,
148    pool_max_y_cache: FxHashMap<Identifier, i32>,
149    jigsaw_rotation_cache: JigsawRotationCache<'a>,
150    jigsaw_priority_cache: FxHashMap<Identifier, Vec<i32>>,
151    queue_order: u64,
152}
153
154impl AssemblyScratch<'_> {
155    fn new() -> Self {
156        Self {
157            parsed_candidates: FxHashSet::default(),
158            source_jigsaw_indices: Vec::new(),
159            candidate_jigsaw_indices: Vec::new(),
160            jigsaw_order_scratch: Vec::new(),
161            jigsaw_priority_scratch: Vec::new(),
162            pool_max_y_cache: FxHashMap::default(),
163            jigsaw_rotation_cache: JigsawRotationCache::default(),
164            jigsaw_priority_cache: FxHashMap::default(),
165            queue_order: 0,
166        }
167    }
168}
169
170/// BFS queue entry ordered by descending `placement_priority`, FIFO within ties.
171#[derive(Eq, PartialEq)]
172struct PieceQueueEntry {
173    priority: i32,
174    order: u64,
175    piece_idx: usize,
176    depth: i32,
177    context_idx: usize,
178}
179
180impl Ord for PieceQueueEntry {
181    fn cmp(&self, other: &Self) -> Ordering {
182        self.priority
183            .cmp(&other.priority)
184            .then_with(|| other.order.cmp(&self.order))
185    }
186}
187
188impl PartialOrd for PieceQueueEntry {
189    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
190        Some(self.cmp(other))
191    }
192}
193
194enum PieceQueue {
195    Small(Vec<PieceQueueEntry>),
196    Large(BinaryHeap<PieceQueueEntry>),
197}
198
199impl PieceQueue {
200    const fn new() -> Self {
201        Self::Small(Vec::new())
202    }
203
204    fn push(&mut self, entry: PieceQueueEntry) {
205        match self {
206            Self::Small(entries) if entries.len() < QUEUE_HEAP_THRESHOLD => {
207                entries.push(entry);
208            }
209            Self::Small(entries) => {
210                let mut heap = BinaryHeap::from(mem::take(entries));
211                heap.push(entry);
212                *self = Self::Large(heap);
213            }
214            Self::Large(heap) => {
215                heap.push(entry);
216            }
217        }
218    }
219
220    fn pop(&mut self) -> Option<PieceQueueEntry> {
221        match self {
222            Self::Small(entries) => {
223                let best_idx = entries
224                    .iter()
225                    .enumerate()
226                    .max_by(|(_, a), (_, b)| a.cmp(b))
227                    .map(|(idx, _)| idx)?;
228                Some(entries.swap_remove(best_idx))
229            }
230            Self::Large(heap) => heap.pop(),
231        }
232    }
233}
234
235fn cached_pool_max_y_size(
236    pool_key: &Identifier,
237    pools: &FxHashMap<Identifier, TemplatePoolData>,
238    templates: &FxHashMap<Identifier, TemplateData>,
239    cache: &mut FxHashMap<Identifier, i32>,
240) -> i32 {
241    if let Some(size) = cache.get(pool_key) {
242        return *size;
243    }
244    let size = pools
245        .get(pool_key)
246        .map_or(0, |pool| pool_max_y_size(pool, templates));
247    cache.insert(pool_key.clone(), size);
248    size
249}
250
251const fn rotation_index(rotation: Rotation) -> usize {
252    match rotation {
253        Rotation::None => 0,
254        Rotation::Clockwise90 => 1,
255        Rotation::Clockwise180 => 2,
256        Rotation::CounterClockwise90 => 3,
257    }
258}
259
260/// Vanilla-matching shuffle (reverse Fisher-Yates).
261fn vanilla_shuffle<T>(list: &mut [T], rng: &mut LegacyRandom) {
262    for i in (1..list.len()).rev() {
263        let j = rng.next_i32_bounded((i + 1) as i32) as usize;
264        list.swap(i, j);
265    }
266}
267
268fn descending_priorities_into(template: &TemplateData, unique: &mut Vec<i32>) {
269    unique.clear();
270    for jigsaw in &template.jigsaws {
271        if !unique.contains(&jigsaw.selection_priority) {
272            unique.push(jigsaw.selection_priority);
273        }
274    }
275    if unique.len() > 1 {
276        unique.sort_unstable_by_key(|priority| Reverse(*priority));
277    }
278}
279
280fn descending_priorities(template: &TemplateData) -> Vec<i32> {
281    let mut unique = Vec::new();
282    descending_priorities_into(template, &mut unique);
283    unique
284}
285
286fn cached_descending_priorities<'cache>(
287    location: &Identifier,
288    template: &TemplateData,
289    cache: &'cache mut FxHashMap<Identifier, Vec<i32>>,
290) -> &'cache [i32] {
291    cache
292        .entry(location.clone())
293        .or_insert_with(|| descending_priorities(template))
294}
295
296fn cached_runtime_rotated_jigsaws<'cache, 'a>(
297    location: &Identifier,
298    template: &'a TemplateData,
299    rotation: Rotation,
300    cache: &'cache mut JigsawRotationCache<'a>,
301) -> &'cache [TransformedJigsaw<'a>] {
302    let idx = rotation_index(rotation);
303    let by_rotation = cache
304        .entry(location.clone())
305        .or_insert_with(|| array::from_fn(|_| None));
306    by_rotation[idx].get_or_insert_with(|| transform_template_jigsaws(template, rotation))
307}
308
309fn transform_template_jigsaws(
310    template: &TemplateData,
311    rotation: Rotation,
312) -> Vec<TransformedJigsaw<'_>> {
313    template
314        .jigsaws
315        .iter()
316        .map(|jigsaw| {
317            let pos = rotation.transform_pos(IVec3::from(jigsaw.pos), IVec3::ZERO);
318            TransformedJigsaw {
319                pos,
320                orientation: jigsaw.orientation.rotate(rotation),
321                name: &jigsaw.name,
322                target: &jigsaw.target,
323                pool: &jigsaw.pool,
324                joint: jigsaw.joint,
325                placement_priority: jigsaw.placement_priority,
326            }
327        })
328        .collect()
329}
330
331fn shuffle_jigsaw_indices_into(
332    template: &TemplateData,
333    priorities: &[i32],
334    rng: &mut LegacyRandom,
335    out: &mut Vec<usize>,
336    order_scratch: &mut Vec<usize>,
337) {
338    out.clear();
339    if template.jigsaws.is_empty() {
340        return;
341    }
342    out.extend(0..template.jigsaws.len());
343    vanilla_shuffle(out, rng);
344    order_jigsaw_indices_by_priorities(template, priorities, out, order_scratch);
345}
346
347fn order_jigsaw_indices_by_priorities(
348    template: &TemplateData,
349    priorities: &[i32],
350    out: &mut Vec<usize>,
351    scratch: &mut Vec<usize>,
352) {
353    if priorities.len() <= 1 {
354        return;
355    }
356    scratch.clear();
357    scratch.extend_from_slice(out);
358    out.clear();
359    for &priority in priorities {
360        out.extend(
361            scratch
362                .iter()
363                .copied()
364                .filter(|&idx| template.jigsaws[idx].selection_priority == priority),
365        );
366    }
367}
368
369fn shuffle_jigsaw_indices_with_priority_cache(
370    location: &Identifier,
371    template: &TemplateData,
372    rng: &mut LegacyRandom,
373    out: &mut Vec<usize>,
374    order_scratch: &mut Vec<usize>,
375    priority_scratch: &mut Vec<i32>,
376    priority_cache: &mut FxHashMap<Identifier, Vec<i32>>,
377) {
378    if template.jigsaws.len() > JIGSAW_PRIORITY_CACHE_THRESHOLD {
379        let priorities = cached_descending_priorities(location, template, priority_cache);
380        shuffle_jigsaw_indices_into(template, priorities, rng, out, order_scratch);
381    } else {
382        descending_priorities_into(template, priority_scratch);
383        shuffle_jigsaw_indices_into(template, priority_scratch, rng, out, order_scratch);
384    }
385}
386
387/// Consumes the same RNG draws as a failed placement attempt for a duplicate pool element.
388///
389/// Vanilla keeps weighted duplicates, but each attempt exhausts every rotation
390/// and target jigsaw before moving on. With unchanged free space, a later
391/// identical duplicate cannot succeed if the first one failed; only the RNG
392/// draws need to be preserved.
393fn prime_duplicate_candidate_rng(
394    element: &PoolElement,
395    templates: &FxHashMap<Identifier, TemplateData>,
396    rotations: [Rotation; 4],
397    rng: &mut LegacyRandom,
398    scratch: &mut AssemblyScratch<'_>,
399) {
400    for _rotation in rotations {
401        if let Some(location) = element_location(element)
402            && let Some(template) = templates.get(location)
403        {
404            shuffle_jigsaw_indices_with_priority_cache(
405                location,
406                template,
407                rng,
408                &mut scratch.candidate_jigsaw_indices,
409                &mut scratch.jigsaw_order_scratch,
410                &mut scratch.jigsaw_priority_scratch,
411                &mut scratch.jigsaw_priority_cache,
412            );
413        }
414        // Feature/Empty elements do not shuffle jigsaws; no RNG to prime here.
415    }
416}
417
418fn feature_synthetic_jigsaw() -> TransformedJigsaw<'static> {
419    TransformedJigsaw {
420        pos: IVec3::ZERO,
421        orientation: JigsawOrientation::DownSouth,
422        name: &SYNTHETIC_BOTTOM_JIGSAW,
423        target: &SYNTHETIC_EMPTY_POOL,
424        pool: &SYNTHETIC_EMPTY_POOL,
425        joint: JointType::Rollable,
426        placement_priority: 0,
427    }
428}
429
430fn shuffled_element_jigsaws<'a>(
431    element: &PoolElement,
432    templates: &'a FxHashMap<Identifier, TemplateData>,
433    rotation: Rotation,
434    rng: &mut LegacyRandom,
435) -> Vec<TransformedJigsaw<'a>> {
436    match element {
437        PoolElement::Single { location, .. } | PoolElement::LegacySingle { location, .. } => {
438            let Some(template) = templates.get(location) else {
439                return Vec::new();
440            };
441
442            let rotated = transform_template_jigsaws(template, rotation);
443            let mut priorities = Vec::new();
444            descending_priorities_into(template, &mut priorities);
445            let mut shuffle_indices = Vec::new();
446            let mut order_scratch = Vec::new();
447            shuffle_jigsaw_indices_into(
448                template,
449                &priorities,
450                rng,
451                &mut shuffle_indices,
452                &mut order_scratch,
453            );
454            shuffle_indices
455                .into_iter()
456                .map(|idx| rotated[idx])
457                .collect()
458        }
459        PoolElement::Feature { .. } => vec![feature_synthetic_jigsaw()],
460        PoolElement::List { elements, .. } => elements.first().map_or_else(Vec::new, |element| {
461            shuffled_element_jigsaws(element, templates, rotation, rng)
462        }),
463        PoolElement::Empty => Vec::new(),
464    }
465}
466
467/// Active source connector during jigsaw BFS.
468struct ActiveSourceJigsaw<'a> {
469    block: TransformedJigsaw<'a>,
470    pos: IVec3,
471}
472
473impl ActiveSourceJigsaw<'_> {
474    fn can_attach_to(&self, target: &TransformedJigsaw<'_>) -> bool {
475        if self.block.orientation.front_direction()
476            != target.orientation.front_direction().opposite()
477        {
478            return false;
479        }
480        if self.block.joint == JointType::Aligned
481            && self.block.orientation.top_direction() != target.orientation.top_direction()
482        {
483            return false;
484        }
485        self.block.target == target.name
486    }
487}
488
489/// A jigsaw block with its position transformed by rotation.
490#[derive(Clone, Copy)]
491struct TransformedJigsaw<'a> {
492    pos: IVec3,
493    orientation: JigsawOrientation,
494    name: &'a Identifier,
495    target: &'a Identifier,
496    pool: &'a Identifier,
497    joint: JointType,
498    placement_priority: i32,
499}
500
501/// Gets the template location from a pool element.
502///
503/// For `List` elements, delegates to the first sub-element matching vanilla's
504/// `ListPoolElement` which uses `elements.get(0)` for jigsaws and BB.
505fn element_location(element: &PoolElement) -> Option<&Identifier> {
506    match element {
507        PoolElement::Single { location, .. } | PoolElement::LegacySingle { location, .. } => {
508            Some(location)
509        }
510        PoolElement::List { elements, .. } => elements.first().and_then(element_location),
511        _ => None,
512    }
513}
514
515/// Vanilla's `StructureTemplatePool.getMaxSize` — max Y span across all templates.
516fn pool_max_y_size(
517    pool: &TemplatePoolData,
518    templates: &FxHashMap<Identifier, TemplateData>,
519) -> i32 {
520    pool.elements
521        .iter()
522        .filter_map(|(element, _)| {
523            let (PoolElement::Single { location: loc, .. }
524            | PoolElement::LegacySingle { location: loc, .. }) = element
525            else {
526                return None;
527            };
528            templates.get(loc).map(|t| t.size[1])
529        })
530        .max()
531        .unwrap_or(0)
532}
533
534/// Gets the bounding box for a pool element at a position with rotation.
535///
536/// Feature elements return a 1×1×1 BB at the given position, matching
537/// vanilla's `FeaturePoolElement.getBoundingBox`.
538/// List elements return the encapsulating BB of all sub-elements, matching
539/// vanilla's `ListPoolElement.getBoundingBox`.
540fn element_bounding_box(
541    element: &PoolElement,
542    templates: &FxHashMap<Identifier, TemplateData>,
543    pos: IVec3,
544    rotation: Rotation,
545) -> Option<BoundingBox> {
546    match element {
547        PoolElement::Feature { .. } => Some(BoundingBox::new(pos, pos)),
548        PoolElement::List { elements, .. } => {
549            let mut result: Option<BoundingBox> = None;
550            for sub in elements {
551                if let Some(sub_bb) = element_bounding_box(sub, templates, pos, rotation) {
552                    result = Some(match result {
553                        Some(prev) => BoundingBox::new(
554                            IVec3::new(
555                                prev.min_x().min(sub_bb.min_x()),
556                                prev.min_y().min(sub_bb.min_y()),
557                                prev.min_z().min(sub_bb.min_z()),
558                            ),
559                            IVec3::new(
560                                prev.max_x().max(sub_bb.max_x()),
561                                prev.max_y().max(sub_bb.max_y()),
562                                prev.max_z().max(sub_bb.max_z()),
563                            ),
564                        ),
565                        None => sub_bb,
566                    });
567                }
568            }
569            result
570        }
571        _ => {
572            let location = element_location(element)?;
573            let template = templates.get(location)?;
574            let size = IVec3::from(template.size);
575            Some(rotation.get_bounding_box(pos, size))
576        }
577    }
578}
579
580fn candidate_bounding_box_at_origin(
581    element: &PoolElement,
582    templates: &FxHashMap<Identifier, TemplateData>,
583    template: Option<&TemplateData>,
584    rotation: Rotation,
585) -> Option<BoundingBox> {
586    match element {
587        PoolElement::Single { .. } | PoolElement::LegacySingle { .. } => {
588            let size = IVec3::from(template?.size);
589            Some(rotation.get_bounding_box(IVec3::ZERO, size))
590        }
591        _ => element_bounding_box(element, templates, IVec3::ZERO, rotation),
592    }
593}
594
595fn expand_pool_weights(pool: &TemplatePoolData) -> Vec<&PoolElement> {
596    let mut expanded = Vec::with_capacity(pool.elements.iter().map(|(_, w)| *w as usize).sum());
597    for (element, weight) in &pool.elements {
598        for _ in 0..*weight {
599            expanded.push(element);
600        }
601    }
602    expanded
603}
604
605/// Appends vanilla's `StructureTemplatePool.getShuffledTemplates` to `out`.
606fn append_shuffled_templates_cached<'a>(
607    pool: &'a TemplatePoolData,
608    cache: &mut PoolTemplateCache<'a>,
609    rng: &mut LegacyRandom,
610    out: &mut Vec<&'a PoolElement>,
611) {
612    let expanded = cache
613        .entry(pool.key.clone())
614        .or_insert_with(|| expand_pool_weights(pool));
615    let start = out.len();
616    out.extend(expanded.iter().copied());
617    vanilla_shuffle(&mut out[start..], rng);
618}
619
620/// Vanilla's `StructureTemplatePool.getRandomTemplate`.
621fn get_random_template<'a>(pool: &'a TemplatePoolData, rng: &mut LegacyRandom) -> &'a PoolElement {
622    let expanded = expand_pool_weights(pool);
623    if expanded.is_empty() {
624        static EMPTY: PoolElement = PoolElement::Empty;
625        return &EMPTY;
626    }
627    let idx = rng.next_i32_bounded(expanded.len() as i32) as usize;
628    expanded[idx]
629}
630
631/// Free-space tracker. Small contexts use a flat list; larger contexts switch
632/// to `BoxOctree` for nearby-box queries.
633enum FreeSpace {
634    Small {
635        boundary: BoundingBox,
636        occupied: Vec<BoundingBox>,
637    },
638    Large {
639        occupied: BoxOctree,
640    },
641}
642
643impl FreeSpace {
644    const fn new(constraint: BoundingBox) -> Self {
645        Self::Small {
646            boundary: constraint,
647            occupied: Vec::new(),
648        }
649    }
650
651    fn add_box(&mut self, bbox: BoundingBox) {
652        match self {
653            Self::Small { occupied, .. } if occupied.len() < FREE_SPACE_OCTREE_THRESHOLD => {
654                occupied.push(bbox);
655            }
656            Self::Small {
657                boundary, occupied, ..
658            } => {
659                let mut octree = BoxOctree::new(*boundary);
660                for stored in occupied.drain(..) {
661                    octree.add_box(stored);
662                }
663                octree.add_box(bbox);
664                *self = Self::Large { occupied: octree };
665            }
666            Self::Large { occupied } => {
667                occupied.add_box(bbox);
668            }
669        }
670    }
671
672    fn collides(&self, candidate: &BoundingBox) -> bool {
673        match self {
674            Self::Small {
675                boundary, occupied, ..
676            } => {
677                if candidate.min_x() < boundary.min_x()
678                    || candidate.max_x() > boundary.max_x()
679                    || candidate.min_y() < boundary.min_y()
680                    || candidate.max_y() > boundary.max_y()
681                    || candidate.min_z() < boundary.min_z()
682                    || candidate.max_z() > boundary.max_z()
683                {
684                    return true;
685                }
686
687                // For integer piece boxes, vanilla's deflated AABB collision is
688                // equivalent to inclusive `BoundingBox` intersection.
689                occupied.iter().any(|stored| candidate.intersects(*stored))
690            }
691            Self::Large { occupied } => {
692                !occupied.within_bounds_but_not_intersecting_children(*candidate)
693            }
694        }
695    }
696}
697
698/// Result of a successful jigsaw assembly.
699pub struct AssemblyResult {
700    /// The placed pieces.
701    pub pieces: Vec<PlacedPiece>,
702    /// The biome check position (centerX, centerY, centerZ from the `GenerationStub`).
703    pub biome_check_pos: IVec3,
704}
705
706struct StartedAssembly {
707    pieces: Vec<PlacedPiece>,
708    biome_check_pos: IVec3,
709}
710
711/// Vanilla's `JigsawPlacement.addPieces` before the lazy `GenerationStub` child builder.
712#[expect(
713    clippy::too_many_arguments,
714    reason = "matches vanilla's addPieces call surface"
715)]
716fn start_assembly(
717    config: &JigsawConfig,
718    rng: &mut LegacyRandom,
719    chunk_x: i32,
720    chunk_z: i32,
721    pools: &FxHashMap<Identifier, TemplatePoolData>,
722    templates: &FxHashMap<Identifier, TemplateData>,
723    alias_map: &FxHashMap<Identifier, Identifier>,
724    get_height: &mut dyn FnMut(i32, i32) -> i32,
725    min_y: i32,
726    max_y: i32,
727) -> Option<StartedAssembly> {
728    let start_y = sample_start_height(config, rng);
729    let start_x = chunk_x * 16;
730    let start_z = chunk_z * 16;
731    let center_rotation = Rotation::get_random(rng);
732
733    let start_pool_key = alias_map
734        .get(&config.start_pool)
735        .unwrap_or(&config.start_pool);
736    let start_pool = pools.get(start_pool_key)?;
737    let center_element = get_random_template(start_pool, rng);
738    if center_element.is_empty() {
739        return None;
740    }
741
742    let anchor_offset = if let Some(ref jigsaw_name) = config.start_jigsaw_name {
743        shuffled_element_jigsaws(center_element, templates, center_rotation, rng)
744            .into_iter()
745            .find_map(|block| (block.name == jigsaw_name).then_some(block.pos))?
746    } else {
747        IVec3::ZERO
748    };
749
750    let adjusted = IVec3::new(
751        start_x - anchor_offset.x,
752        start_y - anchor_offset.y,
753        start_z - anchor_offset.z,
754    );
755
756    let center_bb = element_bounding_box(center_element, templates, adjusted, center_rotation)?;
757
758    let bottom_y = if config.project_start_to_heightmap.is_some() {
759        let mid_x = java_center(center_bb.min_x(), center_bb.max_x());
760        let mid_z = java_center(center_bb.min_z(), center_bb.max_z());
761        start_y + get_height(mid_x, mid_z)
762    } else {
763        adjusted.y
764    };
765
766    let ground_level_delta = center_element.projection().ground_level_delta();
767    let dy = bottom_y - (center_bb.min_y() + ground_level_delta);
768    let center_bb = BoundingBox::new(
769        IVec3::new(center_bb.min_x(), center_bb.min_y() + dy, center_bb.min_z()),
770        IVec3::new(center_bb.max_x(), center_bb.max_y() + dy, center_bb.max_z()),
771    );
772    let adjusted_y = adjusted.y + dy;
773
774    let padding = &config.dimension_padding;
775    if center_bb.min_y() < min_y + padding.bottom || center_bb.max_y() > max_y - 1 - padding.top {
776        return None;
777    }
778
779    let pieces = vec![PlacedPiece {
780        element: center_element.clone(),
781        template_location: element_location(center_element).cloned(),
782        position: IVec3::new(adjusted.x, adjusted_y, adjusted.z),
783        rotation: center_rotation,
784        bounding_box: center_bb,
785        assembly_bb: center_bb,
786        ground_level_delta,
787        projection: center_element.projection(),
788        depth: 0,
789        junctions: Vec::new(),
790    }];
791
792    let center_stub_x = java_center(center_bb.min_x(), center_bb.max_x());
793    let center_stub_z = java_center(center_bb.min_z(), center_bb.max_z());
794    let center_stub_y = bottom_y + anchor_offset.y;
795    let biome_check_pos = IVec3::new(center_stub_x, center_stub_y, center_stub_z);
796
797    Some(StartedAssembly {
798        pieces,
799        biome_check_pos,
800    })
801}
802
803#[expect(
804    clippy::too_many_arguments,
805    reason = "matches vanilla's addPieces child-builder call surface"
806)]
807fn finish_assembly<'a>(
808    mut started: StartedAssembly,
809    config: &JigsawConfig,
810    rng: &mut LegacyRandom,
811    pools: &'a FxHashMap<Identifier, TemplatePoolData>,
812    templates: &'a FxHashMap<Identifier, TemplateData>,
813    alias_map: &FxHashMap<Identifier, Identifier>,
814    get_height: &mut dyn FnMut(i32, i32) -> i32,
815    min_y: i32,
816    max_y: i32,
817) -> AssemblyResult {
818    let biome_check_pos = started.biome_check_pos;
819
820    if config.max_depth <= 0 {
821        return AssemblyResult {
822            pieces: started.pieces,
823            biome_check_pos,
824        };
825    }
826
827    let Some(center_piece) = started.pieces.first() else {
828        return AssemblyResult {
829            pieces: started.pieces,
830            biome_check_pos,
831        };
832    };
833    let center_bb = center_piece.assembly_bb;
834    let center_stub_x = biome_check_pos.x;
835    let center_stub_y = biome_check_pos.y;
836    let center_stub_z = biome_check_pos.z;
837
838    let max_dist = config.max_distance_from_center;
839    let constraint_bb = BoundingBox::new(
840        IVec3::new(
841            center_stub_x - max_dist,
842            (center_stub_y - max_dist).max(min_y + config.dimension_padding.bottom),
843            center_stub_z - max_dist,
844        ),
845        IVec3::new(
846            center_stub_x + max_dist,
847            (center_stub_y + max_dist).min(max_y - 1 - config.dimension_padding.top),
848            center_stub_z + max_dist,
849        ),
850    );
851
852    let mut free_spaces: Vec<FreeSpace> = {
853        let mut space = FreeSpace::new(constraint_bb);
854        space.add_box(center_bb);
855        vec![space]
856    };
857    let mut pool_template_cache = PoolTemplateCache::default();
858    let mut assembly_scratch = AssemblyScratch::new();
859    let mut queue = PieceQueue::new();
860
861    try_placing_children(
862        0,
863        0,
864        0,
865        config,
866        pools,
867        templates,
868        alias_map,
869        &mut pool_template_cache,
870        &mut assembly_scratch,
871        &mut started.pieces,
872        &mut free_spaces,
873        &mut queue,
874        rng,
875        get_height,
876    );
877
878    while let Some(entry) = queue.pop() {
879        try_placing_children(
880            entry.piece_idx,
881            entry.depth,
882            entry.context_idx,
883            config,
884            pools,
885            templates,
886            alias_map,
887            &mut pool_template_cache,
888            &mut assembly_scratch,
889            &mut started.pieces,
890            &mut free_spaces,
891            &mut queue,
892            rng,
893            get_height,
894        );
895    }
896
897    AssemblyResult {
898        pieces: started.pieces,
899        biome_check_pos,
900    }
901}
902
903/// Vanilla's `JigsawPlacement.addPieces`. Returns `None` on failure (empty start
904/// pool, dimension padding violation, etc.).
905#[expect(
906    clippy::too_many_arguments,
907    reason = "matches vanilla's addPieces call surface"
908)]
909#[expect(
910    clippy::implicit_hasher,
911    reason = "FxHashMap avoids SipHash overhead on Identifier lookups"
912)]
913pub fn assemble(
914    config: &JigsawConfig,
915    rng: &mut LegacyRandom,
916    chunk_x: i32,
917    chunk_z: i32,
918    pools: &FxHashMap<Identifier, TemplatePoolData>,
919    templates: &FxHashMap<Identifier, TemplateData>,
920    alias_map: &FxHashMap<Identifier, Identifier>,
921    get_height: &mut dyn FnMut(i32, i32) -> i32,
922    min_y: i32,
923    max_y: i32,
924) -> Option<AssemblyResult> {
925    let started = start_assembly(
926        config, rng, chunk_x, chunk_z, pools, templates, alias_map, get_height, min_y, max_y,
927    )?;
928    Some(finish_assembly(
929        started, config, rng, pools, templates, alias_map, get_height, min_y, max_y,
930    ))
931}
932
933/// Registered under `minecraft:jigsaw` for pool-based structures such as villages,
934/// bastions, ancient cities, and trail ruins.
935pub struct JigsawStructure;
936
937impl Structure for JigsawStructure {
938    fn find_generation_point(
939        &self,
940        ctx: &mut dyn StructureGenerationContext,
941        structure: &StructureData,
942        _rng: &mut LegacyRandom,
943    ) -> Option<GenerationStub> {
944        let config = structure.config.as_jigsaw()?;
945
946        let mut alias_position_rng = LegacyRandom::from_seed(0);
947        alias_position_rng.set_large_feature_seed(ctx.seed(), ctx.chunk_x(), ctx.chunk_z());
948        let start_y = sample_start_height(config, &mut alias_position_rng);
949        let mut alias_source = LegacyRandom::from_seed(ctx.seed() as u64);
950        let mut alias_rng =
951            alias_source
952                .next_positional()
953                .at(ctx.chunk_min_x(), start_y, ctx.chunk_min_z());
954        let alias_map = resolve_aliases(&config.pool_aliases, &mut alias_rng);
955
956        let mut assembly_rng = LegacyRandom::from_seed(0);
957        assembly_rng.set_large_feature_seed(ctx.seed(), ctx.chunk_x(), ctx.chunk_z());
958
959        let started = {
960            let mut get_height = |x: i32, z: i32| ctx.terrain_surface_height(x, z, false);
961            start_assembly(
962                config,
963                &mut assembly_rng,
964                ctx.chunk_x(),
965                ctx.chunk_z(),
966                ctx.template_pools(),
967                ctx.templates(),
968                &alias_map,
969                &mut get_height,
970                ctx.min_y(),
971                ctx.max_y(),
972            )?
973        };
974
975        if started.pieces.is_empty() {
976            return None;
977        }
978
979        let biome = ctx.biome_at(
980            started.biome_check_pos.x,
981            started.biome_check_pos.y,
982            started.biome_check_pos.z,
983        );
984        if !structure.allowed_biomes.contains(&biome.key) {
985            return None;
986        }
987
988        let assembly = {
989            let mut get_height = |x: i32, z: i32| ctx.terrain_surface_height(x, z, false);
990            finish_assembly(
991                started,
992                config,
993                &mut assembly_rng,
994                ctx.template_pools(),
995                ctx.templates(),
996                &alias_map,
997                &mut get_height,
998                ctx.min_y(),
999                ctx.max_y(),
1000            )
1001        };
1002
1003        let pieces = assembly
1004            .pieces
1005            .into_iter()
1006            .map(|piece| StructurePiece {
1007                piece_type: Identifier::new_static("minecraft", "jigsaw"),
1008                bounding_box: piece.assembly_bb,
1009                gen_depth: 0,
1010                orientation: None,
1011                payload: StructurePiecePayload::Jigsaw(JigsawPieceData {
1012                    pool_element: piece.element,
1013                    position: piece.position,
1014                    rotation: piece.rotation,
1015                    liquid_settings: config.liquid_settings,
1016                }),
1017                ground_level_delta: piece.ground_level_delta,
1018                junctions: piece.junctions,
1019                projection: Some(piece.projection),
1020            })
1021            .collect();
1022
1023        Some(GenerationStub {
1024            position: (
1025                assembly.biome_check_pos.x,
1026                assembly.biome_check_pos.y,
1027                assembly.biome_check_pos.z,
1028            ),
1029            pieces,
1030        })
1031    }
1032}
1033
1034/// Vanilla's `tryPlacingChildren`. `context_idx` is this piece's collision context
1035/// in `free_spaces` — external children get the parent's context, internal
1036/// children get the parent's internal free space.
1037#[expect(
1038    clippy::too_many_arguments,
1039    reason = "matches vanilla's tryPlacingChildren signature"
1040)]
1041#[expect(
1042    clippy::too_many_lines,
1043    reason = "inlined to mirror vanilla's source-jigsaw/child-pool loop"
1044)]
1045fn try_placing_children<'a>(
1046    source_idx: usize,
1047    depth: i32,
1048    context_idx: usize,
1049    config: &JigsawConfig,
1050    pools: &'a FxHashMap<Identifier, TemplatePoolData>,
1051    templates: &'a FxHashMap<Identifier, TemplateData>,
1052    alias_map: &FxHashMap<Identifier, Identifier>,
1053    pool_template_cache: &mut PoolTemplateCache<'a>,
1054    scratch: &mut AssemblyScratch<'a>,
1055    pieces: &mut Vec<PlacedPiece>,
1056    free_spaces: &mut Vec<FreeSpace>,
1057    queue: &mut PieceQueue,
1058    rng: &mut LegacyRandom,
1059    get_height: &mut dyn FnMut(i32, i32) -> i32,
1060) {
1061    let source_piece = &pieces[source_idx];
1062    let source_location = element_location(&source_piece.element).cloned();
1063    let source_element_empty = source_piece.element.is_empty();
1064    let source_rotation = source_piece.rotation;
1065    let origin = source_piece.position;
1066    let source_bb = source_piece.assembly_bb;
1067    let source_projection = source_piece.projection;
1068    let source_ground_level_delta = source_piece.ground_level_delta;
1069    let source_template = source_location
1070        .as_ref()
1071        .and_then(|location| templates.get(location).map(|template| (location, template)));
1072
1073    if let Some((location, template)) = source_template {
1074        shuffle_jigsaw_indices_with_priority_cache(
1075            location,
1076            template,
1077            rng,
1078            &mut scratch.source_jigsaw_indices,
1079            &mut scratch.jigsaw_order_scratch,
1080            &mut scratch.jigsaw_priority_scratch,
1081            &mut scratch.jigsaw_priority_cache,
1082        );
1083        if scratch.source_jigsaw_indices.is_empty() {
1084            return;
1085        }
1086    } else if source_element_empty {
1087        return;
1088    }
1089
1090    let source_jigsaw_count = if source_template.is_some() {
1091        scratch.source_jigsaw_indices.len()
1092    } else {
1093        1
1094    };
1095    let source_box_y = source_bb.min_y();
1096    let source_rigid = source_projection == Projection::Rigid;
1097
1098    let mut internal_ctx_idx: Option<usize> = None;
1099    let mut candidates: Vec<&PoolElement> = Vec::new();
1100
1101    'source_jigsaw: for source_jigsaw_i in 0..source_jigsaw_count {
1102        let source = if let Some((location, template)) = source_template {
1103            let rotated = cached_runtime_rotated_jigsaws(
1104                location,
1105                template,
1106                source_rotation,
1107                &mut scratch.jigsaw_rotation_cache,
1108            );
1109            let block = rotated[scratch.source_jigsaw_indices[source_jigsaw_i]];
1110            let pos = block.pos + origin;
1111            ActiveSourceJigsaw { block, pos }
1112        } else {
1113            ActiveSourceJigsaw {
1114                block: feature_synthetic_jigsaw(),
1115                pos: origin,
1116            }
1117        };
1118        candidates.clear();
1119        let front = source.block.orientation.front_direction();
1120        let foff = front.offset_vec();
1121        let target_jigsaw_world = source.pos + foff;
1122
1123        let source_jigsaw_local_y = source.pos.y - source_box_y;
1124
1125        let pool_key = alias_map
1126            .get(source.block.pool)
1127            .unwrap_or(source.block.pool);
1128        let raw_pool = pools.get(pool_key);
1129        let target_pool = raw_pool.filter(|p| !p.elements.is_empty());
1130        let fallback_pool = raw_pool
1131            .and_then(|p| pools.get(&p.fallback))
1132            .filter(|p| !p.elements.is_empty());
1133
1134        let attach_inside = source_bb.contains_xyz(
1135            target_jigsaw_world.x,
1136            target_jigsaw_world.y,
1137            target_jigsaw_world.z,
1138        );
1139
1140        if depth != config.max_depth
1141            && let Some(pool) = target_pool
1142        {
1143            append_shuffled_templates_cached(pool, pool_template_cache, rng, &mut candidates);
1144        }
1145        if let Some(fallback) = fallback_pool {
1146            append_shuffled_templates_cached(fallback, pool_template_cache, rng, &mut candidates);
1147        }
1148
1149        let placement_priority = source.block.placement_priority;
1150        let mut source_jigsaw_base_height: Option<i32> = None;
1151        let dedupe_candidates = candidates.len() > CANDIDATE_DEDUPE_THRESHOLD;
1152        if dedupe_candidates {
1153            scratch.parsed_candidates.clear();
1154        }
1155
1156        for &candidate_element in &candidates {
1157            if candidate_element.is_empty() {
1158                break;
1159            }
1160
1161            let rotations = Rotation::get_shuffled(rng);
1162            if dedupe_candidates
1163                && !scratch
1164                    .parsed_candidates
1165                    .insert(ptr::from_ref(candidate_element))
1166            {
1167                prime_duplicate_candidate_rng(
1168                    candidate_element,
1169                    templates,
1170                    rotations,
1171                    rng,
1172                    scratch,
1173                );
1174                continue;
1175            }
1176
1177            let candidate_location = element_location(candidate_element);
1178            let candidate_template = candidate_location
1179                .and_then(|location| templates.get(location).map(|template| (location, template)));
1180            let candidate_template_data = candidate_template.map(|(_, template)| template);
1181            let candidate_projection = candidate_element.projection();
1182            let candidate_rigid = candidate_projection == Projection::Rigid;
1183
1184            for candidate_rotation in rotations {
1185                let expand_to = if config.use_expansion_hack {
1186                    if let Some((hack_location, template_data)) = candidate_template {
1187                        let hack_box = candidate_rotation
1188                            .get_bounding_box(IVec3::ZERO, IVec3::from(template_data.size));
1189                        if hack_box.max_y() - hack_box.min_y() < 16 {
1190                            let rotated = cached_runtime_rotated_jigsaws(
1191                                hack_location,
1192                                template_data,
1193                                candidate_rotation,
1194                                &mut scratch.jigsaw_rotation_cache,
1195                            );
1196                            rotated
1197                                .iter()
1198                                .map(|j| {
1199                                    let pos = j.pos;
1200                                    let front = j.orientation.front_direction();
1201                                    let front_pos = pos + front.offset_vec();
1202                                    if !hack_box.contains_xyz(front_pos.x, front_pos.y, front_pos.z)
1203                                    {
1204                                        return 0;
1205                                    }
1206                                    let child_pool_key = alias_map.get(j.pool).unwrap_or(j.pool);
1207                                    let child_pool_size = cached_pool_max_y_size(
1208                                        child_pool_key,
1209                                        pools,
1210                                        templates,
1211                                        &mut scratch.pool_max_y_cache,
1212                                    );
1213                                    let child_fallback_size =
1214                                        pools.get(child_pool_key).map_or(0, |pool| {
1215                                            cached_pool_max_y_size(
1216                                                &pool.fallback,
1217                                                pools,
1218                                                templates,
1219                                                &mut scratch.pool_max_y_cache,
1220                                            )
1221                                        });
1222                                    child_pool_size.max(child_fallback_size)
1223                                })
1224                                .max()
1225                                .unwrap_or(0)
1226                        } else {
1227                            0
1228                        }
1229                    } else {
1230                        0
1231                    }
1232                } else {
1233                    0
1234                };
1235
1236                let mut candidate_bb_at_origin: Option<BoundingBox> = None;
1237
1238                let mut try_target_jigsaw = |target: &TransformedJigsaw<'_>| -> bool {
1239                    if !source.can_attach_to(target) {
1240                        return false;
1241                    }
1242
1243                    let target_jigsaw_local = target.pos;
1244
1245                    let raw_target = IVec3::new(
1246                        target_jigsaw_world.x - target_jigsaw_local.x,
1247                        0,
1248                        target_jigsaw_world.z - target_jigsaw_local.z,
1249                    );
1250
1251                    let raw_bb = if let Some(bb) = candidate_bb_at_origin {
1252                        bb.translate(IVec3::new(raw_target.x, 0, raw_target.z))
1253                    } else {
1254                        let Some(bb) = candidate_bounding_box_at_origin(
1255                            candidate_element,
1256                            templates,
1257                            candidate_template_data,
1258                            candidate_rotation,
1259                        ) else {
1260                            return false;
1261                        };
1262                        candidate_bb_at_origin = Some(bb);
1263                        bb.translate(IVec3::new(raw_target.x, 0, raw_target.z))
1264                    };
1265
1266                    let target_jigsaw_local_y = target_jigsaw_local.y;
1267                    let delta_y = source_jigsaw_local_y - target_jigsaw_local_y + foff.y;
1268
1269                    let target_box_y = if source_rigid && candidate_rigid {
1270                        source_box_y + delta_y
1271                    } else {
1272                        let base_height = *source_jigsaw_base_height
1273                            .get_or_insert_with(|| get_height(source.pos.x, source.pos.z));
1274                        base_height - target_jigsaw_local_y
1275                    };
1276
1277                    let y_offset = target_box_y - raw_bb.min_y();
1278                    let candidate_bb = BoundingBox::new(
1279                        IVec3::new(raw_bb.min_x(), raw_bb.min_y() + y_offset, raw_bb.min_z()),
1280                        IVec3::new(raw_bb.max_x(), raw_bb.max_y() + y_offset, raw_bb.max_z()),
1281                    );
1282                    let target_position =
1283                        IVec3::new(raw_target.x, raw_bb.min_y() + y_offset, raw_target.z);
1284
1285                    let expanded_bb = if expand_to > 0 {
1286                        let new_size =
1287                            (expand_to + 1).max(candidate_bb.max_y() - candidate_bb.min_y());
1288                        BoundingBox::new(
1289                            IVec3::new(
1290                                candidate_bb.min_x(),
1291                                candidate_bb.min_y(),
1292                                candidate_bb.min_z(),
1293                            ),
1294                            IVec3::new(
1295                                candidate_bb.max_x(),
1296                                candidate_bb.min_y() + new_size,
1297                                candidate_bb.max_z(),
1298                            ),
1299                        )
1300                    } else {
1301                        candidate_bb
1302                    };
1303
1304                    let effective_ctx = if attach_inside {
1305                        *internal_ctx_idx.get_or_insert_with(|| {
1306                            free_spaces.push(FreeSpace::new(source_bb));
1307                            free_spaces.len() - 1
1308                        })
1309                    } else {
1310                        context_idx
1311                    };
1312
1313                    if free_spaces[effective_ctx].collides(&expanded_bb) {
1314                        return false;
1315                    }
1316
1317                    free_spaces[effective_ctx].add_box(expanded_bb);
1318
1319                    let target_ground_level_delta = if candidate_rigid {
1320                        source_ground_level_delta - delta_y
1321                    } else {
1322                        candidate_projection.ground_level_delta()
1323                    };
1324
1325                    let junction_y = if source_rigid {
1326                        source_box_y + source_jigsaw_local_y
1327                    } else if candidate_rigid {
1328                        target_box_y + target_jigsaw_local_y
1329                    } else {
1330                        let base_height = *source_jigsaw_base_height
1331                            .get_or_insert_with(|| get_height(source.pos.x, source.pos.z));
1332                        base_height + delta_y / 2
1333                    };
1334
1335                    pieces[source_idx].junctions.push(JigsawJunction {
1336                        source_pos: IVec3::new(
1337                            target_jigsaw_world.x,
1338                            junction_y - source_jigsaw_local_y + source_ground_level_delta,
1339                            target_jigsaw_world.z,
1340                        ),
1341                        delta_y,
1342                        dest_projection: candidate_projection,
1343                    });
1344
1345                    let new_piece_idx = pieces.len();
1346                    let mut target_piece = PlacedPiece {
1347                        element: candidate_element.clone(),
1348                        template_location: candidate_location.cloned(),
1349                        position: target_position,
1350                        rotation: candidate_rotation,
1351                        bounding_box: candidate_bb,
1352                        assembly_bb: expanded_bb,
1353                        ground_level_delta: target_ground_level_delta,
1354                        projection: candidate_projection,
1355                        depth: depth + 1,
1356                        junctions: Vec::new(),
1357                    };
1358
1359                    target_piece.junctions.push(JigsawJunction {
1360                        source_pos: IVec3::new(
1361                            source.pos.x,
1362                            junction_y - target_jigsaw_local_y + target_ground_level_delta,
1363                            source.pos.z,
1364                        ),
1365                        delta_y: -delta_y,
1366                        dest_projection: source_projection,
1367                    });
1368
1369                    pieces.push(target_piece);
1370
1371                    if depth < config.max_depth {
1372                        scratch.queue_order += 1;
1373                        queue.push(PieceQueueEntry {
1374                            priority: placement_priority,
1375                            order: scratch.queue_order,
1376                            piece_idx: new_piece_idx,
1377                            depth: depth + 1,
1378                            context_idx: effective_ctx,
1379                        });
1380                    }
1381
1382                    true
1383                };
1384
1385                if let Some((location, template)) = candidate_template {
1386                    let rotated = cached_runtime_rotated_jigsaws(
1387                        location,
1388                        template,
1389                        candidate_rotation,
1390                        &mut scratch.jigsaw_rotation_cache,
1391                    );
1392                    shuffle_jigsaw_indices_with_priority_cache(
1393                        location,
1394                        template,
1395                        rng,
1396                        &mut scratch.candidate_jigsaw_indices,
1397                        &mut scratch.jigsaw_order_scratch,
1398                        &mut scratch.jigsaw_priority_scratch,
1399                        &mut scratch.jigsaw_priority_cache,
1400                    );
1401                    for &target_jigsaw_idx in &scratch.candidate_jigsaw_indices {
1402                        if try_target_jigsaw(&rotated[target_jigsaw_idx]) {
1403                            continue 'source_jigsaw;
1404                        }
1405                    }
1406                } else if try_target_jigsaw(&feature_synthetic_jigsaw()) {
1407                    continue 'source_jigsaw;
1408                }
1409            }
1410        }
1411    }
1412}
1413
1414#[cfg(test)]
1415mod tests {
1416    use super::*;
1417    use steel_registry::structure::DimensionPadding;
1418
1419    fn bbox(min: IVec3, max: IVec3) -> BoundingBox {
1420        BoundingBox::new(min, max)
1421    }
1422
1423    fn free_space_boxes() -> Vec<BoundingBox> {
1424        let mut boxes = Vec::with_capacity(FREE_SPACE_OCTREE_THRESHOLD + 2);
1425        boxes.push(bbox(IVec3::new(-1, -1, -1), IVec3::new(1, 1, 1)));
1426
1427        for y in [-50, 50] {
1428            for x in 0..16 {
1429                for z in 0..16 {
1430                    let min = IVec3::new(-120 + x * 16, y, -120 + z * 16);
1431                    boxes.push(bbox(min, min + IVec3::ONE));
1432                }
1433            }
1434        }
1435
1436        boxes.push(bbox(IVec3::new(-1, -1, -1), IVec3::new(1, 1, 1)));
1437        boxes
1438    }
1439
1440    #[test]
1441    fn free_space_large_matches_small_scan_after_octree_transition() {
1442        let boundary = bbox(IVec3::new(-128, -64, -128), IVec3::new(128, 64, 128));
1443        let boxes = free_space_boxes();
1444        let small = FreeSpace::Small {
1445            boundary,
1446            occupied: boxes.clone(),
1447        };
1448        let mut large = FreeSpace::new(boundary);
1449        for bbox in boxes {
1450            large.add_box(bbox);
1451        }
1452
1453        assert!(matches!(large, FreeSpace::Large { .. }));
1454
1455        let candidates = [
1456            bbox(IVec3::new(1, 1, 1), IVec3::new(3, 3, 3)),
1457            bbox(IVec3::new(2, 2, 2), IVec3::new(4, 4, 4)),
1458            bbox(IVec3::new(-2, 10, -2), IVec3::new(2, 12, 2)),
1459            bbox(IVec3::new(124, 0, 0), IVec3::new(128, 2, 2)),
1460            bbox(IVec3::new(127, 0, 0), IVec3::new(129, 2, 2)),
1461        ];
1462
1463        for candidate in candidates {
1464            assert_eq!(
1465                large.collides(&candidate),
1466                small.collides(&candidate),
1467                "collision mismatch for {candidate:?}"
1468            );
1469        }
1470    }
1471
1472    #[test]
1473    fn start_jigsaw_name_can_anchor_feature_pool_element() {
1474        let pool_key = Identifier::vanilla_static("test/feature_start");
1475        let mut pools = FxHashMap::default();
1476        pools.insert(
1477            pool_key.clone(),
1478            TemplatePoolData {
1479                key: pool_key.clone(),
1480                fallback: Identifier::vanilla_static("empty"),
1481                elements: vec![(
1482                    PoolElement::Feature {
1483                        feature: Identifier::vanilla_static("oak"),
1484                        projection: Projection::Rigid,
1485                    },
1486                    1,
1487                )],
1488            },
1489        );
1490        let templates = FxHashMap::default();
1491        let alias_map = FxHashMap::default();
1492        let config = JigsawConfig {
1493            start_pool: pool_key,
1494            max_depth: 0,
1495            use_expansion_hack: false,
1496            project_start_to_heightmap: None,
1497            start_height: StartHeight::Constant(70),
1498            max_distance_from_center: 80,
1499            start_jigsaw_name: Some(Identifier::vanilla_static("bottom")),
1500            dimension_padding: DimensionPadding { bottom: 0, top: 0 },
1501            pool_aliases: Vec::new(),
1502            liquid_settings: LiquidSettingsData::IgnoreWaterlogging,
1503        };
1504        let mut rng = LegacyRandom::from_seed(1);
1505        let mut get_height = |_: i32, _: i32| 64;
1506
1507        let assembly = assemble(
1508            &config,
1509            &mut rng,
1510            0,
1511            0,
1512            &pools,
1513            &templates,
1514            &alias_map,
1515            &mut get_height,
1516            -64,
1517            320,
1518        )
1519        .expect("feature pool element exposes vanilla's synthetic bottom jigsaw");
1520
1521        assert_eq!(assembly.pieces.len(), 1);
1522    }
1523}