Skip to main content

steel_worldgen/structure/
mineshaft.rs

1//! Mineshaft. Vanilla's `MineshaftPieces` DFS: each child's `addChildren` runs
2//! immediately after creation, before processing the next sibling.
3
4use glam::IVec3;
5use steel_registry::structure::{MineshaftTypeData, StructureConfigData, 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 = 8;
16const MAX_DISTANCE: i32 = 80;
17
18/// Mineshaft variant type.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum MineshaftType {
21    /// Standard mineshaft with oak wood.
22    Normal,
23    /// Badlands mineshaft with dark oak wood, positioned higher.
24    Mesa,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28enum Dir {
29    North,
30    South,
31    West,
32    East,
33}
34
35impl Dir {
36    const fn to_vanilla(self) -> Direction {
37        match self {
38            Dir::North => Direction::North,
39            Dir::South => Direction::South,
40            Dir::West => Direction::West,
41            Dir::East => Direction::East,
42        }
43    }
44}
45
46/// Mineshaft piece kind — produced by the DFS and mapped back to vanilla's
47/// `StructurePieceType` registry IDs for save-format parity.
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum PieceType {
50    /// Start room (one per mineshaft).
51    Room,
52    /// Horizontal corridor segment.
53    Corridor,
54    /// Corridor crossing (possibly two-floored).
55    Crossing,
56    /// Stair segment.
57    Stairs,
58}
59
60impl PieceType {
61    /// Vanilla save-format identifier (lowercased `MSRoom` → `msroom`, etc.).
62    #[must_use]
63    pub const fn piece_id(self) -> &'static str {
64        match self {
65            Self::Room => "msroom",
66            Self::Corridor => "mscorridor",
67            Self::Crossing => "mscrossing",
68            Self::Stairs => "msstairs",
69        }
70    }
71}
72
73struct PieceInfo {
74    bb: BoundingBox,
75    kind: MineshaftPieceKind,
76    gen_depth: i32,
77    dir: Option<Dir>,
78}
79
80struct Pieces {
81    bbs: Vec<BoundingBox>,
82    infos: Vec<PieceInfo>,
83    start_bb: BoundingBox,
84    room_child_entrance_boxes: Vec<BoundingBox>,
85}
86
87impl Pieces {
88    fn has_collision(&self, bb: &BoundingBox) -> bool {
89        self.bbs.iter().any(|b| b.intersects(*bb))
90    }
91}
92
93/// One mineshaft piece in the generated tree.
94pub struct MineshaftPieceData {
95    /// Family-specific placement state.
96    pub payload: MineshaftPiecePayload,
97    /// World-space bounding box, already offset to the final Y position.
98    pub bounding_box: BoundingBox,
99    /// Distance from the start room in the DFS tree (vanilla's `genDepth`).
100    pub gen_depth: i32,
101    /// Vanilla `setOrientation` value: `Some` for corridors and stairs, `None`
102    /// for the start room and crossings.
103    pub orientation: Option<Direction>,
104}
105
106/// Placement payload shared by mineshaft start generation, persistence, and
107/// feature-stage procedural placement.
108#[derive(Debug, Clone, PartialEq)]
109pub struct MineshaftPiecePayload {
110    /// Normal oak or mesa dark-oak mineshaft.
111    pub mineshaft_type: MineshaftType,
112    /// Piece-specific vanilla placement state.
113    pub kind: MineshaftPieceKind,
114}
115
116impl MineshaftPiecePayload {
117    /// Structure piece registry id for the payload kind.
118    #[must_use]
119    pub const fn piece_id(&self) -> &'static str {
120        self.kind.piece_type().piece_id()
121    }
122}
123
124/// Piece-specific mineshaft placement state.
125#[derive(Debug, Clone, PartialEq)]
126pub enum MineshaftPieceKind {
127    /// Start room with the child entrance openings stored by vanilla.
128    Room {
129        /// Child entrance boxes, offset with the room during Y adjustment.
130        child_entrance_boxes: Vec<BoundingBox>,
131    },
132    /// Horizontal corridor segment.
133    Corridor {
134        /// Whether rails can generate through this corridor.
135        has_rails: bool,
136        /// Whether this is a cobweb-heavy cave-spider corridor.
137        spider_corridor: bool,
138        /// Vanilla mutable flag preventing duplicate cave-spider spawners.
139        has_placed_spider: bool,
140        /// Number of five-block corridor sections.
141        num_sections: i32,
142    },
143    /// Corridor crossing.
144    Crossing {
145        /// Direction stored in vanilla's `"D"` field.
146        direction: Direction,
147        /// Whether the crossing has the upper floor.
148        is_two_floored: bool,
149    },
150    /// Stair segment.
151    Stairs,
152}
153
154impl MineshaftPieceKind {
155    /// Vanilla structure piece type represented by this payload.
156    #[must_use]
157    pub const fn piece_type(&self) -> PieceType {
158        match self {
159            Self::Room { .. } => PieceType::Room,
160            Self::Corridor { .. } => PieceType::Corridor,
161            Self::Crossing { .. } => PieceType::Crossing,
162            Self::Stairs => PieceType::Stairs,
163        }
164    }
165}
166
167/// Result of mineshaft generation.
168pub struct MineshaftResult {
169    /// Biome check position `(block_x, block_y, block_z)`.
170    pub biome_check_pos: (i32, i32, i32),
171    /// Pieces in DFS order, offset to final Y position.
172    pub pieces: Vec<MineshaftPieceData>,
173}
174
175/// Generates mineshaft pieces and returns the biome check position + piece data.
176pub fn find_generation_point(
177    rng: &mut LegacyRandom,
178    chunk_x: i32,
179    chunk_z: i32,
180    mtype: MineshaftType,
181    sea_level: i32,
182    min_y: i32,
183    get_surface_height: &mut dyn FnMut(i32, i32) -> i32,
184) -> MineshaftResult {
185    rng.next_f64();
186
187    let middle_x = chunk_x * 16 + 8;
188    let min_z = chunk_z * 16;
189    let room_bb = create_room_bb(rng, chunk_x * 16 + 2, chunk_z * 16 + 2);
190
191    let mut pieces = Pieces {
192        bbs: vec![room_bb],
193        infos: vec![PieceInfo {
194            bb: room_bb,
195            kind: MineshaftPieceKind::Room {
196                child_entrance_boxes: Vec::new(),
197            },
198            gen_depth: 0,
199            dir: None,
200        }],
201        start_bb: room_bb,
202        room_child_entrance_boxes: Vec::new(),
203    };
204    room_add_children(&mut pieces, rng, room_bb);
205
206    let mut overall = pieces.bbs[0];
207    for bb in &pieces.bbs[1..] {
208        overall = union_bb(overall, *bb);
209    }
210
211    let y_offset = if mtype == MineshaftType::Mesa {
212        let center_x = overall.min_x() + (overall.max_x() - overall.min_x() + 1) / 2;
213        let center_z = overall.min_z() + (overall.max_z() - overall.min_z() + 1) / 2;
214        let surface_height = get_surface_height(center_x, center_z);
215        let target = if surface_height <= sea_level {
216            sea_level
217        } else {
218            rng.next_i32_between(sea_level, surface_height)
219        };
220        let center_y = overall.min_y() + (overall.max_y() - overall.min_y() + 1) / 2;
221        target - center_y
222    } else {
223        let max_y = sea_level - 10;
224        let mut y1_pos = (overall.max_y() - overall.min_y() + 1) + min_y + 1;
225        if y1_pos < max_y {
226            y1_pos += rng.next_i32_bounded(max_y - y1_pos);
227        }
228        y1_pos - overall.max_y()
229    };
230
231    MineshaftResult {
232        biome_check_pos: (middle_x, 50 + y_offset, min_z),
233        pieces: pieces
234            .infos
235            .iter()
236            .map(|info| MineshaftPieceData {
237                payload: MineshaftPiecePayload {
238                    mineshaft_type: mtype,
239                    kind: offset_piece_kind(
240                        &info.kind,
241                        &pieces.room_child_entrance_boxes,
242                        y_offset,
243                    ),
244                },
245                bounding_box: BoundingBox::new(
246                    info.bb.min_corner() + IVec3::new(0, y_offset, 0),
247                    info.bb.max_corner() + IVec3::new(0, y_offset, 0),
248                ),
249                gen_depth: info.gen_depth,
250                orientation: info.dir.map(Dir::to_vanilla),
251            })
252            .collect(),
253    }
254}
255
256fn offset_piece_kind(
257    kind: &MineshaftPieceKind,
258    room_child_entrance_boxes: &[BoundingBox],
259    y_offset: i32,
260) -> MineshaftPieceKind {
261    match kind {
262        MineshaftPieceKind::Room { .. } => MineshaftPieceKind::Room {
263            child_entrance_boxes: room_child_entrance_boxes
264                .iter()
265                .map(|bb| {
266                    BoundingBox::new(
267                        bb.min_corner() + IVec3::new(0, y_offset, 0),
268                        bb.max_corner() + IVec3::new(0, y_offset, 0),
269                    )
270                })
271                .collect(),
272        },
273        other => other.clone(),
274    }
275}
276
277fn create_room_bb(rng: &mut LegacyRandom, west: i32, north: i32) -> BoundingBox {
278    BoundingBox::new(
279        IVec3::new(west, 50, north),
280        IVec3::new(
281            west + 7 + rng.next_i32_bounded(6),
282            54 + rng.next_i32_bounded(6),
283            north + 7 + rng.next_i32_bounded(6),
284        ),
285    )
286}
287
288fn room_add_children(pieces: &mut Pieces, rng: &mut LegacyRandom, bb: BoundingBox) {
289    let x_span = bb.max_x() - bb.min_x() + 1;
290    let z_span = bb.max_z() - bb.min_z() + 1;
291    let height_space = ((bb.max_y() - bb.min_y() + 1) - 3 - 1).max(1);
292
293    for (dir, span) in [
294        (Dir::North, x_span),
295        (Dir::South, x_span),
296        (Dir::West, z_span),
297        (Dir::East, z_span),
298    ] {
299        let mut pos = 0;
300        while pos < span {
301            pos += rng.next_i32_bounded(span);
302            if pos + 3 > span {
303                break;
304            }
305            let fy = bb.min_y() + rng.next_i32_bounded(height_space) + 1;
306            let (fx, fz) = match dir {
307                Dir::North => (bb.min_x() + pos, bb.min_z() - 1),
308                Dir::South => (bb.min_x() + pos, bb.max_z() + 1),
309                Dir::West => (bb.min_x() - 1, bb.min_z() + pos),
310                Dir::East => (bb.max_x() + 1, bb.min_z() + pos),
311            };
312            if let Some(child_bb) = generate_and_add(pieces, rng, fx, fy, fz, dir, 0) {
313                let entrance = match dir {
314                    Dir::North => BoundingBox::new(
315                        IVec3::new(child_bb.min_x(), child_bb.min_y(), bb.min_z()),
316                        IVec3::new(child_bb.max_x(), child_bb.max_y(), bb.min_z() + 1),
317                    ),
318                    Dir::South => BoundingBox::new(
319                        IVec3::new(child_bb.min_x(), child_bb.min_y(), bb.max_z() - 1),
320                        IVec3::new(child_bb.max_x(), child_bb.max_y(), bb.max_z()),
321                    ),
322                    Dir::West => BoundingBox::new(
323                        IVec3::new(bb.min_x(), child_bb.min_y(), child_bb.min_z()),
324                        IVec3::new(bb.min_x() + 1, child_bb.max_y(), child_bb.max_z()),
325                    ),
326                    Dir::East => BoundingBox::new(
327                        IVec3::new(bb.max_x() - 1, child_bb.min_y(), child_bb.min_z()),
328                        IVec3::new(bb.max_x(), child_bb.max_y(), child_bb.max_z()),
329                    ),
330                };
331                pieces.room_child_entrance_boxes.push(entrance);
332            }
333            pos += 4;
334        }
335    }
336}
337
338fn generate_and_add(
339    pieces: &mut Pieces,
340    rng: &mut LegacyRandom,
341    foot_x: i32,
342    foot_y: i32,
343    foot_z: i32,
344    dir: Dir,
345    depth: i32,
346) -> Option<BoundingBox> {
347    if depth > MAX_DEPTH
348        || (foot_x - pieces.start_bb.min_x()).abs() > MAX_DISTANCE
349        || (foot_z - pieces.start_bb.min_z()).abs() > MAX_DISTANCE
350    {
351        return None;
352    }
353    let roll = rng.next_i32_bounded(100);
354    if roll >= 80 {
355        try_add_crossing(pieces, rng, foot_x, foot_y, foot_z, dir, depth + 1)
356    } else if roll >= 70 {
357        try_add_stairs(pieces, rng, foot_x, foot_y, foot_z, dir, depth + 1)
358    } else {
359        try_add_corridor(pieces, rng, foot_x, foot_y, foot_z, dir, depth + 1)
360    }
361}
362
363fn push_piece(
364    pieces: &mut Pieces,
365    bb: BoundingBox,
366    kind: MineshaftPieceKind,
367    gen_depth: i32,
368    dir: Dir,
369) {
370    pieces.bbs.push(bb);
371    let saved_dir = match &kind {
372        MineshaftPieceKind::Corridor { .. } | MineshaftPieceKind::Stairs => Some(dir),
373        MineshaftPieceKind::Room { .. } | MineshaftPieceKind::Crossing { .. } => None,
374    };
375    pieces.infos.push(PieceInfo {
376        bb,
377        kind,
378        gen_depth,
379        dir: saved_dir,
380    });
381}
382
383const fn corridor_num_sections(bb: BoundingBox, dir: Dir) -> i32 {
384    match dir {
385        Dir::North | Dir::South => (bb.max_z() - bb.min_z() + 1) / 5,
386        Dir::West | Dir::East => (bb.max_x() - bb.min_x() + 1) / 5,
387    }
388}
389
390fn corridor_payload(bb: BoundingBox, dir: Dir, rng: &mut LegacyRandom) -> MineshaftPieceKind {
391    let has_rails = rng.next_i32_bounded(3) == 0;
392    let spider_corridor = !has_rails && rng.next_i32_bounded(23) == 0;
393    MineshaftPieceKind::Corridor {
394        has_rails,
395        spider_corridor,
396        has_placed_spider: false,
397        num_sections: corridor_num_sections(bb, dir),
398    }
399}
400
401const fn crossing_payload(dir: Dir, is_two_floored: bool) -> MineshaftPieceKind {
402    MineshaftPieceKind::Crossing {
403        direction: dir.to_vanilla(),
404        is_two_floored,
405    }
406}
407
408const fn stairs_payload() -> MineshaftPieceKind {
409    MineshaftPieceKind::Stairs
410}
411
412fn try_add_corridor(
413    pieces: &mut Pieces,
414    rng: &mut LegacyRandom,
415    foot_x: i32,
416    foot_y: i32,
417    foot_z: i32,
418    dir: Dir,
419    gen_depth: i32,
420) -> Option<BoundingBox> {
421    let mut corridor_length = rng.next_i32_bounded(3) + 2;
422    while corridor_length > 0 {
423        let block_length = corridor_length * 5;
424        let bb = move_bb(
425            match dir {
426                Dir::North => {
427                    BoundingBox::new(IVec3::new(0, 0, -(block_length - 1)), IVec3::new(2, 2, 0))
428                }
429                Dir::South => {
430                    BoundingBox::new(IVec3::new(0, 0, 0), IVec3::new(2, 2, block_length - 1))
431                }
432                Dir::West => {
433                    BoundingBox::new(IVec3::new(-(block_length - 1), 0, 0), IVec3::new(0, 2, 2))
434                }
435                Dir::East => {
436                    BoundingBox::new(IVec3::new(0, 0, 0), IVec3::new(block_length - 1, 2, 2))
437                }
438            },
439            foot_x,
440            foot_y,
441            foot_z,
442        );
443        if !pieces.has_collision(&bb) {
444            let kind = corridor_payload(bb, dir, rng);
445            push_piece(pieces, bb, kind, gen_depth, dir);
446            corridor_add_children(pieces, rng, bb, dir, gen_depth);
447            return Some(bb);
448        }
449        corridor_length -= 1;
450    }
451    None
452}
453
454fn try_add_crossing(
455    pieces: &mut Pieces,
456    rng: &mut LegacyRandom,
457    foot_x: i32,
458    foot_y: i32,
459    foot_z: i32,
460    dir: Dir,
461    gen_depth: i32,
462) -> Option<BoundingBox> {
463    let is_two_floored = rng.next_i32_bounded(4) == 0;
464    let y1 = if is_two_floored { 6 } else { 2 };
465    let bb = move_bb(
466        match dir {
467            Dir::North => BoundingBox::new(IVec3::new(-1, 0, -4), IVec3::new(3, y1, 0)),
468            Dir::South => BoundingBox::new(IVec3::new(-1, 0, 0), IVec3::new(3, y1, 4)),
469            Dir::West => BoundingBox::new(IVec3::new(-4, 0, -1), IVec3::new(0, y1, 3)),
470            Dir::East => BoundingBox::new(IVec3::new(0, 0, -1), IVec3::new(4, y1, 3)),
471        },
472        foot_x,
473        foot_y,
474        foot_z,
475    );
476    if pieces.has_collision(&bb) {
477        return None;
478    }
479    push_piece(
480        pieces,
481        bb,
482        crossing_payload(dir, is_two_floored),
483        gen_depth,
484        dir,
485    );
486    crossing_add_children(pieces, rng, bb, dir, gen_depth, is_two_floored);
487    Some(bb)
488}
489
490fn try_add_stairs(
491    pieces: &mut Pieces,
492    rng: &mut LegacyRandom,
493    foot_x: i32,
494    foot_y: i32,
495    foot_z: i32,
496    dir: Dir,
497    gen_depth: i32,
498) -> Option<BoundingBox> {
499    let bb = move_bb(
500        match dir {
501            Dir::North => BoundingBox::new(IVec3::new(0, -5, -8), IVec3::new(2, 2, 0)),
502            Dir::South => BoundingBox::new(IVec3::new(0, -5, 0), IVec3::new(2, 2, 8)),
503            Dir::West => BoundingBox::new(IVec3::new(-8, -5, 0), IVec3::new(0, 2, 2)),
504            Dir::East => BoundingBox::new(IVec3::new(0, -5, 0), IVec3::new(8, 2, 2)),
505        },
506        foot_x,
507        foot_y,
508        foot_z,
509    );
510    if pieces.has_collision(&bb) {
511        return None;
512    }
513    push_piece(pieces, bb, stairs_payload(), gen_depth, dir);
514    stairs_add_children(pieces, rng, bb, dir, gen_depth);
515    Some(bb)
516}
517
518fn corridor_add_children(
519    pieces: &mut Pieces,
520    rng: &mut LegacyRandom,
521    bb: BoundingBox,
522    dir: Dir,
523    depth: i32,
524) {
525    let end_selection = rng.next_i32_bounded(4);
526    let fy = bb.min_y() - 1 + rng.next_i32_bounded(3);
527    #[expect(
528        clippy::match_same_arms,
529        reason = "arms kept per-direction to mirror vanilla's switch dispatch"
530    )]
531    let (fx, fz, d) = match (dir, end_selection) {
532        (Dir::North, 0 | 1) => (bb.min_x(), bb.min_z() - 1, Dir::North),
533        (Dir::North, 2) => (bb.min_x() - 1, bb.min_z(), Dir::West),
534        (Dir::North, _) => (bb.max_x() + 1, bb.min_z(), Dir::East),
535        (Dir::South, 0 | 1) => (bb.min_x(), bb.max_z() + 1, Dir::South),
536        (Dir::South, 2) => (bb.min_x() - 1, bb.max_z() - 3, Dir::West),
537        (Dir::South, _) => (bb.max_x() + 1, bb.max_z() - 3, Dir::East),
538        (Dir::West, 0 | 1) => (bb.min_x() - 1, bb.min_z(), Dir::West),
539        (Dir::West, 2) => (bb.min_x(), bb.min_z() - 1, Dir::North),
540        (Dir::West, _) => (bb.min_x(), bb.max_z() + 1, Dir::South),
541        (Dir::East, 0 | 1) => (bb.max_x() + 1, bb.min_z(), Dir::East),
542        (Dir::East, 2) => (bb.max_x() - 3, bb.min_z() - 1, Dir::North),
543        (Dir::East, _) => (bb.max_x() - 3, bb.max_z() + 1, Dir::South),
544    };
545    let _ = generate_and_add(pieces, rng, fx, fy, fz, d, depth);
546
547    if depth >= MAX_DEPTH {
548        return;
549    }
550    match dir {
551        Dir::North | Dir::South => {
552            let mut z = bb.min_z() + 3;
553            while z + 3 <= bb.max_z() {
554                match rng.next_i32_bounded(5) {
555                    0 => {
556                        let _ = generate_and_add(
557                            pieces,
558                            rng,
559                            bb.min_x() - 1,
560                            bb.min_y(),
561                            z,
562                            Dir::West,
563                            depth + 1,
564                        );
565                    }
566                    1 => {
567                        let _ = generate_and_add(
568                            pieces,
569                            rng,
570                            bb.max_x() + 1,
571                            bb.min_y(),
572                            z,
573                            Dir::East,
574                            depth + 1,
575                        );
576                    }
577                    _ => {}
578                }
579                z += 5;
580            }
581        }
582        Dir::West | Dir::East => {
583            let mut x = bb.min_x() + 3;
584            while x + 3 <= bb.max_x() {
585                match rng.next_i32_bounded(5) {
586                    0 => {
587                        let _ = generate_and_add(
588                            pieces,
589                            rng,
590                            x,
591                            bb.min_y(),
592                            bb.min_z() - 1,
593                            Dir::North,
594                            depth + 1,
595                        );
596                    }
597                    1 => {
598                        let _ = generate_and_add(
599                            pieces,
600                            rng,
601                            x,
602                            bb.min_y(),
603                            bb.max_z() + 1,
604                            Dir::South,
605                            depth + 1,
606                        );
607                    }
608                    _ => {}
609                }
610                x += 5;
611            }
612        }
613    }
614}
615
616fn crossing_add_children(
617    pieces: &mut Pieces,
618    rng: &mut LegacyRandom,
619    bb: BoundingBox,
620    dir: Dir,
621    depth: i32,
622    is_two_floored: bool,
623) {
624    let outs: [(i32, i32, Dir); 3] = match dir {
625        Dir::North => [
626            (bb.min_x() + 1, bb.min_z() - 1, Dir::North),
627            (bb.min_x() - 1, bb.min_z() + 1, Dir::West),
628            (bb.max_x() + 1, bb.min_z() + 1, Dir::East),
629        ],
630        Dir::South => [
631            (bb.min_x() + 1, bb.max_z() + 1, Dir::South),
632            (bb.min_x() - 1, bb.min_z() + 1, Dir::West),
633            (bb.max_x() + 1, bb.min_z() + 1, Dir::East),
634        ],
635        Dir::West => [
636            (bb.min_x() + 1, bb.min_z() - 1, Dir::North),
637            (bb.min_x() + 1, bb.max_z() + 1, Dir::South),
638            (bb.min_x() - 1, bb.min_z() + 1, Dir::West),
639        ],
640        Dir::East => [
641            (bb.min_x() + 1, bb.min_z() - 1, Dir::North),
642            (bb.min_x() + 1, bb.max_z() + 1, Dir::South),
643            (bb.max_x() + 1, bb.min_z() + 1, Dir::East),
644        ],
645    };
646    for (x, z, d) in outs {
647        let _ = generate_and_add(pieces, rng, x, bb.min_y(), z, d, depth);
648    }
649
650    if is_two_floored {
651        for (x, z, d) in [
652            (bb.min_x() + 1, bb.min_z() - 1, Dir::North),
653            (bb.min_x() - 1, bb.min_z() + 1, Dir::West),
654            (bb.max_x() + 1, bb.min_z() + 1, Dir::East),
655            (bb.min_x() + 1, bb.max_z() + 1, Dir::South),
656        ] {
657            if rng.next_bool() {
658                let _ = generate_and_add(pieces, rng, x, bb.min_y() + 4, z, d, depth);
659            }
660        }
661    }
662}
663
664fn stairs_add_children(
665    pieces: &mut Pieces,
666    rng: &mut LegacyRandom,
667    bb: BoundingBox,
668    dir: Dir,
669    depth: i32,
670) {
671    let (x, z) = match dir {
672        Dir::North => (bb.min_x(), bb.min_z() - 1),
673        Dir::South => (bb.min_x(), bb.max_z() + 1),
674        Dir::West => (bb.min_x() - 1, bb.min_z()),
675        Dir::East => (bb.max_x() + 1, bb.min_z()),
676    };
677    let _ = generate_and_add(pieces, rng, x, bb.min_y(), z, dir, depth);
678}
679
680fn move_bb(bb: BoundingBox, dx: i32, dy: i32, dz: i32) -> BoundingBox {
681    let offset = IVec3::new(dx, dy, dz);
682    BoundingBox::new(bb.min_corner() + offset, bb.max_corner() + offset)
683}
684
685fn union_bb(a: BoundingBox, b: BoundingBox) -> BoundingBox {
686    BoundingBox::new(
687        IVec3::new(
688            a.min_x().min(b.min_x()),
689            a.min_y().min(b.min_y()),
690            a.min_z().min(b.min_z()),
691        ),
692        IVec3::new(
693            a.max_x().max(b.max_x()),
694            a.max_y().max(b.max_y()),
695            a.max_z().max(b.max_z()),
696        ),
697    )
698}
699
700/// Registered under `"minecraft:mineshaft"`.
701pub struct MineshaftStructure;
702
703impl Structure for MineshaftStructure {
704    fn find_generation_point(
705        &self,
706        ctx: &mut dyn StructureGenerationContext,
707        structure: &StructureData,
708        rng: &mut LegacyRandom,
709    ) -> Option<GenerationStub> {
710        let StructureConfigData::Mineshaft { mineshaft_type } = &structure.config else {
711            return None;
712        };
713        let mtype = match mineshaft_type {
714            MineshaftTypeData::Normal => MineshaftType::Normal,
715            MineshaftTypeData::Mesa => MineshaftType::Mesa,
716        };
717
718        let mut get_height = |x: i32, z: i32| ctx.terrain_surface_height(x, z, false);
719
720        let result = find_generation_point(
721            rng,
722            ctx.chunk_x(),
723            ctx.chunk_z(),
724            mtype,
725            ctx.sea_level(),
726            ctx.min_y(),
727            &mut get_height,
728        );
729
730        let (bx, by, bz) = result.biome_check_pos;
731        let biome = ctx.biome_at(bx, by, bz);
732        if !structure.allowed_biomes.contains(&biome.key) {
733            return None;
734        }
735
736        Some(GenerationStub {
737            position: result.biome_check_pos,
738            pieces: result
739                .pieces
740                .into_iter()
741                .map(|p| {
742                    let payload = p.payload;
743                    StructurePiece {
744                        piece_type: Identifier::new_static("minecraft", payload.piece_id()),
745                        bounding_box: p.bounding_box,
746                        gen_depth: p.gen_depth,
747                        orientation: p.orientation,
748                        payload: StructurePiecePayload::Procedural(ProceduralPieceData::Mineshaft(
749                            payload,
750                        )),
751                        ground_level_delta: 0,
752                        junctions: Vec::new(),
753                        projection: None,
754                    }
755                })
756                .collect(),
757        })
758    }
759}
760
761#[cfg(test)]
762mod tests {
763    use super::*;
764
765    #[test]
766    fn mineshaft_matches_vanilla_seed_13579_chunk_0_0() {
767        let mut rng = LegacyRandom::from_seed(0);
768        rng.set_large_feature_seed(13579, 0, 0);
769        rng.next_f64();
770
771        let room_bb = create_room_bb(&mut rng, 2, 2);
772        assert_eq!(
773            room_bb,
774            BoundingBox::new(IVec3::new(2, 50, 2), IVec3::new(13, 56, 9),)
775        );
776
777        let mut pieces = Pieces {
778            bbs: vec![room_bb],
779            infos: vec![PieceInfo {
780                bb: room_bb,
781                kind: MineshaftPieceKind::Room {
782                    child_entrance_boxes: Vec::new(),
783                },
784                gen_depth: 0,
785                dir: None,
786            }],
787            start_bb: room_bb,
788            room_child_entrance_boxes: Vec::new(),
789        };
790        room_add_children(&mut pieces, &mut rng, room_bb);
791        assert_eq!(pieces.bbs.len(), 92);
792
793        let mut overall = pieces.bbs[0];
794        for bb in &pieces.bbs[1..] {
795            overall = union_bb(overall, *bb);
796        }
797        assert_eq!(
798            overall,
799            BoundingBox::new(IVec3::new(-45, 42, -74), IVec3::new(60, 59, 41),)
800        );
801
802        let max_y = 63 - 10;
803        let mut y1_pos = (overall.max_y() - overall.min_y() + 1) + (-64) + 1;
804        if y1_pos < max_y {
805            y1_pos += rng.next_i32_bounded(max_y - y1_pos);
806        }
807        let y_offset = y1_pos - overall.max_y();
808        assert_eq!(y_offset, -70);
809        assert_eq!(50 + y_offset, -20);
810    }
811
812    #[test]
813    fn mineshaft_generation_captures_piece_payload_state() {
814        let mut rng = LegacyRandom::from_seed(0);
815        rng.set_large_feature_seed(13579, 0, 0);
816        let mut surface_height = |_, _| 63;
817
818        let result = find_generation_point(
819            &mut rng,
820            0,
821            0,
822            MineshaftType::Normal,
823            63,
824            -64,
825            &mut surface_height,
826        );
827
828        let MineshaftPieceKind::Room {
829            child_entrance_boxes,
830        } = &result.pieces[0].payload.kind
831        else {
832            panic!("first mineshaft piece should be the start room");
833        };
834        assert!(!child_entrance_boxes.is_empty());
835
836        let corridor = result
837            .pieces
838            .iter()
839            .find_map(|piece| match &piece.payload.kind {
840                MineshaftPieceKind::Corridor {
841                    has_rails,
842                    spider_corridor,
843                    has_placed_spider,
844                    num_sections,
845                } => Some((
846                    *has_rails,
847                    *spider_corridor,
848                    *has_placed_spider,
849                    *num_sections,
850                )),
851                _ => None,
852            })
853            .expect("seed should generate at least one corridor");
854        assert!(!corridor.2);
855        assert!(corridor.3 > 0);
856        assert!(
857            result
858                .pieces
859                .iter()
860                .all(|piece| piece.payload.mineshaft_type == MineshaftType::Normal)
861        );
862    }
863}