Skip to main content

steel_core/worldgen/structure/piece_placer/
mineshaft.rs

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}