Skip to main content

steel_worldgen/structure/
stronghold.rs

1//! Stronghold piece generation. Vanilla's `StrongholdPieces` recursive BFS;
2//! produces bounding boxes only (no blocks).
3
4use 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
30/// Vanilla's `BoundingBox.orientBox`.
31const 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        // South + default
52        _ => 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/// Vanilla `StrongholdPiece.SmallDoorType`.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum StrongholdSmallDoorType {
82    /// Three-block cave-air opening.
83    Opening,
84    /// Oak door framed by stone bricks.
85    WoodDoor,
86    /// Iron-bar grate opening.
87    Grates,
88    /// Iron door with stone buttons.
89    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/// Vanilla stronghold piece runtime state needed by `postProcess`.
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum StrongholdPieceData {
106    /// Straight corridor with optional side exits.
107    Straight {
108        /// Vanilla `entryDoor`.
109        entry_door: StrongholdSmallDoorType,
110        /// Vanilla `leftChild`.
111        left_child: bool,
112        /// Vanilla `rightChild`.
113        right_child: bool,
114    },
115    /// Prison hall.
116    PrisonHall {
117        /// Vanilla `entryDoor`.
118        entry_door: StrongholdSmallDoorType,
119    },
120    /// Left turn.
121    LeftTurn {
122        /// Vanilla `entryDoor`.
123        entry_door: StrongholdSmallDoorType,
124    },
125    /// Right turn.
126    RightTurn {
127        /// Vanilla `entryDoor`.
128        entry_door: StrongholdSmallDoorType,
129    },
130    /// Room crossing with one of five vanilla decorations.
131    RoomCrossing {
132        /// Vanilla `entryDoor`.
133        entry_door: StrongholdSmallDoorType,
134        /// Vanilla `type`.
135        crossing_type: i32,
136    },
137    /// Straight stair corridor.
138    StraightStairsDown {
139        /// Vanilla `entryDoor`.
140        entry_door: StrongholdSmallDoorType,
141    },
142    /// Descending stairs, including the source/start piece.
143    StairsDown {
144        /// Vanilla `entryDoor`.
145        entry_door: StrongholdSmallDoorType,
146        /// Vanilla `isSource`.
147        is_source: bool,
148    },
149    /// Five-way crossing with low/high side exits.
150    FiveCrossing {
151        /// Vanilla `entryDoor`.
152        entry_door: StrongholdSmallDoorType,
153        /// Vanilla `leftLow`.
154        left_low: bool,
155        /// Vanilla `leftHigh`.
156        left_high: bool,
157        /// Vanilla `rightLow`.
158        right_low: bool,
159        /// Vanilla `rightHigh`.
160        right_high: bool,
161    },
162    /// Corridor containing a loot chest.
163    ChestCorridor {
164        /// Vanilla `entryDoor`.
165        entry_door: StrongholdSmallDoorType,
166        /// Vanilla `hasPlacedChest`.
167        has_placed_chest: bool,
168    },
169    /// Library room.
170    Library {
171        /// Vanilla `entryDoor`.
172        entry_door: StrongholdSmallDoorType,
173        /// Vanilla `isTall`.
174        is_tall: bool,
175    },
176    /// End portal room.
177    PortalRoom {
178        /// Vanilla `hasPlacedSpawner`.
179        has_placed_spawner: bool,
180    },
181    /// Collision filler corridor.
182    FillerCorridor {
183        /// Vanilla `steps`.
184        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    /// Returns the vanilla structure piece id for this payload.
207    #[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    /// Vanilla's `updatePieceWeight`. STOPS generation when no limited pieces
313    /// have room, even if unlimited pieces remain.
314    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/// One generated stronghold piece.
677#[derive(Debug, Clone, Copy, PartialEq)]
678pub struct StrongholdGeneratedPiece {
679    /// World-space bounding box.
680    pub bounding_box: BoundingBox,
681    /// Horizontal orientation.
682    pub orientation: Direction,
683    /// Vanilla generation depth.
684    pub gen_depth: i32,
685    /// Stronghold piece placement payload.
686    pub data: StrongholdPieceData,
687}
688
689/// All stronghold pieces for a chunk. Vanilla calls
690/// `setOrientation(direction)` on every stronghold piece, and threads
691/// `genDepth` through the DFS via each subclass's `createPiece` helper.
692#[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
773/// Registered under `"minecraft:stronghold"`. Biome check at chunk center, surface Y.
774pub 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}