1use 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#[derive(Debug, Clone)]
28pub struct PlacedPiece {
29 pub element: PoolElement,
31 pub template_location: Option<Identifier>,
33 pub position: IVec3,
35 pub rotation: Rotation,
37 pub bounding_box: BoundingBox,
39 pub assembly_bb: BoundingBox,
42 pub ground_level_delta: i32,
44 pub projection: Projection,
46 pub depth: i32,
48 pub junctions: Vec<JigsawJunction>,
50}
51
52#[derive(Debug, Clone)]
54pub struct JigsawPieceData {
55 pub pool_element: PoolElement,
57 pub position: IVec3,
59 pub rotation: Rotation,
61 pub liquid_settings: LiquidSettingsData,
63}
64
65#[derive(Debug, Clone)]
67pub struct JigsawJunction {
68 pub source_pos: IVec3,
70 pub delta_y: i32,
72 pub dest_projection: Projection,
74}
75
76pub 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
127const 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#[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
260fn 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
387fn 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 }
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
467struct 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#[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
501fn 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
515fn 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
534fn 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
605fn 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
620fn 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
631enum 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 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
698pub struct AssemblyResult {
700 pub pieces: Vec<PlacedPiece>,
702 pub biome_check_pos: IVec3,
704}
705
706struct StartedAssembly {
707 pieces: Vec<PlacedPiece>,
708 biome_check_pos: IVec3,
709}
710
711#[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#[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
933pub 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#[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}