1use std::sync::Arc;
2
3use glam::DVec3;
4use steel_registry::blocks::block_state_ext::BlockStateExt as _;
5use steel_registry::blocks::properties::{BlockStateProperties, RailShape};
6use steel_registry::blocks::shapes::SupportType;
7use steel_registry::vanilla_biome_tags::BiomeTag;
8use steel_registry::{Registry, RegistryExt, vanilla_blocks, vanilla_entities};
9use steel_utils::axis::Axis;
10use steel_utils::random::Random;
11use steel_utils::random::worldgen_random::WorldgenRandom;
12use steel_utils::{BlockPos, BlockStateId, BoundingBox, Direction, Identifier, types::UpdateFlags};
13
14use super::StructurePiecePlacer;
15use crate::chunk::heightmap::HeightmapType;
16use crate::entity::{entities::ChestMinecartEntity, next_entity_id};
17use crate::worldgen::generator::vanilla::fuzzed_biome_at_block;
18use crate::worldgen::region::WorldGenRegion;
19use steel_worldgen::structure::mineshaft::{
20 MineshaftPieceKind, MineshaftPiecePayload, MineshaftType,
21};
22
23const ABANDONED_MINESHAFT_LOOT: Identifier =
24 Identifier::new_static("minecraft", "chests/abandoned_mineshaft");
25const CAVE_SPIDER_ENTITY: &str = "minecraft:cave_spider";
26
27impl StructurePiecePlacer {
28 #[expect(
29 clippy::too_many_arguments,
30 reason = "structure-piece placement carries vanilla postProcess inputs"
31 )]
32 pub(super) fn place_mineshaft_piece(
33 region: &mut WorldGenRegion<'_>,
34 registry: &Registry,
35 bounding_box: BoundingBox,
36 orientation: Option<Direction>,
37 data: &mut MineshaftPiecePayload,
38 clip: BoundingBox,
39 random: &mut WorldgenRandom,
40 biome_zoom_seed: i64,
41 ) -> bool {
42 let mineshaft_type = data.mineshaft_type;
43 let mut placer = MineshaftPlacer {
44 region,
45 registry,
46 bounding_box,
47 orientation,
48 clip,
49 mineshaft_type,
50 biome_zoom_seed,
51 };
52 if placer.is_in_invalid_location() {
53 return false;
54 }
55
56 match &mut data.kind {
57 MineshaftPieceKind::Room {
58 child_entrance_boxes,
59 } => placer.place_room(child_entrance_boxes),
60 MineshaftPieceKind::Corridor {
61 has_rails,
62 spider_corridor,
63 has_placed_spider,
64 num_sections,
65 } => placer.place_corridor(
66 random,
67 mineshaft_type,
68 *has_rails,
69 *spider_corridor,
70 has_placed_spider,
71 *num_sections,
72 ),
73 MineshaftPieceKind::Crossing { is_two_floored, .. } => {
74 placer.place_crossing(mineshaft_type, *is_two_floored);
75 }
76 MineshaftPieceKind::Stairs => placer.place_stairs(),
77 }
78 true
79 }
80}
81
82struct MineshaftPlacer<'a, 'world> {
83 region: &'a mut WorldGenRegion<'world>,
84 registry: &'a Registry,
85 bounding_box: BoundingBox,
86 orientation: Option<Direction>,
87 clip: BoundingBox,
88 mineshaft_type: MineshaftType,
89 biome_zoom_seed: i64,
90}
91
92impl MineshaftPlacer<'_, '_> {
93 fn place_room(&mut self, child_entrance_boxes: &[BoundingBox]) {
94 self.generate_box(
95 self.bounding_box.min_x(),
96 self.bounding_box.min_y() + 1,
97 self.bounding_box.min_z(),
98 self.bounding_box.max_x(),
99 (self.bounding_box.min_y() + 3).min(self.bounding_box.max_y()),
100 self.bounding_box.max_z(),
101 Self::cave_air(),
102 Self::cave_air(),
103 false,
104 );
105
106 for entrance_box in child_entrance_boxes {
107 self.generate_box(
108 entrance_box.min_x(),
109 entrance_box.max_y() - 2,
110 entrance_box.min_z(),
111 entrance_box.max_x(),
112 entrance_box.max_y(),
113 entrance_box.max_z(),
114 Self::cave_air(),
115 Self::cave_air(),
116 false,
117 );
118 }
119
120 self.generate_upper_half_sphere(
121 self.bounding_box.min_x(),
122 self.bounding_box.min_y() + 4,
123 self.bounding_box.min_z(),
124 self.bounding_box.max_x(),
125 self.bounding_box.max_y(),
126 self.bounding_box.max_z(),
127 Self::cave_air(),
128 false,
129 );
130 }
131
132 fn place_corridor(
133 &mut self,
134 random: &mut WorldgenRandom,
135 mineshaft_type: MineshaftType,
136 has_rails: bool,
137 spider_corridor: bool,
138 has_placed_spider: &mut bool,
139 num_sections: i32,
140 ) {
141 let length = num_sections * 5 - 1;
142 let planks = Self::planks_state(mineshaft_type);
143 self.generate_box(
144 0,
145 0,
146 0,
147 2,
148 1,
149 length,
150 Self::cave_air(),
151 Self::cave_air(),
152 false,
153 );
154 self.generate_maybe_box(
155 random,
156 0.8,
157 0,
158 2,
159 0,
160 2,
161 2,
162 length,
163 Self::cave_air(),
164 Self::cave_air(),
165 false,
166 false,
167 );
168 if spider_corridor {
169 self.generate_maybe_box(
170 random,
171 0.6,
172 0,
173 0,
174 0,
175 2,
176 1,
177 length,
178 Self::cobweb(),
179 Self::cave_air(),
180 false,
181 true,
182 );
183 }
184
185 for section in 0..num_sections {
186 let z = 2 + section * 5;
187 self.place_support(random, mineshaft_type, 0, 0, z, 2, 2);
188 self.maybe_place_cobweb(random, 0.1, 0, 2, z - 1);
189 self.maybe_place_cobweb(random, 0.1, 2, 2, z - 1);
190 self.maybe_place_cobweb(random, 0.1, 0, 2, z + 1);
191 self.maybe_place_cobweb(random, 0.1, 2, 2, z + 1);
192 self.maybe_place_cobweb(random, 0.05, 0, 2, z - 2);
193 self.maybe_place_cobweb(random, 0.05, 2, 2, z - 2);
194 self.maybe_place_cobweb(random, 0.05, 0, 2, z + 2);
195 self.maybe_place_cobweb(random, 0.05, 2, 2, z + 2);
196
197 if random.next_i32_bounded(100) == 0 {
198 self.create_chest(random, 2, 0, z - 1);
199 }
200 if random.next_i32_bounded(100) == 0 {
201 self.create_chest(random, 0, 0, z + 1);
202 }
203
204 if spider_corridor && !*has_placed_spider {
205 let new_z = z - 1 + random.next_i32_bounded(3);
206 let pos = self.world_pos(1, 0, new_z);
207 if self.clip.contains_blockpos(pos) && self.is_interior(1, 0, new_z) {
208 *has_placed_spider = true;
209 let spawner = Self::spawner();
210 let _ = self
211 .region
212 .set_block_state(pos, spawner, UpdateFlags::UPDATE_CLIENTS);
213 self.set_spawner_entity(pos, spawner, CAVE_SPIDER_ENTITY);
214 }
215 }
216 }
217
218 for x in 0..=2 {
219 for z in 0..=length {
220 self.set_planks_block(planks, x, -1, z);
221 }
222 }
223
224 self.place_double_lower_or_upper_support(mineshaft_type, 0, -1, 2);
225 if num_sections > 1 {
226 self.place_double_lower_or_upper_support(mineshaft_type, 0, -1, length - 2);
227 }
228
229 if has_rails {
230 let rail =
231 Self::rail().set_value(&BlockStateProperties::RAIL_SHAPE, RailShape::NorthSouth);
232 for z in 0..=length {
233 let floor = self.get_block(1, -1, z);
234 if !floor.is_air() && floor.is_solid_render() {
235 let probability = if self.is_interior(1, 0, z) { 0.7 } else { 0.9 };
236 self.maybe_generate_block(random, probability, 1, 0, z, rail);
237 }
238 }
239 }
240 }
241
242 #[expect(
243 clippy::too_many_lines,
244 reason = "crossing placement follows vanilla's imperative piece layout"
245 )]
246 fn place_crossing(&mut self, mineshaft_type: MineshaftType, is_two_floored: bool) {
247 let planks = Self::planks_state(mineshaft_type);
248 if is_two_floored {
249 self.generate_box(
250 self.bounding_box.min_x() + 1,
251 self.bounding_box.min_y(),
252 self.bounding_box.min_z(),
253 self.bounding_box.max_x() - 1,
254 self.bounding_box.min_y() + 2,
255 self.bounding_box.max_z(),
256 Self::cave_air(),
257 Self::cave_air(),
258 false,
259 );
260 self.generate_box(
261 self.bounding_box.min_x(),
262 self.bounding_box.min_y(),
263 self.bounding_box.min_z() + 1,
264 self.bounding_box.max_x(),
265 self.bounding_box.min_y() + 2,
266 self.bounding_box.max_z() - 1,
267 Self::cave_air(),
268 Self::cave_air(),
269 false,
270 );
271 self.generate_box(
272 self.bounding_box.min_x() + 1,
273 self.bounding_box.max_y() - 2,
274 self.bounding_box.min_z(),
275 self.bounding_box.max_x() - 1,
276 self.bounding_box.max_y(),
277 self.bounding_box.max_z(),
278 Self::cave_air(),
279 Self::cave_air(),
280 false,
281 );
282 self.generate_box(
283 self.bounding_box.min_x(),
284 self.bounding_box.max_y() - 2,
285 self.bounding_box.min_z() + 1,
286 self.bounding_box.max_x(),
287 self.bounding_box.max_y(),
288 self.bounding_box.max_z() - 1,
289 Self::cave_air(),
290 Self::cave_air(),
291 false,
292 );
293 self.generate_box(
294 self.bounding_box.min_x() + 1,
295 self.bounding_box.min_y() + 3,
296 self.bounding_box.min_z() + 1,
297 self.bounding_box.max_x() - 1,
298 self.bounding_box.min_y() + 3,
299 self.bounding_box.max_z() - 1,
300 Self::cave_air(),
301 Self::cave_air(),
302 false,
303 );
304 } else {
305 self.generate_box(
306 self.bounding_box.min_x() + 1,
307 self.bounding_box.min_y(),
308 self.bounding_box.min_z(),
309 self.bounding_box.max_x() - 1,
310 self.bounding_box.max_y(),
311 self.bounding_box.max_z(),
312 Self::cave_air(),
313 Self::cave_air(),
314 false,
315 );
316 self.generate_box(
317 self.bounding_box.min_x(),
318 self.bounding_box.min_y(),
319 self.bounding_box.min_z() + 1,
320 self.bounding_box.max_x(),
321 self.bounding_box.max_y(),
322 self.bounding_box.max_z() - 1,
323 Self::cave_air(),
324 Self::cave_air(),
325 false,
326 );
327 }
328
329 self.place_support_pillar(
330 mineshaft_type,
331 self.bounding_box.min_x() + 1,
332 self.bounding_box.min_y(),
333 self.bounding_box.min_z() + 1,
334 self.bounding_box.max_y(),
335 );
336 self.place_support_pillar(
337 mineshaft_type,
338 self.bounding_box.min_x() + 1,
339 self.bounding_box.min_y(),
340 self.bounding_box.max_z() - 1,
341 self.bounding_box.max_y(),
342 );
343 self.place_support_pillar(
344 mineshaft_type,
345 self.bounding_box.max_x() - 1,
346 self.bounding_box.min_y(),
347 self.bounding_box.min_z() + 1,
348 self.bounding_box.max_y(),
349 );
350 self.place_support_pillar(
351 mineshaft_type,
352 self.bounding_box.max_x() - 1,
353 self.bounding_box.min_y(),
354 self.bounding_box.max_z() - 1,
355 self.bounding_box.max_y(),
356 );
357
358 let y = self.bounding_box.min_y() - 1;
359 for x in self.bounding_box.min_x()..=self.bounding_box.max_x() {
360 for z in self.bounding_box.min_z()..=self.bounding_box.max_z() {
361 self.set_planks_block(planks, x, y, z);
362 }
363 }
364 }
365
366 fn place_stairs(&mut self) {
367 self.generate_box(0, 5, 0, 2, 7, 1, Self::cave_air(), Self::cave_air(), false);
368 self.generate_box(0, 0, 7, 2, 2, 8, Self::cave_air(), Self::cave_air(), false);
369 for i in 0..5 {
370 let min_y = 5 - i - i32::from(i < 4);
371 self.generate_box(
372 0,
373 min_y,
374 2 + i,
375 2,
376 7 - i,
377 2 + i,
378 Self::cave_air(),
379 Self::cave_air(),
380 false,
381 );
382 }
383 }
384
385 fn is_in_invalid_location(&self) -> bool {
386 let x0 = (self.bounding_box.min_x() - 1).max(self.clip.min_x());
387 let y0 = (self.bounding_box.min_y() - 1).max(self.clip.min_y());
388 let z0 = (self.bounding_box.min_z() - 1).max(self.clip.min_z());
389 let x1 = (self.bounding_box.max_x() + 1).min(self.clip.max_x());
390 let y1 = (self.bounding_box.max_y() + 1).min(self.clip.max_y());
391 let z1 = (self.bounding_box.max_z() + 1).min(self.clip.max_z());
392
393 let biome_pos = BlockPos::new(
394 i32::midpoint(x0, x1),
395 i32::midpoint(y0, y1),
396 i32::midpoint(z0, z1),
397 );
398 if self.is_mineshaft_blocking_biome(biome_pos) {
399 return true;
400 }
401
402 for x in x0..=x1 {
403 for z in z0..=z1 {
404 if self.is_liquid_block(BlockPos::new(x, y0, z))
405 || self.is_liquid_block(BlockPos::new(x, y1, z))
406 {
407 return true;
408 }
409 }
410 }
411
412 for x in x0..=x1 {
413 for y in y0..=y1 {
414 if self.is_liquid_block(BlockPos::new(x, y, z0))
415 || self.is_liquid_block(BlockPos::new(x, y, z1))
416 {
417 return true;
418 }
419 }
420 }
421
422 for z in z0..=z1 {
423 for y in y0..=y1 {
424 if self.is_liquid_block(BlockPos::new(x0, y, z))
425 || self.is_liquid_block(BlockPos::new(x1, y, z))
426 {
427 return true;
428 }
429 }
430 }
431
432 false
433 }
434
435 fn is_mineshaft_blocking_biome(&self, pos: BlockPos) -> bool {
436 let biome_id = fuzzed_biome_at_block(self.biome_zoom_seed, pos, |quart| {
437 self.region.noise_biome_id(quart.x, quart.y, quart.z)
438 });
439 let Some(biome) = self.registry.biomes.by_id(usize::from(biome_id)) else {
440 panic!("noise biome id {biome_id} is not registered");
441 };
442 biome.has_tag(&BiomeTag::MINESHAFT_BLOCKING)
443 }
444
445 #[expect(
446 clippy::too_many_arguments,
447 reason = "box fill helpers use vanilla min/max coordinate signatures"
448 )]
449 fn generate_box(
450 &mut self,
451 x0: i32,
452 y0: i32,
453 z0: i32,
454 x1: i32,
455 y1: i32,
456 z1: i32,
457 edge_block: BlockStateId,
458 fill_block: BlockStateId,
459 skip_air: bool,
460 ) {
461 for y in y0..=y1 {
462 for x in x0..=x1 {
463 for z in z0..=z1 {
464 if skip_air && self.get_block(x, y, z).is_air() {
465 continue;
466 }
467 let state = if y != y0 && y != y1 && x != x0 && x != x1 && z != z0 && z != z1 {
468 fill_block
469 } else {
470 edge_block
471 };
472 self.place_block(state, x, y, z);
473 }
474 }
475 }
476 }
477
478 #[expect(
479 clippy::too_many_arguments,
480 reason = "mirrors StructurePiece.generateMaybeBox"
481 )]
482 fn generate_maybe_box(
483 &mut self,
484 random: &mut WorldgenRandom,
485 probability: f32,
486 x0: i32,
487 y0: i32,
488 z0: i32,
489 x1: i32,
490 y1: i32,
491 z1: i32,
492 edge_block: BlockStateId,
493 fill_block: BlockStateId,
494 skip_air: bool,
495 has_to_be_inside: bool,
496 ) {
497 for y in y0..=y1 {
498 for x in x0..=x1 {
499 for z in z0..=z1 {
500 if random.next_f32() > probability
501 || skip_air && self.get_block(x, y, z).is_air()
502 || has_to_be_inside && !self.is_interior(x, y, z)
503 {
504 continue;
505 }
506 let state = if y != y0 && y != y1 && x != x0 && x != x1 && z != z0 && z != z1 {
507 fill_block
508 } else {
509 edge_block
510 };
511 self.place_block(state, x, y, z);
512 }
513 }
514 }
515 }
516
517 #[expect(
518 clippy::too_many_arguments,
519 reason = "mirrors StructurePiece.generateUpperHalfSphere"
520 )]
521 fn generate_upper_half_sphere(
522 &mut self,
523 x0: i32,
524 y0: i32,
525 z0: i32,
526 x1: i32,
527 y1: i32,
528 z1: i32,
529 fill_block: BlockStateId,
530 skip_air: bool,
531 ) {
532 let diag_x = (x1 - x0 + 1) as f32;
533 let diag_y = (y1 - y0 + 1) as f32;
534 let diag_z = (z1 - z0 + 1) as f32;
535 let cx = x0 as f32 + diag_x / 2.0;
536 let cz = z0 as f32 + diag_z / 2.0;
537
538 for y in y0..=y1 {
539 let normalized_y = (y - y0) as f32 / diag_y;
540 for x in x0..=x1 {
541 let normalized_x = (x as f32 - cx) / (diag_x * 0.5);
542 for z in z0..=z1 {
543 let normalized_z = (z as f32 - cz) / (diag_z * 0.5);
544 if skip_air && self.get_block(x, y, z).is_air() {
545 continue;
546 }
547 let distance = normalized_x * normalized_x
548 + normalized_y * normalized_y
549 + normalized_z * normalized_z;
550 if distance <= 1.05 {
551 self.place_block(fill_block, x, y, z);
552 }
553 }
554 }
555 }
556 }
557
558 fn maybe_generate_block(
559 &mut self,
560 random: &mut WorldgenRandom,
561 probability: f32,
562 x: i32,
563 y: i32,
564 z: i32,
565 state: BlockStateId,
566 ) {
567 if random.next_f32() < probability {
568 self.place_block(state, x, y, z);
569 }
570 }
571
572 fn maybe_place_cobweb(
573 &mut self,
574 random: &mut WorldgenRandom,
575 probability: f32,
576 x: i32,
577 y: i32,
578 z: i32,
579 ) {
580 if self.is_interior(x, y, z)
581 && random.next_f32() < probability
582 && self.has_sturdy_neighbors(x, y, z, 2)
583 {
584 self.place_block(Self::cobweb(), x, y, z);
585 }
586 }
587
588 fn create_chest(&mut self, random: &mut WorldgenRandom, x: i32, y: i32, z: i32) -> bool {
589 let pos = self.world_pos(x, y, z);
590 if !self.clip.contains_blockpos(pos)
591 || !self.block_state(pos).is_air()
592 || self.block_state(pos.below()).is_air()
593 {
594 return false;
595 }
596
597 let shape = if random.next_bool() {
598 RailShape::NorthSouth
599 } else {
600 RailShape::EastWest
601 };
602 let rail = Self::rail().set_value(&BlockStateProperties::RAIL_SHAPE, shape);
603 self.place_block(rail, x, y, z);
604 let loot_seed = random.next_i64();
605 let chest = Arc::new(ChestMinecartEntity::new(
606 &vanilla_entities::CHEST_MINECART,
607 next_entity_id(),
608 DVec3::new(
609 f64::from(pos.x()) + 0.5,
610 f64::from(pos.y()) + 0.5,
611 f64::from(pos.z()) + 0.5,
612 ),
613 self.region.weak_world(),
614 ));
615 chest.set_loot_table(ABANDONED_MINESHAFT_LOOT, loot_seed);
616 let _ = self.region.add_fresh_entity(chest);
617 true
618 }
619
620 fn set_spawner_entity(&mut self, pos: BlockPos, state: BlockStateId, entity_id: &'static str) {
621 let _ = StructurePiecePlacer::set_spawner_entity(self.region, pos, state, entity_id);
622 }
623
624 #[expect(
625 clippy::too_many_arguments,
626 reason = "support placement follows vanilla coordinate parameters"
627 )]
628 fn place_support(
629 &mut self,
630 random: &mut WorldgenRandom,
631 mineshaft_type: MineshaftType,
632 x0: i32,
633 y0: i32,
634 z: i32,
635 y1: i32,
636 x1: i32,
637 ) {
638 if !self.is_supporting_box(x0, x1, y1, z) {
639 return;
640 }
641
642 let planks = Self::planks_state(mineshaft_type);
643 let fence = Self::fence_state(mineshaft_type);
644 self.generate_box(
645 x0,
646 y0,
647 z,
648 x0,
649 y1 - 1,
650 z,
651 fence.set_value(&BlockStateProperties::WEST, true),
652 Self::cave_air(),
653 false,
654 );
655 self.generate_box(
656 x1,
657 y0,
658 z,
659 x1,
660 y1 - 1,
661 z,
662 fence.set_value(&BlockStateProperties::EAST, true),
663 Self::cave_air(),
664 false,
665 );
666 if random.next_i32_bounded(4) == 0 {
667 self.generate_box(x0, y1, z, x0, y1, z, planks, Self::cave_air(), false);
668 self.generate_box(x1, y1, z, x1, y1, z, planks, Self::cave_air(), false);
669 } else {
670 self.generate_box(x0, y1, z, x1, y1, z, planks, Self::cave_air(), false);
671 self.maybe_generate_block(
672 random,
673 0.05,
674 x0 + 1,
675 y1,
676 z - 1,
677 Self::wall_torch().set_value(&BlockStateProperties::FACING, Direction::South),
678 );
679 self.maybe_generate_block(
680 random,
681 0.05,
682 x0 + 1,
683 y1,
684 z + 1,
685 Self::wall_torch().set_value(&BlockStateProperties::FACING, Direction::North),
686 );
687 }
688 }
689
690 fn is_supporting_box(&self, x0: i32, x1: i32, y1: i32, z: i32) -> bool {
691 for x in x0..=x1 {
692 if self.get_block(x, y1 + 1, z).is_air() {
693 return false;
694 }
695 }
696 true
697 }
698
699 fn has_sturdy_neighbors(&self, x: i32, y: i32, z: i32, count: i32) -> bool {
700 let pos = self.world_pos(x, y, z);
701 let mut sturdy_neighbors = 0;
702 for direction in [
703 Direction::Down,
704 Direction::Up,
705 Direction::North,
706 Direction::South,
707 Direction::West,
708 Direction::East,
709 ] {
710 let neighbor = pos.relative(direction);
711 if self.clip.contains_blockpos(neighbor)
712 && self
713 .block_state(neighbor)
714 .is_face_sturdy_at(neighbor, direction.opposite())
715 {
716 sturdy_neighbors += 1;
717 if sturdy_neighbors >= count {
718 return true;
719 }
720 }
721 }
722 false
723 }
724
725 fn place_double_lower_or_upper_support(
726 &mut self,
727 mineshaft_type: MineshaftType,
728 x: i32,
729 y: i32,
730 z: i32,
731 ) {
732 let wood = Self::wood_state(mineshaft_type);
733 let planks = Self::planks_state(mineshaft_type);
734 if self.get_block(x, y, z).get_block() == planks.get_block() {
735 self.fill_pillar_down_or_chain_up(mineshaft_type, wood, x, y, z);
736 }
737 if self.get_block(x + 2, y, z).get_block() == planks.get_block() {
738 self.fill_pillar_down_or_chain_up(mineshaft_type, wood, x + 2, y, z);
739 }
740 }
741
742 fn fill_pillar_down_or_chain_up(
743 &mut self,
744 mineshaft_type: MineshaftType,
745 pillar_state: BlockStateId,
746 x: i32,
747 y: i32,
748 z: i32,
749 ) {
750 let pos = self.world_pos(x, y, z);
751 if !self.clip.contains_blockpos(pos) {
752 return;
753 }
754
755 let world_y = pos.y();
756 let mut check_below = true;
757 let mut check_above = true;
758 let mut distance = 1;
759 while check_below || check_above {
760 if check_below {
761 let below_pos = BlockPos::new(pos.x(), world_y - distance, pos.z());
762 let below_state = self.block_state(below_pos);
763 let empty_below = Self::is_replaceable_by_structures(below_state)
764 && below_state.get_block() != &vanilla_blocks::LAVA;
765 if !empty_below && Self::can_place_column_on_top_of(below_state, below_pos) {
766 self.fill_column_between(
767 pillar_state,
768 pos.x(),
769 pos.z(),
770 world_y - distance + 1,
771 world_y,
772 );
773 return;
774 }
775 check_below =
776 distance <= 20 && empty_below && below_pos.y() > self.region.min_y() + 1;
777 }
778
779 if check_above {
780 let above_pos = BlockPos::new(pos.x(), world_y + distance, pos.z());
781 let above_state = self.block_state(above_pos);
782 let empty_above = Self::is_replaceable_by_structures(above_state);
783 if !empty_above && Self::can_hang_chain_below(above_state, above_pos) {
784 let fence_pos = BlockPos::new(pos.x(), world_y + 1, pos.z());
785 let _ = self.region.set_block_state(
786 fence_pos,
787 Self::fence_state(mineshaft_type),
788 UpdateFlags::UPDATE_CLIENTS,
789 );
790 self.fill_column_between(
791 Self::chain(),
792 pos.x(),
793 pos.z(),
794 world_y + 2,
795 world_y + distance,
796 );
797 return;
798 }
799 check_above =
800 distance <= 50 && empty_above && above_pos.y() < self.region.max_y_exclusive();
801 }
802
803 distance += 1;
804 }
805 }
806
807 fn fill_column_between(
808 &mut self,
809 state: BlockStateId,
810 x: i32,
811 z: i32,
812 bottom_inclusive: i32,
813 top_exclusive: i32,
814 ) {
815 for y in bottom_inclusive..top_exclusive {
816 let _ = self.region.set_block_state(
817 BlockPos::new(x, y, z),
818 state,
819 UpdateFlags::UPDATE_CLIENTS,
820 );
821 }
822 }
823
824 fn can_place_column_on_top_of(state_below: BlockStateId, pos_below: BlockPos) -> bool {
825 state_below.is_face_sturdy_at(pos_below, Direction::Up)
826 }
827
828 fn can_hang_chain_below(state_above: BlockStateId, pos_above: BlockPos) -> bool {
829 state_above.is_face_sturdy_for_at(pos_above, Direction::Down, SupportType::Center)
830 && !Self::is_falling_block(state_above)
831 }
832
833 fn place_support_pillar(
834 &mut self,
835 mineshaft_type: MineshaftType,
836 x: i32,
837 y0: i32,
838 z: i32,
839 y1: i32,
840 ) {
841 if !self.get_block(x, y1 + 1, z).is_air() {
842 self.generate_box(
843 x,
844 y0,
845 z,
846 x,
847 y1,
848 z,
849 Self::planks_state(mineshaft_type),
850 Self::cave_air(),
851 false,
852 );
853 }
854 }
855
856 fn set_planks_block(&mut self, planks: BlockStateId, x: i32, y: i32, z: i32) {
857 if !self.is_interior(x, y, z) {
858 return;
859 }
860 let pos = self.world_pos(x, y, z);
861 let existing = self.block_state(pos);
862 if !existing.is_face_sturdy_at(pos, Direction::Up) {
863 let _ = self
864 .region
865 .set_block_state(pos, planks, UpdateFlags::UPDATE_CLIENTS);
866 }
867 }
868
869 fn is_interior(&self, x: i32, y: i32, z: i32) -> bool {
870 let pos = self.world_pos(x, y + 1, z);
871 self.clip.contains_blockpos(pos)
872 && pos.y()
873 < self
874 .region
875 .height_at(HeightmapType::OceanFloorWg, pos.x(), pos.z())
876 }
877
878 fn place_block(&mut self, state: BlockStateId, x: i32, y: i32, z: i32) {
879 let pos = self.world_pos(x, y, z);
880 if !self.clip.contains_blockpos(pos) || !self.can_be_replaced(x, y, z) {
881 return;
882 }
883
884 let state = self.transform_state(state);
885 let _ = self
886 .region
887 .set_block_state(pos, state, UpdateFlags::UPDATE_CLIENTS);
888 if Self::needs_shape_postprocessing(state) {
889 self.region.mark_pos_for_postprocessing(pos);
890 }
891 }
892
893 fn get_block(&self, x: i32, y: i32, z: i32) -> BlockStateId {
894 let pos = self.world_pos(x, y, z);
895 if self.clip.contains_blockpos(pos) {
896 self.block_state(pos)
897 } else {
898 Self::air()
899 }
900 }
901
902 fn block_state(&self, pos: BlockPos) -> BlockStateId {
903 if self.region.is_outside_build_height(pos.y()) {
904 Self::air()
905 } else {
906 self.region.block_state(pos)
907 }
908 }
909
910 fn is_liquid_block(&self, pos: BlockPos) -> bool {
911 self.block_state(pos).get_block().config.liquid
912 }
913
914 fn can_be_replaced(&self, x: i32, y: i32, z: i32) -> bool {
915 let state = self.get_block(x, y, z);
916 let block = state.get_block();
917 block != Self::planks_state(self.mineshaft_type).get_block()
918 && block != Self::wood_state(self.mineshaft_type).get_block()
919 && block != Self::fence_state(self.mineshaft_type).get_block()
920 && block != &vanilla_blocks::IRON_CHAIN
921 }
922
923 fn is_replaceable_by_structures(state: BlockStateId) -> bool {
924 state.is_air()
925 || state.get_block().config.liquid
926 || state.get_block() == &vanilla_blocks::GLOW_LICHEN
927 || state.get_block() == &vanilla_blocks::SEAGRASS
928 || state.get_block() == &vanilla_blocks::TALL_SEAGRASS
929 }
930
931 const fn world_pos(&self, x: i32, y: i32, z: i32) -> BlockPos {
932 let world_y = if self.orientation.is_some() {
933 y + self.bounding_box.min_y()
934 } else {
935 y
936 };
937 let (world_x, world_z) = match self.orientation {
938 Some(Direction::North) => {
939 (self.bounding_box.min_x() + x, self.bounding_box.max_z() - z)
940 }
941 Some(Direction::South) => {
942 (self.bounding_box.min_x() + x, self.bounding_box.min_z() + z)
943 }
944 Some(Direction::West) => (self.bounding_box.max_x() - z, self.bounding_box.min_z() + x),
945 Some(Direction::East) => (self.bounding_box.min_x() + z, self.bounding_box.min_z() + x),
946 None | Some(Direction::Up | Direction::Down) => (x, z),
947 };
948 BlockPos::new(world_x, world_y, world_z)
949 }
950
951 fn transform_state(&self, mut state: BlockStateId) -> BlockStateId {
952 if let Some(facing) = state.try_get_value(&BlockStateProperties::FACING) {
953 state = state.set_value(
954 &BlockStateProperties::FACING,
955 self.transform_direction(facing),
956 );
957 } else if let Some(facing) = state.try_get_value(&BlockStateProperties::HORIZONTAL_FACING) {
958 state = state.set_value(
959 &BlockStateProperties::HORIZONTAL_FACING,
960 self.transform_direction(facing),
961 );
962 }
963 if let Some(shape) = state.try_get_value(&BlockStateProperties::RAIL_SHAPE) {
964 state = state.set_value(
965 &BlockStateProperties::RAIL_SHAPE,
966 self.transform_rail_shape(shape),
967 );
968 }
969 self.transform_side_bools(state)
970 }
971
972 fn transform_side_bools(&self, state: BlockStateId) -> BlockStateId {
973 let sides = [
974 (
975 Direction::North,
976 state.try_get_value(&BlockStateProperties::NORTH),
977 ),
978 (
979 Direction::East,
980 state.try_get_value(&BlockStateProperties::EAST),
981 ),
982 (
983 Direction::South,
984 state.try_get_value(&BlockStateProperties::SOUTH),
985 ),
986 (
987 Direction::West,
988 state.try_get_value(&BlockStateProperties::WEST),
989 ),
990 ];
991 if sides.iter().all(|(_, value)| value.is_none()) {
992 return state;
993 }
994
995 let mut transformed = state;
996 for direction in [
997 Direction::North,
998 Direction::East,
999 Direction::South,
1000 Direction::West,
1001 ] {
1002 transformed = Self::set_side(transformed, direction, false);
1003 }
1004 for (direction, value) in sides {
1005 if value == Some(true) {
1006 transformed =
1007 Self::set_side(transformed, self.transform_direction(direction), true);
1008 }
1009 }
1010 transformed
1011 }
1012
1013 fn set_side(state: BlockStateId, direction: Direction, value: bool) -> BlockStateId {
1014 match direction {
1015 Direction::North => state.set_value(&BlockStateProperties::NORTH, value),
1016 Direction::East => state.set_value(&BlockStateProperties::EAST, value),
1017 Direction::South => state.set_value(&BlockStateProperties::SOUTH, value),
1018 Direction::West => state.set_value(&BlockStateProperties::WEST, value),
1019 Direction::Up | Direction::Down => state,
1020 }
1021 }
1022
1023 const fn transform_direction(&self, direction: Direction) -> Direction {
1024 let mirrored = match self.orientation {
1025 Some(Direction::South | Direction::West) => Self::mirror_left_right(direction),
1026 _ => direction,
1027 };
1028 match self.orientation {
1029 Some(Direction::West | Direction::East) => mirrored.rotate_y_clockwise(),
1030 _ => mirrored,
1031 }
1032 }
1033
1034 const fn mirror_left_right(direction: Direction) -> Direction {
1035 match direction {
1036 Direction::North => Direction::South,
1037 Direction::South => Direction::North,
1038 other => other,
1039 }
1040 }
1041
1042 const fn transform_rail_shape(&self, shape: RailShape) -> RailShape {
1043 match shape {
1044 RailShape::NorthSouth => match self.transform_direction(Direction::North).axis() {
1045 Axis::X => RailShape::EastWest,
1046 _ => RailShape::NorthSouth,
1047 },
1048 RailShape::EastWest => match self.transform_direction(Direction::East).axis() {
1049 Axis::Z => RailShape::NorthSouth,
1050 _ => RailShape::EastWest,
1051 },
1052 other => other,
1053 }
1054 }
1055
1056 fn air() -> BlockStateId {
1057 vanilla_blocks::AIR.default_state()
1058 }
1059
1060 fn cave_air() -> BlockStateId {
1061 vanilla_blocks::CAVE_AIR.default_state()
1062 }
1063
1064 fn cobweb() -> BlockStateId {
1065 vanilla_blocks::COBWEB.default_state()
1066 }
1067
1068 fn rail() -> BlockStateId {
1069 vanilla_blocks::RAIL.default_state()
1070 }
1071
1072 fn spawner() -> BlockStateId {
1073 vanilla_blocks::SPAWNER.default_state()
1074 }
1075
1076 fn chain() -> BlockStateId {
1077 vanilla_blocks::IRON_CHAIN.default_state()
1078 }
1079
1080 fn wall_torch() -> BlockStateId {
1081 vanilla_blocks::WALL_TORCH.default_state()
1082 }
1083
1084 fn wood_state(mineshaft_type: MineshaftType) -> BlockStateId {
1085 match mineshaft_type {
1086 MineshaftType::Normal => vanilla_blocks::OAK_LOG.default_state(),
1087 MineshaftType::Mesa => vanilla_blocks::DARK_OAK_LOG.default_state(),
1088 }
1089 }
1090
1091 fn planks_state(mineshaft_type: MineshaftType) -> BlockStateId {
1092 match mineshaft_type {
1093 MineshaftType::Normal => vanilla_blocks::OAK_PLANKS.default_state(),
1094 MineshaftType::Mesa => vanilla_blocks::DARK_OAK_PLANKS.default_state(),
1095 }
1096 }
1097
1098 fn fence_state(mineshaft_type: MineshaftType) -> BlockStateId {
1099 match mineshaft_type {
1100 MineshaftType::Normal => vanilla_blocks::OAK_FENCE.default_state(),
1101 MineshaftType::Mesa => vanilla_blocks::DARK_OAK_FENCE.default_state(),
1102 }
1103 }
1104
1105 fn needs_shape_postprocessing(state: BlockStateId) -> bool {
1106 let block = state.get_block();
1107 block == &vanilla_blocks::WALL_TORCH
1108 || block == &vanilla_blocks::OAK_FENCE
1109 || block == &vanilla_blocks::DARK_OAK_FENCE
1110 }
1111
1112 fn is_falling_block(state: BlockStateId) -> bool {
1113 let block = state.get_block();
1114 block == &vanilla_blocks::SAND
1115 || block == &vanilla_blocks::RED_SAND
1116 || block == &vanilla_blocks::GRAVEL
1117 || block == &vanilla_blocks::WHITE_CONCRETE_POWDER
1118 || block == &vanilla_blocks::ORANGE_CONCRETE_POWDER
1119 || block == &vanilla_blocks::MAGENTA_CONCRETE_POWDER
1120 || block == &vanilla_blocks::LIGHT_BLUE_CONCRETE_POWDER
1121 || block == &vanilla_blocks::YELLOW_CONCRETE_POWDER
1122 || block == &vanilla_blocks::LIME_CONCRETE_POWDER
1123 || block == &vanilla_blocks::PINK_CONCRETE_POWDER
1124 || block == &vanilla_blocks::GRAY_CONCRETE_POWDER
1125 || block == &vanilla_blocks::LIGHT_GRAY_CONCRETE_POWDER
1126 || block == &vanilla_blocks::CYAN_CONCRETE_POWDER
1127 || block == &vanilla_blocks::PURPLE_CONCRETE_POWDER
1128 || block == &vanilla_blocks::BLUE_CONCRETE_POWDER
1129 || block == &vanilla_blocks::BROWN_CONCRETE_POWDER
1130 || block == &vanilla_blocks::GREEN_CONCRETE_POWDER
1131 || block == &vanilla_blocks::RED_CONCRETE_POWDER
1132 || block == &vanilla_blocks::BLACK_CONCRETE_POWDER
1133 || block == &vanilla_blocks::ANVIL
1134 || block == &vanilla_blocks::CHIPPED_ANVIL
1135 || block == &vanilla_blocks::DAMAGED_ANVIL
1136 || block == &vanilla_blocks::DRAGON_EGG
1137 }
1138}