1use 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
27const 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum FortressPieceData {
105 BridgeCrossing,
107 BridgeEndFiller {
109 self_seed: i32,
111 },
112 BridgeStraight,
114 CastleCorridorStairs,
116 CastleCorridorTBalcony,
118 CastleEntrance,
120 CastleSmallCorridorCrossing,
122 CastleSmallCorridorLeftTurn {
124 is_needing_chest: bool,
126 },
127 CastleSmallCorridor,
129 CastleSmallCorridorRightTurn {
131 is_needing_chest: bool,
133 },
134 CastleStalkRoom,
136 MonsterThrone {
138 has_placed_spawner: bool,
140 },
141 RoomCrossing,
143 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#[derive(Debug, Clone, Copy)]
260pub struct FortressPiece {
261 pub data: FortressPieceData,
263 pub bounding_box: BoundingBox,
265 pub orientation: Option<Direction>,
267 pub gen_depth: i32,
269}
270
271fn 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
293fn 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
333fn 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
355fn 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
431fn 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#[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 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
636pub 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
679pub 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 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}