1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum MineshaftType {
21 Normal,
23 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum PieceType {
50 Room,
52 Corridor,
54 Crossing,
56 Stairs,
58}
59
60impl PieceType {
61 #[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
93pub struct MineshaftPieceData {
95 pub payload: MineshaftPiecePayload,
97 pub bounding_box: BoundingBox,
99 pub gen_depth: i32,
101 pub orientation: Option<Direction>,
104}
105
106#[derive(Debug, Clone, PartialEq)]
109pub struct MineshaftPiecePayload {
110 pub mineshaft_type: MineshaftType,
112 pub kind: MineshaftPieceKind,
114}
115
116impl MineshaftPiecePayload {
117 #[must_use]
119 pub const fn piece_id(&self) -> &'static str {
120 self.kind.piece_type().piece_id()
121 }
122}
123
124#[derive(Debug, Clone, PartialEq)]
126pub enum MineshaftPieceKind {
127 Room {
129 child_entrance_boxes: Vec<BoundingBox>,
131 },
132 Corridor {
134 has_rails: bool,
136 spider_corridor: bool,
138 has_placed_spider: bool,
140 num_sections: i32,
142 },
143 Crossing {
145 direction: Direction,
147 is_two_floored: bool,
149 },
150 Stairs,
152}
153
154impl MineshaftPieceKind {
155 #[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
167pub struct MineshaftResult {
169 pub biome_check_pos: (i32, i32, i32),
171 pub pieces: Vec<MineshaftPieceData>,
173}
174
175pub 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
700pub 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}