Skip to main content

steel_worldgen/structure/
fortress.rs

1//! Nether fortress. Vanilla's `NetherFortressPieces`: start with a `BridgeCrossing`,
2//! then weighted BFS over bridge/castle pools honoring place-count, prev-piece, and
3//! collision constraints. Structure is vertically offset into `Y ∈ [48, 70]`.
4
5use glam::IVec3;
6use steel_registry::structure::StructureData;
7use steel_utils::BoundingBox;
8use steel_utils::Direction;
9use steel_utils::Identifier;
10use steel_utils::random::Random;
11use steel_utils::random::legacy_random::LegacyRandom;
12
13use crate::structure::{
14    GenerationStub, ProceduralPieceData, Structure, StructureGenerationContext, StructurePiece,
15    StructurePiecePayload,
16};
17
18const MAX_DEPTH: i32 = 30;
19const LOWEST_Y: i32 = 10;
20const MAGIC_START_Y: i32 = 64;
21const START_X_OFFSET: i32 = 2;
22const START_Z_OFFSET: i32 = 2;
23const DIST_LIMIT: i32 = 112;
24const Y_LOW_ALLOWED: i32 = 48;
25const Y_HIGH_ALLOWED: i32 = 70;
26
27/// Vanilla `Direction.Plane.HORIZONTAL` order: N, E, S, W.
28const HORIZONTAL_ORDER: [Direction; 4] = [
29    Direction::North,
30    Direction::East,
31    Direction::South,
32    Direction::West,
33];
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub(crate) enum FortressPieceKind {
37    BridgeCrossing,
38    BridgeEndFiller,
39    BridgeStraight,
40    CastleCorridorStairs,
41    CastleCorridorTBalcony,
42    CastleEntrance,
43    CastleSmallCorridorCrossing,
44    CastleSmallCorridorLeftTurn,
45    CastleSmallCorridor,
46    CastleSmallCorridorRightTurn,
47    CastleStalkRoom,
48    MonsterThrone,
49    RoomCrossing,
50    StairsRoom,
51}
52
53impl FortressPieceKind {
54    /// Vanilla's `StructurePieceType` registry path (lowercased, no namespace).
55    pub(crate) const fn piece_id(self) -> &'static str {
56        match self {
57            FortressPieceKind::BridgeCrossing => "nebcr",
58            FortressPieceKind::BridgeEndFiller => "nebef",
59            FortressPieceKind::BridgeStraight => "nebs",
60            FortressPieceKind::CastleCorridorStairs => "neccs",
61            FortressPieceKind::CastleCorridorTBalcony => "nectb",
62            FortressPieceKind::CastleEntrance => "nece",
63            FortressPieceKind::CastleSmallCorridorCrossing => "nescsc",
64            FortressPieceKind::CastleSmallCorridorLeftTurn => "nesclt",
65            FortressPieceKind::CastleSmallCorridor => "nesc",
66            FortressPieceKind::CastleSmallCorridorRightTurn => "nescrt",
67            FortressPieceKind::CastleStalkRoom => "necsr",
68            FortressPieceKind::MonsterThrone => "nemt",
69            FortressPieceKind::RoomCrossing => "nerc",
70            FortressPieceKind::StairsRoom => "nesr",
71        }
72    }
73
74    /// `(offset, size)` for vanilla's `orientBox`.
75    const fn geom(self) -> (IVec3, IVec3) {
76        match self {
77            FortressPieceKind::BridgeCrossing => (IVec3::new(-8, -3, 0), IVec3::new(19, 10, 19)),
78            FortressPieceKind::BridgeEndFiller => (IVec3::new(-1, -3, 0), IVec3::new(5, 10, 8)),
79            FortressPieceKind::BridgeStraight => (IVec3::new(-1, -3, 0), IVec3::new(5, 10, 19)),
80            FortressPieceKind::CastleCorridorStairs => {
81                (IVec3::new(-1, -7, 0), IVec3::new(5, 14, 10))
82            }
83            FortressPieceKind::CastleCorridorTBalcony => {
84                (IVec3::new(-3, 0, 0), IVec3::new(9, 7, 9))
85            }
86            FortressPieceKind::CastleEntrance | FortressPieceKind::CastleStalkRoom => {
87                (IVec3::new(-5, -3, 0), IVec3::new(13, 14, 13))
88            }
89            FortressPieceKind::CastleSmallCorridorCrossing
90            | FortressPieceKind::CastleSmallCorridorLeftTurn
91            | FortressPieceKind::CastleSmallCorridor
92            | FortressPieceKind::CastleSmallCorridorRightTurn => {
93                (IVec3::new(-1, 0, 0), IVec3::new(5, 7, 5))
94            }
95            FortressPieceKind::MonsterThrone => (IVec3::new(-2, 0, 0), IVec3::new(7, 8, 9)),
96            FortressPieceKind::RoomCrossing => (IVec3::new(-2, 0, 0), IVec3::new(7, 9, 7)),
97            FortressPieceKind::StairsRoom => (IVec3::new(-2, 0, 0), IVec3::new(7, 11, 7)),
98        }
99    }
100}
101
102/// Vanilla nether-fortress piece payload persisted for feature-stage placement.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum FortressPieceData {
105    /// Bridge crossing piece.
106    BridgeCrossing,
107    /// Dead-end bridge filler piece.
108    BridgeEndFiller {
109        /// Vanilla `BridgeEndFiller.selfSeed`.
110        self_seed: i32,
111    },
112    /// Straight bridge segment.
113    BridgeStraight,
114    /// Castle corridor stair segment.
115    CastleCorridorStairs,
116    /// Castle corridor T balcony segment.
117    CastleCorridorTBalcony,
118    /// Castle entrance room.
119    CastleEntrance,
120    /// Small castle corridor crossing.
121    CastleSmallCorridorCrossing,
122    /// Small castle corridor left turn.
123    CastleSmallCorridorLeftTurn {
124        /// Vanilla `isNeedingChest`.
125        is_needing_chest: bool,
126    },
127    /// Small straight castle corridor.
128    CastleSmallCorridor,
129    /// Small castle corridor right turn.
130    CastleSmallCorridorRightTurn {
131        /// Vanilla `isNeedingChest`.
132        is_needing_chest: bool,
133    },
134    /// Nether-wart stair room.
135    CastleStalkRoom,
136    /// Blaze-spawner throne room.
137    MonsterThrone {
138        /// Vanilla `hasPlacedSpawner`.
139        has_placed_spawner: bool,
140    },
141    /// Bridge room crossing.
142    RoomCrossing,
143    /// Bridge stair room.
144    StairsRoom,
145}
146
147impl FortressPieceData {
148    #[must_use]
149    pub(crate) const fn kind(self) -> FortressPieceKind {
150        match self {
151            Self::BridgeCrossing => FortressPieceKind::BridgeCrossing,
152            Self::BridgeEndFiller { .. } => FortressPieceKind::BridgeEndFiller,
153            Self::BridgeStraight => FortressPieceKind::BridgeStraight,
154            Self::CastleCorridorStairs => FortressPieceKind::CastleCorridorStairs,
155            Self::CastleCorridorTBalcony => FortressPieceKind::CastleCorridorTBalcony,
156            Self::CastleEntrance => FortressPieceKind::CastleEntrance,
157            Self::CastleSmallCorridorCrossing => FortressPieceKind::CastleSmallCorridorCrossing,
158            Self::CastleSmallCorridorLeftTurn { .. } => {
159                FortressPieceKind::CastleSmallCorridorLeftTurn
160            }
161            Self::CastleSmallCorridor => FortressPieceKind::CastleSmallCorridor,
162            Self::CastleSmallCorridorRightTurn { .. } => {
163                FortressPieceKind::CastleSmallCorridorRightTurn
164            }
165            Self::CastleStalkRoom => FortressPieceKind::CastleStalkRoom,
166            Self::MonsterThrone { .. } => FortressPieceKind::MonsterThrone,
167            Self::RoomCrossing => FortressPieceKind::RoomCrossing,
168            Self::StairsRoom => FortressPieceKind::StairsRoom,
169        }
170    }
171
172    #[must_use]
173    pub(crate) const fn piece_id(self) -> &'static str {
174        self.kind().piece_id()
175    }
176
177    fn new(kind: FortressPieceKind, rng: &mut LegacyRandom) -> Self {
178        match kind {
179            FortressPieceKind::BridgeCrossing => Self::BridgeCrossing,
180            FortressPieceKind::BridgeEndFiller => Self::BridgeEndFiller {
181                self_seed: rng.next_i32(),
182            },
183            FortressPieceKind::BridgeStraight => Self::BridgeStraight,
184            FortressPieceKind::CastleCorridorStairs => Self::CastleCorridorStairs,
185            FortressPieceKind::CastleCorridorTBalcony => Self::CastleCorridorTBalcony,
186            FortressPieceKind::CastleEntrance => Self::CastleEntrance,
187            FortressPieceKind::CastleSmallCorridorCrossing => Self::CastleSmallCorridorCrossing,
188            FortressPieceKind::CastleSmallCorridorLeftTurn => Self::CastleSmallCorridorLeftTurn {
189                is_needing_chest: rng.next_i32_bounded(3) == 0,
190            },
191            FortressPieceKind::CastleSmallCorridor => Self::CastleSmallCorridor,
192            FortressPieceKind::CastleSmallCorridorRightTurn => Self::CastleSmallCorridorRightTurn {
193                is_needing_chest: rng.next_i32_bounded(3) == 0,
194            },
195            FortressPieceKind::CastleStalkRoom => Self::CastleStalkRoom,
196            FortressPieceKind::MonsterThrone => Self::MonsterThrone {
197                has_placed_spawner: false,
198            },
199            FortressPieceKind::RoomCrossing => Self::RoomCrossing,
200            FortressPieceKind::StairsRoom => Self::StairsRoom,
201        }
202    }
203}
204
205#[derive(Debug, Clone, Copy)]
206struct PieceWeight {
207    kind: FortressPieceKind,
208    weight: i32,
209    max_place_count: i32,
210    allow_in_row: bool,
211    place_count: i32,
212}
213
214impl PieceWeight {
215    const fn new(kind: FortressPieceKind, weight: i32, max: i32, allow_in_row: bool) -> Self {
216        Self {
217            kind,
218            weight,
219            max_place_count: max,
220            allow_in_row,
221            place_count: 0,
222        }
223    }
224
225    const fn do_place(&self) -> bool {
226        self.max_place_count == 0 || self.place_count < self.max_place_count
227    }
228}
229
230fn bridge_weights() -> Vec<PieceWeight> {
231    vec![
232        PieceWeight::new(FortressPieceKind::BridgeStraight, 30, 0, true),
233        PieceWeight::new(FortressPieceKind::BridgeCrossing, 10, 4, false),
234        PieceWeight::new(FortressPieceKind::RoomCrossing, 10, 4, false),
235        PieceWeight::new(FortressPieceKind::StairsRoom, 10, 3, false),
236        PieceWeight::new(FortressPieceKind::MonsterThrone, 5, 2, false),
237        PieceWeight::new(FortressPieceKind::CastleEntrance, 5, 1, false),
238    ]
239}
240
241fn castle_weights() -> Vec<PieceWeight> {
242    vec![
243        PieceWeight::new(FortressPieceKind::CastleSmallCorridor, 25, 0, true),
244        PieceWeight::new(FortressPieceKind::CastleSmallCorridorCrossing, 15, 5, false),
245        PieceWeight::new(
246            FortressPieceKind::CastleSmallCorridorRightTurn,
247            5,
248            10,
249            false,
250        ),
251        PieceWeight::new(FortressPieceKind::CastleSmallCorridorLeftTurn, 5, 10, false),
252        PieceWeight::new(FortressPieceKind::CastleCorridorStairs, 10, 3, true),
253        PieceWeight::new(FortressPieceKind::CastleCorridorTBalcony, 7, 2, false),
254        PieceWeight::new(FortressPieceKind::CastleStalkRoom, 5, 2, false),
255    ]
256}
257
258/// Output piece record.
259#[derive(Debug, Clone, Copy)]
260pub struct FortressPiece {
261    /// Piece-specific state needed for placement and persistence.
262    pub data: FortressPieceData,
263    /// World-space bounding box.
264    pub bounding_box: BoundingBox,
265    /// Piece facing direction.
266    pub orientation: Option<Direction>,
267    /// Generation depth.
268    pub gen_depth: i32,
269}
270
271/// Matches `BoundingBox.orientBox`.
272fn orient_box(foot: IVec3, off: IVec3, size: IVec3, dir: Direction) -> BoundingBox {
273    let s = size - IVec3::ONE;
274    match dir {
275        Direction::South => BoundingBox::new(foot + off, foot + off + s),
276        Direction::North => BoundingBox::new(
277            foot + off + IVec3::new(0, 0, -s.z),
278            foot + off + IVec3::new(s.x, s.y, 0),
279        ),
280        Direction::East => {
281            let b = foot + IVec3::new(off.z, off.y, off.x);
282            BoundingBox::new(b, b + IVec3::new(s.z, s.y, s.x))
283        }
284        Direction::West => {
285            let b = foot + IVec3::new(off.z, off.y, off.x);
286            let rs = IVec3::new(s.z, s.y, s.x);
287            BoundingBox::new(b - IVec3::new(rs.x, 0, 0), b + IVec3::new(0, rs.y, rs.z))
288        }
289        _ => unreachable!("orient_box non-horizontal direction"),
290    }
291}
292
293/// Matches `StructurePiece.makeBoundingBox`: width rotates with the direction axis.
294fn make_bounding_box(
295    pos: IVec3,
296    dir: Direction,
297    width: i32,
298    height: i32,
299    depth: i32,
300) -> BoundingBox {
301    let size = match dir {
302        Direction::North | Direction::South => IVec3::new(width, height, depth),
303        Direction::East | Direction::West => IVec3::new(depth, height, width),
304        _ => unreachable!(),
305    };
306    BoundingBox::new(pos, pos + size - IVec3::ONE)
307}
308
309const fn is_ok_box(bb: &BoundingBox) -> bool {
310    bb.min_y() > LOWEST_Y
311}
312
313fn find_collision<'a>(pieces: &'a [FortressPiece], bb: &BoundingBox) -> Option<&'a FortressPiece> {
314    pieces.iter().find(|p| p.bounding_box.intersects(*bb))
315}
316
317struct Builder {
318    pieces: Vec<FortressPiece>,
319    pending: Vec<FortressPiece>,
320    start_bb_min: IVec3,
321    bridge_weights: Vec<PieceWeight>,
322    castle_weights: Vec<PieceWeight>,
323    previous_kind: Option<FortressPieceKind>,
324}
325
326impl Builder {
327    fn add_and_enqueue(&mut self, piece: FortressPiece) {
328        self.pieces.push(piece);
329        self.pending.push(piece);
330    }
331}
332
333/// Mirrors vanilla's `findAndCreateBridgePieceFactory` + `PIECE.createPiece`.
334fn create_piece(
335    kind: FortressPieceKind,
336    pieces: &[FortressPiece],
337    rng: &mut LegacyRandom,
338    foot: IVec3,
339    dir: Direction,
340    gen_depth: i32,
341) -> Option<FortressPiece> {
342    let (off, size) = kind.geom();
343    let bb = orient_box(foot, off, size, dir);
344    if !is_ok_box(&bb) || find_collision(pieces, &bb).is_some() {
345        return None;
346    }
347    Some(FortressPiece {
348        data: FortressPieceData::new(kind, rng),
349        bounding_box: bb,
350        orientation: Some(dir),
351        gen_depth,
352    })
353}
354
355/// Vanilla's `generatePiece`. Falls back to `BridgeEndFiller` if no weighted pick
356/// succeeds within 5 attempts. On ineligible picks vanilla falls through to
357/// subsequent pieces in the list.
358fn generate_piece_weighted(
359    is_castle: bool,
360    builder: &mut Builder,
361    rng: &mut LegacyRandom,
362    foot: IVec3,
363    dir: Direction,
364    depth: i32,
365) -> Option<FortressPiece> {
366    let total_weight: i32 = {
367        let pool = if is_castle {
368            &builder.castle_weights
369        } else {
370            &builder.bridge_weights
371        };
372        let has_any = pool
373            .iter()
374            .any(|p| p.max_place_count > 0 && p.place_count < p.max_place_count);
375        let sum: i32 = pool.iter().map(|p| p.weight).sum();
376        if has_any { sum } else { -1 }
377    };
378
379    if total_weight > 0 && depth <= MAX_DEPTH {
380        for _ in 0..5 {
381            let mut choice = rng.next_i32_bounded(total_weight);
382            let mut i = 0;
383            loop {
384                let (kind, allow_in_row, do_place) = {
385                    let pool = if is_castle {
386                        &builder.castle_weights
387                    } else {
388                        &builder.bridge_weights
389                    };
390                    if i >= pool.len() {
391                        break;
392                    }
393                    choice -= pool[i].weight;
394                    (pool[i].kind, pool[i].allow_in_row, pool[i].do_place())
395                };
396                if choice >= 0 {
397                    i += 1;
398                    continue;
399                }
400                if !do_place || (Some(kind) == builder.previous_kind && !allow_in_row) {
401                    break;
402                }
403                if let Some(p) = create_piece(kind, &builder.pieces, rng, foot, dir, depth) {
404                    let pool = if is_castle {
405                        &mut builder.castle_weights
406                    } else {
407                        &mut builder.bridge_weights
408                    };
409                    pool[i].place_count += 1;
410                    builder.previous_kind = Some(kind);
411                    if !pool[i].do_place() {
412                        pool.remove(i);
413                    }
414                    return Some(p);
415                }
416                i += 1;
417            }
418        }
419    }
420
421    create_piece(
422        FortressPieceKind::BridgeEndFiller,
423        &builder.pieces,
424        rng,
425        foot,
426        dir,
427        depth,
428    )
429}
430
431/// Out-of-range branch builds a `BridgeEndFiller` (consuming RNG for `selfSeed`)
432/// then discards it. We mirror vanilla: call `create_piece` for RNG sync, don't add.
433fn generate_and_add_piece(
434    is_castle: bool,
435    builder: &mut Builder,
436    rng: &mut LegacyRandom,
437    foot: IVec3,
438    dir: Direction,
439    depth: i32,
440) {
441    if (foot.x - builder.start_bb_min.x).abs() > DIST_LIMIT
442        || (foot.z - builder.start_bb_min.z).abs() > DIST_LIMIT
443    {
444        let _ = create_piece(
445            FortressPieceKind::BridgeEndFiller,
446            &builder.pieces,
447            rng,
448            foot,
449            dir,
450            depth,
451        );
452        return;
453    }
454    if let Some(piece) = generate_piece_weighted(is_castle, builder, rng, foot, dir, depth + 1) {
455        builder.add_and_enqueue(piece);
456    }
457}
458
459/// Parent context threaded through `generate_child_*`.
460#[derive(Clone, Copy)]
461struct ParentRef {
462    bb: BoundingBox,
463    orientation: Direction,
464    gen_depth: i32,
465}
466
467fn generate_child_forward(
468    parent: ParentRef,
469    builder: &mut Builder,
470    rng: &mut LegacyRandom,
471    x_off: i32,
472    y_off: i32,
473    is_castle: bool,
474) {
475    let bb = parent.bb;
476    let y = bb.min_y() + y_off;
477    let foot = match parent.orientation {
478        Direction::North => IVec3::new(bb.min_x() + x_off, y, bb.min_z() - 1),
479        Direction::South => IVec3::new(bb.min_x() + x_off, y, bb.max_z() + 1),
480        Direction::West => IVec3::new(bb.min_x() - 1, y, bb.min_z() + x_off),
481        Direction::East => IVec3::new(bb.max_x() + 1, y, bb.min_z() + x_off),
482        _ => return,
483    };
484    generate_and_add_piece(
485        is_castle,
486        builder,
487        rng,
488        foot,
489        parent.orientation,
490        parent.gen_depth,
491    );
492}
493
494fn generate_child_left(
495    parent: ParentRef,
496    builder: &mut Builder,
497    rng: &mut LegacyRandom,
498    y_off: i32,
499    z_off: i32,
500    is_castle: bool,
501) {
502    let bb = parent.bb;
503    let (foot, dir) = match parent.orientation {
504        Direction::North | Direction::South => (
505            IVec3::new(bb.min_x() - 1, bb.min_y() + y_off, bb.min_z() + z_off),
506            Direction::West,
507        ),
508        Direction::West | Direction::East => (
509            IVec3::new(bb.min_x() + z_off, bb.min_y() + y_off, bb.min_z() - 1),
510            Direction::North,
511        ),
512        _ => return,
513    };
514    generate_and_add_piece(is_castle, builder, rng, foot, dir, parent.gen_depth);
515}
516
517fn generate_child_right(
518    parent: ParentRef,
519    builder: &mut Builder,
520    rng: &mut LegacyRandom,
521    y_off: i32,
522    z_off: i32,
523    is_castle: bool,
524) {
525    let bb = parent.bb;
526    let (foot, dir) = match parent.orientation {
527        Direction::North | Direction::South => (
528            IVec3::new(bb.max_x() + 1, bb.min_y() + y_off, bb.min_z() + z_off),
529            Direction::East,
530        ),
531        Direction::West | Direction::East => (
532            IVec3::new(bb.min_x() + z_off, bb.min_y() + y_off, bb.max_z() + 1),
533            Direction::South,
534        ),
535        _ => return,
536    };
537    generate_and_add_piece(is_castle, builder, rng, foot, dir, parent.gen_depth);
538}
539
540fn add_children(piece: FortressPiece, builder: &mut Builder, rng: &mut LegacyRandom) {
541    let Some(orientation) = piece.orientation else {
542        return;
543    };
544    let parent = ParentRef {
545        bb: piece.bounding_box,
546        orientation,
547        gen_depth: piece.gen_depth,
548    };
549    match piece.data.kind() {
550        FortressPieceKind::BridgeCrossing => {
551            generate_child_forward(parent, builder, rng, 8, 3, false);
552            generate_child_left(parent, builder, rng, 3, 8, false);
553            generate_child_right(parent, builder, rng, 3, 8, false);
554        }
555        FortressPieceKind::BridgeStraight => {
556            generate_child_forward(parent, builder, rng, 1, 3, false);
557        }
558        FortressPieceKind::CastleCorridorStairs | FortressPieceKind::CastleSmallCorridor => {
559            generate_child_forward(parent, builder, rng, 1, 0, true);
560        }
561        FortressPieceKind::CastleCorridorTBalcony => {
562            let z_off = match orientation {
563                Direction::West | Direction::North => 5,
564                _ => 1,
565            };
566            let l = rng.next_i32_bounded(8) > 0;
567            generate_child_left(parent, builder, rng, 0, z_off, l);
568            let r = rng.next_i32_bounded(8) > 0;
569            generate_child_right(parent, builder, rng, 0, z_off, r);
570        }
571        FortressPieceKind::CastleEntrance => {
572            generate_child_forward(parent, builder, rng, 5, 3, true);
573        }
574        FortressPieceKind::CastleSmallCorridorCrossing => {
575            generate_child_forward(parent, builder, rng, 1, 0, true);
576            generate_child_left(parent, builder, rng, 0, 1, true);
577            generate_child_right(parent, builder, rng, 0, 1, true);
578        }
579        FortressPieceKind::CastleSmallCorridorLeftTurn => {
580            generate_child_left(parent, builder, rng, 0, 1, true);
581        }
582        FortressPieceKind::CastleSmallCorridorRightTurn => {
583            generate_child_right(parent, builder, rng, 0, 1, true);
584        }
585        FortressPieceKind::CastleStalkRoom => {
586            generate_child_forward(parent, builder, rng, 5, 3, true);
587            generate_child_forward(parent, builder, rng, 5, 11, true);
588        }
589        FortressPieceKind::RoomCrossing => {
590            generate_child_forward(parent, builder, rng, 2, 0, false);
591            generate_child_left(parent, builder, rng, 0, 2, false);
592            generate_child_right(parent, builder, rng, 0, 2, false);
593        }
594        FortressPieceKind::StairsRoom => {
595            generate_child_right(parent, builder, rng, 6, 2, false);
596        }
597        // MonsterThrone, BridgeEndFiller: leaves.
598        FortressPieceKind::MonsterThrone | FortressPieceKind::BridgeEndFiller => {}
599    }
600}
601
602fn overall_bb(pieces: &[FortressPiece]) -> BoundingBox {
603    let mut bb = pieces[0].bounding_box;
604    for p in &pieces[1..] {
605        bb = BoundingBox::encapsulating(&bb, &p.bounding_box);
606    }
607    bb
608}
609
610fn move_inside_heights(
611    pieces: &mut [FortressPiece],
612    rng: &mut LegacyRandom,
613    lowest_allowed: i32,
614    highest_allowed: i32,
615) {
616    if pieces.is_empty() {
617        return;
618    }
619    let bb = overall_bb(pieces);
620    let height_span = highest_allowed - lowest_allowed + 1 - bb.height();
621    let y0 = if height_span > 1 {
622        lowest_allowed + rng.next_i32_bounded(height_span)
623    } else {
624        lowest_allowed
625    };
626    let dy = y0 - bb.min_y();
627    if dy == 0 {
628        return;
629    }
630    let delta = IVec3::new(0, dy, 0);
631    for p in pieces {
632        p.bounding_box = p.bounding_box.translate(delta);
633    }
634}
635
636/// All fortress pieces for the chunk, vertically offset into `Y ∈ [48, 70]`.
637pub fn generate_fortress_pieces(
638    chunk_x: i32,
639    chunk_z: i32,
640    rng: &mut LegacyRandom,
641) -> Vec<FortressPiece> {
642    let start_dir = HORIZONTAL_ORDER[rng.next_i32_bounded(4) as usize];
643    let west = (chunk_x << 4) + START_X_OFFSET;
644    let north = (chunk_z << 4) + START_Z_OFFSET;
645    let start_bb = make_bounding_box(
646        IVec3::new(west, MAGIC_START_Y, north),
647        start_dir,
648        19,
649        10,
650        19,
651    );
652    let start_piece = FortressPiece {
653        data: FortressPieceData::BridgeCrossing,
654        bounding_box: start_bb,
655        orientation: Some(start_dir),
656        gen_depth: 0,
657    };
658
659    let mut builder = Builder {
660        pieces: vec![start_piece],
661        pending: Vec::new(),
662        start_bb_min: start_bb.min_corner(),
663        bridge_weights: bridge_weights(),
664        castle_weights: castle_weights(),
665        previous_kind: None,
666    };
667
668    add_children(start_piece, &mut builder, rng);
669    while !builder.pending.is_empty() {
670        let pos = rng.next_i32_bounded(builder.pending.len() as i32) as usize;
671        let pending = builder.pending.remove(pos);
672        add_children(pending, &mut builder, rng);
673    }
674
675    move_inside_heights(&mut builder.pieces, rng, Y_LOW_ALLOWED, Y_HIGH_ALLOWED);
676    builder.pieces
677}
678
679/// Registered under `"minecraft:fortress"`. Shares the `nether_complexes` set with
680/// `bastion_remnant` (jigsaw), so it's dispatched from the jigsaw arm's fallthrough.
681pub struct NetherFortressStructure;
682
683impl Structure for NetherFortressStructure {
684    fn find_generation_point(
685        &self,
686        ctx: &mut dyn StructureGenerationContext,
687        structure: &StructureData,
688        rng: &mut LegacyRandom,
689    ) -> Option<GenerationStub> {
690        // Biome check at (chunkMinX, 64, chunkMinZ) per vanilla.
691        let (biome_x, biome_z) = (ctx.chunk_min_x(), ctx.chunk_min_z());
692        let biome = ctx.biome_at(biome_x, 64, biome_z);
693        if !structure.allowed_biomes.contains(&biome.key) {
694            return None;
695        }
696
697        let pieces_out = generate_fortress_pieces(ctx.chunk_x(), ctx.chunk_z(), rng);
698        if pieces_out.is_empty() {
699            return None;
700        }
701
702        Some(GenerationStub {
703            position: (biome_x, 64, biome_z),
704            pieces: pieces_out
705                .into_iter()
706                .map(|p| StructurePiece {
707                    piece_type: Identifier::new_static("minecraft", p.data.piece_id()),
708                    bounding_box: p.bounding_box,
709                    gen_depth: p.gen_depth,
710                    orientation: p.orientation,
711                    payload: StructurePiecePayload::Procedural(
712                        ProceduralPieceData::NetherFortress(p.data),
713                    ),
714                    ground_level_delta: 0,
715                    junctions: Vec::new(),
716                    projection: None,
717                })
718                .collect(),
719        })
720    }
721}
722
723#[cfg(test)]
724mod tests {
725    use super::*;
726
727    #[test]
728    fn fortress_constructor_rng_state_is_captured_in_piece_payloads() {
729        let mut expected = LegacyRandom::from_seed(1234);
730        let expected_self_seed = expected.next_i32();
731        let mut rng = LegacyRandom::from_seed(1234);
732        let filler = create_piece(
733            FortressPieceKind::BridgeEndFiller,
734            &[],
735            &mut rng,
736            IVec3::new(0, 64, 0),
737            Direction::South,
738            1,
739        )
740        .expect("bridge end filler should fit");
741        assert_eq!(
742            filler.data,
743            FortressPieceData::BridgeEndFiller {
744                self_seed: expected_self_seed,
745            }
746        );
747        assert_eq!(rng.next_i32(), expected.next_i32());
748
749        let mut expected = LegacyRandom::from_seed(5678);
750        let expected_needs_chest = expected.next_i32_bounded(3) == 0;
751        let mut rng = LegacyRandom::from_seed(5678);
752        let turn = create_piece(
753            FortressPieceKind::CastleSmallCorridorLeftTurn,
754            &[],
755            &mut rng,
756            IVec3::new(0, 64, 0),
757            Direction::South,
758            1,
759        )
760        .expect("small corridor turn should fit");
761        assert_eq!(
762            turn.data,
763            FortressPieceData::CastleSmallCorridorLeftTurn {
764                is_needing_chest: expected_needs_chest,
765            }
766        );
767        assert_eq!(rng.next_i32(), expected.next_i32());
768    }
769}