1use glam::IVec3;
5use steel_registry::structure::StructureData;
6use steel_utils::random::Random;
7use steel_utils::random::legacy_random::LegacyRandom;
8use steel_utils::{BoundingBox, Direction, Identifier};
9
10use crate::structure::{
11 GenerationStub, ProceduralPieceData, Structure, StructureGenerationContext, StructurePiece,
12 StructurePiecePayload,
13};
14
15const MAX_DEPTH: i32 = 50;
16const MAX_DISTANCE: i32 = 112;
17const LOWEST_Y: i32 = 10;
18
19const HORIZONTAL_DIRS: [Direction; 4] = [
20 Direction::North,
21 Direction::East,
22 Direction::South,
23 Direction::West,
24];
25
26fn random_horizontal(rng: &mut LegacyRandom) -> Direction {
27 HORIZONTAL_DIRS[rng.next_i32_bounded(4) as usize]
28}
29
30const fn orient_box(foot: IVec3, off: IVec3, size: IVec3, dir: Direction) -> BoundingBox {
32 let fx = foot.x;
33 let fy = foot.y + off.y;
34 let fz = foot.z;
35 let w = size.x;
36 let h = size.y;
37 let d = size.z;
38 match dir {
39 Direction::North => BoundingBox::new(
40 IVec3::new(fx + off.x, fy, fz - d + 1 + off.z),
41 IVec3::new(fx + w - 1 + off.x, fy + h - 1, fz + off.z),
42 ),
43 Direction::West => BoundingBox::new(
44 IVec3::new(fx - d + 1 + off.z, fy, fz + off.x),
45 IVec3::new(fx + off.z, fy + h - 1, fz + w - 1 + off.x),
46 ),
47 Direction::East => BoundingBox::new(
48 IVec3::new(fx + off.z, fy, fz + off.x),
49 IVec3::new(fx + d - 1 + off.z, fy + h - 1, fz + w - 1 + off.x),
50 ),
51 _ => BoundingBox::new(
53 IVec3::new(fx + off.x, fy, fz + off.z),
54 IVec3::new(fx + w - 1 + off.x, fy + h - 1, fz + d - 1 + off.z),
55 ),
56 }
57}
58
59const fn is_ok(bb: &BoundingBox) -> bool {
60 bb.min_y() > LOWEST_Y
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64enum PT {
65 Straight,
66 Prison,
67 LeftTurn,
68 RightTurn,
69 RoomCrossing,
70 StraightStairs,
71 StairsDown,
72 FiveCrossing,
73 ChestCorridor,
74 Library,
75 Portal,
76 Filler,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum StrongholdSmallDoorType {
82 Opening,
84 WoodDoor,
86 Grates,
88 IronDoor,
90}
91
92impl StrongholdSmallDoorType {
93 fn random(rng: &mut LegacyRandom) -> Self {
94 match rng.next_i32_bounded(5) {
95 2 => Self::WoodDoor,
96 3 => Self::Grates,
97 4 => Self::IronDoor,
98 _ => Self::Opening,
99 }
100 }
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum StrongholdPieceData {
106 Straight {
108 entry_door: StrongholdSmallDoorType,
110 left_child: bool,
112 right_child: bool,
114 },
115 PrisonHall {
117 entry_door: StrongholdSmallDoorType,
119 },
120 LeftTurn {
122 entry_door: StrongholdSmallDoorType,
124 },
125 RightTurn {
127 entry_door: StrongholdSmallDoorType,
129 },
130 RoomCrossing {
132 entry_door: StrongholdSmallDoorType,
134 crossing_type: i32,
136 },
137 StraightStairsDown {
139 entry_door: StrongholdSmallDoorType,
141 },
142 StairsDown {
144 entry_door: StrongholdSmallDoorType,
146 is_source: bool,
148 },
149 FiveCrossing {
151 entry_door: StrongholdSmallDoorType,
153 left_low: bool,
155 left_high: bool,
157 right_low: bool,
159 right_high: bool,
161 },
162 ChestCorridor {
164 entry_door: StrongholdSmallDoorType,
166 has_placed_chest: bool,
168 },
169 Library {
171 entry_door: StrongholdSmallDoorType,
173 is_tall: bool,
175 },
176 PortalRoom {
178 has_placed_spawner: bool,
180 },
181 FillerCorridor {
183 steps: i32,
185 },
186}
187
188impl StrongholdPieceData {
189 const fn piece_type(self) -> PT {
190 match self {
191 Self::Straight { .. } => PT::Straight,
192 Self::PrisonHall { .. } => PT::Prison,
193 Self::LeftTurn { .. } => PT::LeftTurn,
194 Self::RightTurn { .. } => PT::RightTurn,
195 Self::RoomCrossing { .. } => PT::RoomCrossing,
196 Self::StraightStairsDown { .. } => PT::StraightStairs,
197 Self::StairsDown { .. } => PT::StairsDown,
198 Self::FiveCrossing { .. } => PT::FiveCrossing,
199 Self::ChestCorridor { .. } => PT::ChestCorridor,
200 Self::Library { .. } => PT::Library,
201 Self::PortalRoom { .. } => PT::Portal,
202 Self::FillerCorridor { .. } => PT::Filler,
203 }
204 }
205
206 #[must_use]
208 pub const fn piece_id(self) -> &'static str {
209 match self {
210 Self::StairsDown {
211 is_source: true, ..
212 } => "shstart",
213 Self::StairsDown { .. } => "shsd",
214 Self::Straight { .. } => "shs",
215 Self::PrisonHall { .. } => "shph",
216 Self::LeftTurn { .. } => "shlt",
217 Self::RightTurn { .. } => "shrt",
218 Self::RoomCrossing { .. } => "shrc",
219 Self::StraightStairsDown { .. } => "shssd",
220 Self::FiveCrossing { .. } => "sh5c",
221 Self::ChestCorridor { .. } => "shcc",
222 Self::Library { .. } => "shli",
223 Self::PortalRoom { .. } => "shpr",
224 Self::FillerCorridor { .. } => "shfc",
225 }
226 }
227
228 const fn start() -> Self {
229 Self::StairsDown {
230 entry_door: StrongholdSmallDoorType::Opening,
231 is_source: true,
232 }
233 }
234}
235
236struct PieceWeight {
237 pt: PT,
238 weight: i32,
239 max: i32,
240 count: i32,
241 min_depth: i32,
242}
243impl PieceWeight {
244 const fn can(&self, depth: i32) -> bool {
245 (self.max == 0 || self.count < self.max) && depth >= self.min_depth
246 }
247}
248
249fn weights() -> Vec<PieceWeight> {
250 #[rustfmt::skip]
251 const W: &[(PT, i32, i32, i32)] = &[
252 (PT::Straight, 40, 0, 0),
253 (PT::Prison, 5, 5, 0),
254 (PT::LeftTurn, 20, 0, 0),
255 (PT::RightTurn, 20, 0, 0),
256 (PT::RoomCrossing, 10, 6, 0),
257 (PT::StraightStairs, 5, 5, 0),
258 (PT::StairsDown, 5, 5, 0),
259 (PT::FiveCrossing, 5, 4, 0),
260 (PT::ChestCorridor, 5, 4, 0),
261 (PT::Library, 10, 2, 5),
262 (PT::Portal, 20, 1, 6),
263 ];
264 W.iter()
265 .map(|&(pt, weight, max, min_depth)| PieceWeight {
266 pt,
267 weight,
268 max,
269 count: 0,
270 min_depth,
271 })
272 .collect()
273}
274struct Piece {
275 bb: BoundingBox,
276 dir: Direction,
277 depth: i32,
278 data: StrongholdPieceData,
279}
280
281impl Piece {
282 const fn new(bb: BoundingBox, dir: Direction, depth: i32, data: StrongholdPieceData) -> Self {
283 Self {
284 bb,
285 dir,
286 depth,
287 data,
288 }
289 }
290
291 const fn pt(&self) -> PT {
292 self.data.piece_type()
293 }
294}
295
296struct State {
297 pieces: Vec<Piece>,
298 pending: Vec<usize>,
299 wts: Vec<PieceWeight>,
300 start_bb: BoundingBox,
301 prev_pt: Option<PT>,
302 has_portal: bool,
303 imposed: Option<PT>,
304 total_weight: i32,
305}
306
307impl State {
308 fn collides(&self, bb: BoundingBox) -> bool {
309 self.pieces.iter().any(|p| p.bb.intersects(bb))
310 }
311
312 fn update_weights(&mut self) -> bool {
315 let mut has_any = false;
316 self.total_weight = 0;
317 for w in &self.wts {
318 if w.max > 0 && w.count < w.max {
319 has_any = true;
320 }
321 self.total_weight += w.weight;
322 }
323 has_any
324 }
325}
326
327fn find_box(pt: PT, s: &State, foot: IVec3, dir: Direction) -> Option<BoundingBox> {
328 let bb = match pt {
329 PT::Straight | PT::ChestCorridor => {
330 orient_box(foot, IVec3::new(-1, -1, 0), IVec3::new(5, 5, 7), dir)
331 }
332 PT::StairsDown => orient_box(foot, IVec3::new(-1, -7, 0), IVec3::new(5, 11, 5), dir),
333 PT::StraightStairs => orient_box(foot, IVec3::new(-1, -7, 0), IVec3::new(5, 11, 8), dir),
334 PT::LeftTurn | PT::RightTurn => {
335 orient_box(foot, IVec3::new(-1, -1, 0), IVec3::new(5, 5, 5), dir)
336 }
337 PT::RoomCrossing => orient_box(foot, IVec3::new(-4, -1, 0), IVec3::new(11, 7, 11), dir),
338 PT::Prison => orient_box(foot, IVec3::new(-1, -1, 0), IVec3::new(9, 5, 11), dir),
339 PT::FiveCrossing => orient_box(foot, IVec3::new(-4, -3, 0), IVec3::new(10, 9, 11), dir),
340 PT::Portal => orient_box(foot, IVec3::new(-4, -1, 0), IVec3::new(11, 8, 16), dir),
341 PT::Library => {
342 let tall = orient_box(foot, IVec3::new(-4, -1, 0), IVec3::new(14, 11, 15), dir);
343 if is_ok(&tall) && !s.collides(tall) {
344 return Some(tall);
345 }
346 orient_box(foot, IVec3::new(-4, -1, 0), IVec3::new(14, 6, 15), dir)
347 }
348 PT::Filler => {
349 let full_box = orient_box(foot, IVec3::new(-1, -1, 0), IVec3::new(5, 5, 4), dir);
350 let collision = s.pieces.iter().find(|p| p.bb.intersects(full_box))?;
351 if collision.bb.min_y() != full_box.min_y() {
352 return None;
353 }
354 for d in (1..=2).rev() {
355 let b = orient_box(foot, IVec3::new(-1, -1, 0), IVec3::new(5, 5, d), dir);
356 if !collision.bb.intersects(b) {
357 return Some(orient_box(
358 foot,
359 IVec3::new(-1, -1, 0),
360 IVec3::new(5, 5, d + 1),
361 dir,
362 ));
363 }
364 }
365 return None;
366 }
367 };
368 if is_ok(&bb) && !s.collides(bb) {
369 Some(bb)
370 } else {
371 None
372 }
373}
374
375fn create_piece(
376 pt: PT,
377 bb: BoundingBox,
378 dir: Direction,
379 depth: i32,
380 rng: &mut LegacyRandom,
381) -> Piece {
382 let data = match pt {
383 PT::Straight => {
384 let entry_door = StrongholdSmallDoorType::random(rng);
385 StrongholdPieceData::Straight {
386 entry_door,
387 left_child: rng.next_i32_bounded(2) == 0,
388 right_child: rng.next_i32_bounded(2) == 0,
389 }
390 }
391 PT::FiveCrossing => {
392 let entry_door = StrongholdSmallDoorType::random(rng);
393 StrongholdPieceData::FiveCrossing {
394 entry_door,
395 left_low: rng.next_bool(),
396 left_high: rng.next_bool(),
397 right_low: rng.next_bool(),
398 right_high: rng.next_i32_bounded(3) > 0,
399 }
400 }
401 PT::RoomCrossing => {
402 let entry_door = StrongholdSmallDoorType::random(rng);
403 StrongholdPieceData::RoomCrossing {
404 entry_door,
405 crossing_type: rng.next_i32_bounded(5),
406 }
407 }
408 PT::Library => {
409 let entry_door = StrongholdSmallDoorType::random(rng);
410 StrongholdPieceData::Library {
411 entry_door,
412 is_tall: bb.height() > 6,
413 }
414 }
415 PT::Portal => StrongholdPieceData::PortalRoom {
416 has_placed_spawner: false,
417 },
418 PT::Filler => StrongholdPieceData::FillerCorridor {
419 steps: if matches!(dir, Direction::North | Direction::South) {
420 bb.depth()
421 } else {
422 bb.width()
423 },
424 },
425 PT::StairsDown => StrongholdPieceData::StairsDown {
426 entry_door: StrongholdSmallDoorType::random(rng),
427 is_source: false,
428 },
429 PT::ChestCorridor => StrongholdPieceData::ChestCorridor {
430 entry_door: StrongholdSmallDoorType::random(rng),
431 has_placed_chest: false,
432 },
433 PT::StraightStairs => StrongholdPieceData::StraightStairsDown {
434 entry_door: StrongholdSmallDoorType::random(rng),
435 },
436 PT::LeftTurn => StrongholdPieceData::LeftTurn {
437 entry_door: StrongholdSmallDoorType::random(rng),
438 },
439 PT::RightTurn => StrongholdPieceData::RightTurn {
440 entry_door: StrongholdSmallDoorType::random(rng),
441 },
442 PT::Prison => StrongholdPieceData::PrisonHall {
443 entry_door: StrongholdSmallDoorType::random(rng),
444 },
445 };
446 Piece::new(bb, dir, depth, data)
447}
448
449fn generate_piece(
450 s: &mut State,
451 rng: &mut LegacyRandom,
452 fx: i32,
453 fy: i32,
454 fz: i32,
455 dir: Direction,
456 depth: i32,
457) -> Option<Piece> {
458 if !s.update_weights() {
459 return None;
460 }
461
462 let foot = IVec3::new(fx, fy, fz);
463
464 if let Some(imp) = s.imposed.take()
465 && let Some(bb) = find_box(imp, s, foot, dir)
466 {
467 return Some(create_piece(imp, bb, dir, depth, rng));
468 }
469
470 for _ in 0..5 {
471 if s.total_weight <= 0 {
472 break;
473 }
474 let mut choice = rng.next_i32_bounded(s.total_weight);
475 for wi in 0..s.wts.len() {
476 choice -= s.wts[wi].weight;
477 if choice < 0 {
478 if !s.wts[wi].can(depth) || Some(s.wts[wi].pt) == s.prev_pt {
479 break;
480 }
481 if let Some(bb) = find_box(s.wts[wi].pt, s, foot, dir) {
482 let pt = s.wts[wi].pt;
483 let piece = create_piece(pt, bb, dir, depth, rng);
484 s.wts[wi].count += 1;
485 s.prev_pt = Some(pt);
486 if s.wts[wi].max > 0 && s.wts[wi].count >= s.wts[wi].max {
487 s.wts.remove(wi);
488 }
489 return Some(piece);
490 }
491 }
492 }
493 }
494
495 if let Some(bb) = find_box(PT::Filler, s, foot, dir)
496 && bb.min_y() > 1
497 {
498 return Some(create_piece(PT::Filler, bb, dir, depth, rng));
499 }
500 None
501}
502
503fn gen_and_add(
504 s: &mut State,
505 rng: &mut LegacyRandom,
506 fx: i32,
507 fy: i32,
508 fz: i32,
509 dir: Direction,
510 depth: i32,
511) {
512 if depth > MAX_DEPTH
513 || (fx - s.start_bb.min_x()).abs() > MAX_DISTANCE
514 || (fz - s.start_bb.min_z()).abs() > MAX_DISTANCE
515 {
516 return;
517 }
518 if let Some(piece) = generate_piece(s, rng, fx, fy, fz, dir, depth) {
519 if piece.pt() == PT::Portal {
520 s.has_portal = true;
521 }
522 let idx = s.pieces.len();
523 s.pieces.push(piece);
524 s.pending.push(idx);
525 }
526}
527
528fn add_children(s: &mut State, rng: &mut LegacyRandom, idx: usize) {
529 let Piece {
530 bb,
531 dir,
532 depth,
533 data,
534 ..
535 } = s.pieces[idx];
536 let pt = data.piece_type();
537 let nw_facing = matches!(dir, Direction::North | Direction::East);
538
539 match pt {
540 PT::StairsDown => {
541 if depth == 0 {
542 s.imposed = Some(PT::FiveCrossing);
543 }
544 fwd(s, rng, bb, dir, depth, 1, 1);
545 }
546 PT::StraightStairs | PT::ChestCorridor | PT::Prison => {
547 fwd(s, rng, bb, dir, depth, 1, 1);
548 }
549 PT::Straight => {
550 let StrongholdPieceData::Straight {
551 left_child: lc,
552 right_child: rc,
553 ..
554 } = data
555 else {
556 return;
557 };
558 fwd(s, rng, bb, dir, depth, 1, 1);
559 if lc {
560 left(s, rng, bb, dir, depth, 1, 2);
561 }
562 if rc {
563 right(s, rng, bb, dir, depth, 1, 2);
564 }
565 }
566 PT::LeftTurn => {
567 if nw_facing {
568 left(s, rng, bb, dir, depth, 1, 1);
569 } else {
570 right(s, rng, bb, dir, depth, 1, 1);
571 }
572 }
573 PT::RightTurn => {
574 if nw_facing {
575 right(s, rng, bb, dir, depth, 1, 1);
576 } else {
577 left(s, rng, bb, dir, depth, 1, 1);
578 }
579 }
580 PT::RoomCrossing => {
581 fwd(s, rng, bb, dir, depth, 4, 1);
582 left(s, rng, bb, dir, depth, 1, 4);
583 right(s, rng, bb, dir, depth, 1, 4);
584 }
585 PT::FiveCrossing => {
586 let StrongholdPieceData::FiveCrossing {
587 left_low: ll,
588 left_high: lh,
589 right_low: rl,
590 right_high: rh,
591 ..
592 } = data
593 else {
594 return;
595 };
596 let (za, zb) = if matches!(dir, Direction::West | Direction::North) {
597 (5, 3)
598 } else {
599 (3, 5)
600 };
601 fwd(s, rng, bb, dir, depth, 5, 1);
602 if ll {
603 left(s, rng, bb, dir, depth, za, 1);
604 }
605 if lh {
606 left(s, rng, bb, dir, depth, zb, 7);
607 }
608 if rl {
609 right(s, rng, bb, dir, depth, za, 1);
610 }
611 if rh {
612 right(s, rng, bb, dir, depth, zb, 7);
613 }
614 }
615 PT::Library | PT::Filler | PT::Portal => {}
616 }
617}
618
619fn fwd(
620 s: &mut State,
621 rng: &mut LegacyRandom,
622 bb: BoundingBox,
623 dir: Direction,
624 depth: i32,
625 x_off: i32,
626 y_off: i32,
627) {
628 let (fx, fz) = match dir {
629 Direction::North => (bb.min_x() + x_off, bb.min_z() - 1),
630 Direction::South => (bb.min_x() + x_off, bb.max_z() + 1),
631 Direction::West => (bb.min_x() - 1, bb.min_z() + x_off),
632 Direction::East => (bb.max_x() + 1, bb.min_z() + x_off),
633 _ => return,
634 };
635 gen_and_add(s, rng, fx, bb.min_y() + y_off, fz, dir, depth + 1);
636}
637
638fn left(
639 s: &mut State,
640 rng: &mut LegacyRandom,
641 bb: BoundingBox,
642 dir: Direction,
643 depth: i32,
644 y_off: i32,
645 z_off: i32,
646) {
647 let (fx, fz, d) = match dir {
648 Direction::North | Direction::South => {
649 (bb.min_x() - 1, bb.min_z() + z_off, Direction::West)
650 }
651 Direction::West | Direction::East => (bb.min_x() + z_off, bb.min_z() - 1, Direction::North),
652 _ => return,
653 };
654 gen_and_add(s, rng, fx, bb.min_y() + y_off, fz, d, depth + 1);
655}
656
657fn right(
658 s: &mut State,
659 rng: &mut LegacyRandom,
660 bb: BoundingBox,
661 dir: Direction,
662 depth: i32,
663 y_off: i32,
664 z_off: i32,
665) {
666 let (fx, fz, d) = match dir {
667 Direction::North | Direction::South => {
668 (bb.max_x() + 1, bb.min_z() + z_off, Direction::East)
669 }
670 Direction::West | Direction::East => (bb.min_x() + z_off, bb.max_z() + 1, Direction::South),
671 _ => return,
672 };
673 gen_and_add(s, rng, fx, bb.min_y() + y_off, fz, d, depth + 1);
674}
675
676#[derive(Debug, Clone, Copy, PartialEq)]
678pub struct StrongholdGeneratedPiece {
679 pub bounding_box: BoundingBox,
681 pub orientation: Direction,
683 pub gen_depth: i32,
685 pub data: StrongholdPieceData,
687}
688
689#[must_use]
693pub fn generate_pieces(seed: i64, chunk_x: i32, chunk_z: i32) -> Vec<StrongholdGeneratedPiece> {
694 let west = chunk_x * 16 + 2;
695 let north = chunk_z * 16 + 2;
696
697 let mut tries = 0i64;
698 loop {
699 let mut rng = LegacyRandom::from_seed(0);
700 rng.set_large_feature_seed(seed.wrapping_add(tries), chunk_x, chunk_z);
701 tries += 1;
702
703 let start_dir = random_horizontal(&mut rng);
704 let start_bb = BoundingBox::new(
705 IVec3::new(west, 64, north),
706 IVec3::new(west + 4, 74, north + 4),
707 );
708
709 let mut s = State {
710 pieces: vec![Piece::new(
711 start_bb,
712 start_dir,
713 0,
714 StrongholdPieceData::start(),
715 )],
716 pending: Vec::new(),
717 wts: weights(),
718 start_bb,
719 prev_pt: None,
720 has_portal: false,
721 imposed: None,
722 total_weight: 0,
723 };
724
725 add_children(&mut s, &mut rng, 0);
726 while !s.pending.is_empty() {
727 let idx = rng.next_i32_bounded(s.pending.len() as i32) as usize;
728 let piece_idx = s.pending.remove(idx);
729 add_children(&mut s, &mut rng, piece_idx);
730 }
731
732 if s.pieces.is_empty() || !s.has_portal {
733 continue;
734 }
735
736 let (min_y, max_y) = (-64, 63 - 10);
737 let mut overall = s.pieces[0].bb;
738 for p in &s.pieces[1..] {
739 overall = BoundingBox::new(
740 IVec3::new(
741 overall.min_x().min(p.bb.min_x()),
742 overall.min_y().min(p.bb.min_y()),
743 overall.min_z().min(p.bb.min_z()),
744 ),
745 IVec3::new(
746 overall.max_x().max(p.bb.max_x()),
747 overall.max_y().max(p.bb.max_y()),
748 overall.max_z().max(p.bb.max_z()),
749 ),
750 );
751 }
752 let mut y1_pos = (overall.max_y() - overall.min_y() + 1) + min_y + 1;
753 if y1_pos < max_y {
754 y1_pos += rng.next_i32_bounded(max_y - y1_pos);
755 }
756 let dy = y1_pos - overall.max_y();
757 return s
758 .pieces
759 .into_iter()
760 .map(|p| StrongholdGeneratedPiece {
761 bounding_box: BoundingBox::new(
762 IVec3::new(p.bb.min_x(), p.bb.min_y() + dy, p.bb.min_z()),
763 IVec3::new(p.bb.max_x(), p.bb.max_y() + dy, p.bb.max_z()),
764 ),
765 orientation: p.dir,
766 gen_depth: p.depth,
767 data: p.data,
768 })
769 .collect();
770 }
771}
772
773pub struct StrongholdStructure;
775
776impl Structure for StrongholdStructure {
777 fn find_generation_point(
778 &self,
779 ctx: &mut dyn StructureGenerationContext,
780 structure: &StructureData,
781 _rng: &mut LegacyRandom,
782 ) -> Option<GenerationStub> {
783 let surface_y = ctx.surface_y();
784 let biome = ctx.biome_at(ctx.center_block_x(), surface_y, ctx.center_block_z());
785 if !structure.allowed_biomes.contains(&biome.key) {
786 return None;
787 }
788
789 Some(GenerationStub {
790 position: (ctx.center_block_x(), surface_y, ctx.center_block_z()),
791 pieces: generate_pieces(ctx.seed(), ctx.chunk_x(), ctx.chunk_z())
792 .into_iter()
793 .map(|piece| {
794 let piece_type = piece.data.piece_id();
795 StructurePiece {
796 piece_type: Identifier::new_static("minecraft", piece_type),
797 bounding_box: piece.bounding_box,
798 gen_depth: piece.gen_depth,
799 orientation: Some(piece.orientation),
800 payload: StructurePiecePayload::Procedural(
801 ProceduralPieceData::Stronghold(piece.data),
802 ),
803 ground_level_delta: 0,
804 junctions: Vec::new(),
805 projection: None,
806 }
807 })
808 .collect(),
809 })
810 }
811}
812
813#[cfg(test)]
814mod tests {
815 use super::*;
816
817 #[test]
818 fn orient_box_swaps_offsets_for_east_west_like_vanilla() {
819 let foot = IVec3::new(100, 50, 200);
820 let off = IVec3::new(-1, -1, 0);
821 let size = IVec3::new(5, 5, 7);
822
823 assert_eq!(
824 orient_box(foot, off, size, Direction::East),
825 BoundingBox::new(IVec3::new(100, 49, 199), IVec3::new(106, 53, 203))
826 );
827 assert_eq!(
828 orient_box(foot, off, size, Direction::West),
829 BoundingBox::new(IVec3::new(94, 49, 199), IVec3::new(100, 53, 203))
830 );
831 }
832
833 #[test]
834 fn constructor_rng_state_is_captured_in_piece_payloads() {
835 let bb = BoundingBox::new(IVec3::new(0, 20, 0), IVec3::new(13, 30, 14));
836 let mut actual = LegacyRandom::from_seed(12_345);
837 let mut expected = LegacyRandom::from_seed(12_345);
838
839 let straight = create_piece(PT::Straight, bb, Direction::South, 3, &mut actual);
840 let straight_door = StrongholdSmallDoorType::random(&mut expected);
841 assert_eq!(
842 straight.data,
843 StrongholdPieceData::Straight {
844 entry_door: straight_door,
845 left_child: expected.next_i32_bounded(2) == 0,
846 right_child: expected.next_i32_bounded(2) == 0,
847 }
848 );
849 assert_eq!(actual.next_i32(), expected.next_i32());
850
851 let five_crossing = create_piece(PT::FiveCrossing, bb, Direction::South, 4, &mut actual);
852 let five_crossing_door = StrongholdSmallDoorType::random(&mut expected);
853 assert_eq!(
854 five_crossing.data,
855 StrongholdPieceData::FiveCrossing {
856 entry_door: five_crossing_door,
857 left_low: expected.next_bool(),
858 left_high: expected.next_bool(),
859 right_low: expected.next_bool(),
860 right_high: expected.next_i32_bounded(3) > 0,
861 }
862 );
863 assert_eq!(actual.next_i32(), expected.next_i32());
864
865 let room_crossing = create_piece(PT::RoomCrossing, bb, Direction::South, 5, &mut actual);
866 let room_crossing_door = StrongholdSmallDoorType::random(&mut expected);
867 assert_eq!(
868 room_crossing.data,
869 StrongholdPieceData::RoomCrossing {
870 entry_door: room_crossing_door,
871 crossing_type: expected.next_i32_bounded(5),
872 }
873 );
874 assert_eq!(actual.next_i32(), expected.next_i32());
875 }
876
877 #[test]
878 fn library_and_filler_payloads_capture_non_random_state() {
879 let tall_library = create_piece(
880 PT::Library,
881 BoundingBox::new(IVec3::new(0, 20, 0), IVec3::new(13, 30, 14)),
882 Direction::South,
883 7,
884 &mut LegacyRandom::from_seed(1),
885 );
886 assert!(matches!(
887 tall_library.data,
888 StrongholdPieceData::Library { is_tall: true, .. }
889 ));
890
891 let short_library = create_piece(
892 PT::Library,
893 BoundingBox::new(IVec3::new(0, 20, 0), IVec3::new(13, 25, 14)),
894 Direction::South,
895 7,
896 &mut LegacyRandom::from_seed(1),
897 );
898 assert!(matches!(
899 short_library.data,
900 StrongholdPieceData::Library { is_tall: false, .. }
901 ));
902
903 let filler = create_piece(
904 PT::Filler,
905 BoundingBox::new(IVec3::new(0, 20, 0), IVec3::new(4, 24, 2)),
906 Direction::North,
907 4,
908 &mut LegacyRandom::from_seed(1),
909 );
910 assert_eq!(
911 filler.data,
912 StrongholdPieceData::FillerCorridor { steps: 3 }
913 );
914 }
915}