Skip to main content

steel_core/worldgen/structure/piece_placer/
mod.rs

1//! Feature-stage structure piece placement boundary.
2//!
3//! Structure starts are generated before noise, but vanilla emits the piece
4//! blocks during biome decoration. This module is the single dispatch point for
5//! that pass; individual family placers must fill in exact vanilla behavior
6//! before any payload variant starts writing blocks.
7
8mod buried_treasure;
9mod desert_pyramid;
10mod fortress;
11mod jungle_temple;
12mod mineshaft;
13mod ocean_monument;
14mod pool_element;
15mod ruined_portal;
16mod scattered_feature;
17mod stronghold;
18mod swamp_hut;
19mod template_piece;
20mod template_processors;
21
22use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
23use steel_registry::block_entity_type::BlockEntityTypeRef;
24use steel_registry::blocks::block_state_ext::BlockStateExt as _;
25use steel_registry::blocks::properties::BlockStateProperties;
26use steel_registry::structure::StructureRef;
27use steel_registry::{Registry, vanilla_block_entity_types, vanilla_blocks};
28use steel_utils::random::Random;
29use steel_utils::random::worldgen_random::WorldgenRandom;
30use steel_utils::{
31    BlockPos, BlockStateId, BoundingBox, Direction, Identifier, PackedBlockPos, Rotation,
32    types::UpdateFlags,
33};
34
35use crate::worldgen::region::WorldGenRegion;
36use steel_worldgen::structure::{
37    ProceduralPieceData, StructureMirror, StructurePiece, StructurePiecePayload,
38};
39
40pub(crate) struct StructurePiecePlacer;
41
42impl StructurePiecePlacer {
43    /// Vanilla jigsaw pool-element placement flags: `UPDATE_CLIENTS | UPDATE_KNOWN_SHAPE`.
44    pub(crate) const JIGSAW_UPDATE_FLAGS: UpdateFlags =
45        UpdateFlags::UPDATE_CLIENTS.union(UpdateFlags::UPDATE_KNOWN_SHAPE);
46    /// Vanilla template-piece placement flags: `UPDATE_CLIENTS`.
47    pub(crate) const TEMPLATE_UPDATE_FLAGS: UpdateFlags = UpdateFlags::UPDATE_CLIENTS;
48
49    /// Places one already-clipped structure piece.
50    ///
51    /// Returns whether the vanilla placement call succeeded. Later milestones
52    /// must implement each remaining payload variant completely before it can
53    /// return `true`.
54    #[expect(
55        clippy::too_many_lines,
56        reason = "single dispatch boundary for all structure piece payload families"
57    )]
58    pub(crate) fn place_piece(
59        region: &mut WorldGenRegion<'_>,
60        registry: &Registry,
61        piece: &mut StructurePiece,
62        reference_pos: BlockPos,
63        clip: BoundingBox,
64        random: &mut WorldgenRandom,
65        biome_zoom_seed: i64,
66    ) -> bool {
67        let mut piece_bounding_box = piece.bounding_box;
68        let piece_orientation = piece.orientation;
69        let placed = match &mut piece.payload {
70            StructurePiecePayload::Jigsaw(data) => Self::place_pool_element(
71                region,
72                registry,
73                &data.pool_element,
74                BlockPos::new(data.position.x, data.position.y, data.position.z),
75                reference_pos,
76                data.rotation,
77                clip,
78                random,
79                data.liquid_settings,
80                biome_zoom_seed,
81            ),
82            StructurePiecePayload::Template(data) => Self::place_template_piece(
83                region,
84                registry,
85                data,
86                &mut piece_bounding_box,
87                reference_pos,
88                clip,
89                random,
90            ),
91            StructurePiecePayload::Procedural(ProceduralPieceData::Mineshaft(data)) => {
92                Self::place_mineshaft_piece(
93                    region,
94                    registry,
95                    piece_bounding_box,
96                    piece_orientation,
97                    data,
98                    clip,
99                    random,
100                    biome_zoom_seed,
101                )
102            }
103            StructurePiecePayload::Procedural(ProceduralPieceData::NetherFortress(data)) => {
104                Self::place_nether_fortress_piece(
105                    region,
106                    registry,
107                    piece_bounding_box,
108                    piece_orientation,
109                    data,
110                    clip,
111                    random,
112                )
113            }
114            StructurePiecePayload::Procedural(ProceduralPieceData::OceanMonument(data)) => {
115                Self::place_ocean_monument_piece(
116                    region,
117                    registry,
118                    piece_bounding_box,
119                    piece_orientation,
120                    data,
121                    clip,
122                    random,
123                )
124            }
125            StructurePiecePayload::Procedural(ProceduralPieceData::Stronghold(data)) => {
126                Self::place_stronghold_piece(
127                    region,
128                    registry,
129                    piece_bounding_box,
130                    piece_orientation,
131                    data,
132                    clip,
133                    random,
134                )
135            }
136            StructurePiecePayload::Procedural(ProceduralPieceData::BuriedTreasure) => {
137                Self::place_buried_treasure_piece(region, &mut piece_bounding_box, clip, random)
138            }
139            StructurePiecePayload::Procedural(ProceduralPieceData::DesertPyramid(data)) => {
140                Self::place_desert_pyramid_piece(
141                    region,
142                    registry,
143                    &mut piece_bounding_box,
144                    piece_orientation,
145                    data,
146                    clip,
147                    random,
148                )
149            }
150            StructurePiecePayload::Procedural(ProceduralPieceData::JungleTemple(data)) => {
151                Self::place_jungle_temple_piece(
152                    region,
153                    registry,
154                    &mut piece_bounding_box,
155                    piece_orientation,
156                    data,
157                    clip,
158                    random,
159                )
160            }
161            StructurePiecePayload::Procedural(ProceduralPieceData::SwampHut(data)) => {
162                Self::place_swamp_hut_piece(
163                    region,
164                    registry,
165                    &mut piece_bounding_box,
166                    piece_orientation,
167                    data,
168                    clip,
169                    random,
170                )
171            }
172            StructurePiecePayload::Procedural(ProceduralPieceData::Unimplemented) => false,
173        };
174        piece.bounding_box = piece_bounding_box;
175        placed
176    }
177
178    pub(crate) fn after_place_structure(
179        region: &mut WorldGenRegion<'_>,
180        structure: StructureRef,
181        pieces: &mut [StructurePiece],
182        clip: BoundingBox,
183    ) {
184        if structure.structure_type == Identifier::new_static("minecraft", "desert_pyramid") {
185            Self::after_place_desert_pyramid(region, pieces, clip);
186        }
187    }
188
189    const VANILLA_HORIZONTAL_DIRECTIONS: [Direction; 4] = [
190        Direction::North,
191        Direction::East,
192        Direction::South,
193        Direction::West,
194    ];
195
196    pub(super) fn reorient_chest(
197        region: &WorldGenRegion<'_>,
198        pos: BlockPos,
199        state: BlockStateId,
200    ) -> BlockStateId {
201        let mut solid_neighbor = None;
202
203        for direction in Self::VANILLA_HORIZONTAL_DIRECTIONS {
204            let relative_pos = pos.relative(direction);
205            let neighbor = region.block_state(relative_pos);
206            if neighbor.get_block() == &vanilla_blocks::CHEST {
207                return state;
208            }
209
210            if neighbor.is_solid_render() {
211                if solid_neighbor.is_some() {
212                    solid_neighbor = None;
213                    break;
214                }
215                solid_neighbor = Some(direction);
216            }
217        }
218
219        if let Some(direction) = solid_neighbor {
220            return state.set_value(
221                &BlockStateProperties::HORIZONTAL_FACING,
222                direction.opposite(),
223            );
224        }
225
226        let mut lock_dir = state.get_value(&BlockStateProperties::HORIZONTAL_FACING);
227        let mut relative_pos = pos.relative(lock_dir);
228        if region.block_state(relative_pos).is_solid_render() {
229            lock_dir = lock_dir.opposite();
230            relative_pos = pos.relative(lock_dir);
231        }
232        if region.block_state(relative_pos).is_solid_render() {
233            lock_dir = lock_dir.rotate_y_clockwise();
234            relative_pos = pos.relative(lock_dir);
235        }
236        if region.block_state(relative_pos).is_solid_render() {
237            lock_dir = lock_dir.opposite();
238        }
239        state.set_value(&BlockStateProperties::HORIZONTAL_FACING, lock_dir)
240    }
241
242    pub(super) fn create_loot_chest(
243        region: &mut WorldGenRegion<'_>,
244        clip: BoundingBox,
245        random: &mut WorldgenRandom,
246        pos: BlockPos,
247        loot_table: &'static str,
248    ) -> bool {
249        if !clip.contains_blockpos(pos)
250            || region.block_state(pos).get_block() == &vanilla_blocks::CHEST
251        {
252            return false;
253        }
254
255        let state = Self::reorient_chest(region, pos, vanilla_blocks::CHEST.default_state());
256        if !region.set_block_state(pos, state, UpdateFlags::UPDATE_CLIENTS) {
257            return false;
258        }
259
260        Self::set_loot_table_block_entity(
261            region,
262            pos,
263            &vanilla_block_entity_types::CHEST,
264            state,
265            loot_table,
266            random.next_i64(),
267        )
268    }
269
270    pub(super) fn set_loot_table_block_entity(
271        region: &WorldGenRegion<'_>,
272        pos: BlockPos,
273        block_entity_type: BlockEntityTypeRef,
274        state: BlockStateId,
275        loot_table: &'static str,
276        seed: i64,
277    ) -> bool {
278        let mut nbt = NbtCompound::new();
279        nbt.insert("LootTable", loot_table);
280        if seed != 0 {
281            nbt.insert("LootTableSeed", seed);
282        }
283        region.set_block_entity_data(pos, block_entity_type, state, nbt)
284    }
285
286    pub(super) fn set_spawner_entity(
287        region: &WorldGenRegion<'_>,
288        pos: BlockPos,
289        state: BlockStateId,
290        entity_id: &'static str,
291    ) -> bool {
292        let mut entity = NbtCompound::new();
293        entity.insert("id", entity_id);
294
295        let mut spawn_data = NbtCompound::new();
296        spawn_data.insert("entity", NbtTag::Compound(entity));
297
298        let mut nbt = NbtCompound::new();
299        nbt.insert("Delay", 20_i16);
300        nbt.insert("MinSpawnDelay", 200_i16);
301        nbt.insert("MaxSpawnDelay", 800_i16);
302        nbt.insert("SpawnCount", 4_i16);
303        nbt.insert("MaxNearbyEntities", 6_i16);
304        nbt.insert("RequiredPlayerRange", 16_i16);
305        nbt.insert("SpawnRange", 4_i16);
306        nbt.insert("SpawnData", NbtTag::Compound(spawn_data));
307        nbt.insert(
308            "SpawnPotentials",
309            NbtTag::List(NbtList::Compound(Vec::new())),
310        );
311
312        region.set_block_entity_data(pos, &vanilla_block_entity_types::MOB_SPAWNER, state, nbt)
313    }
314
315    pub(super) fn set_brushable_loot_table(
316        region: &WorldGenRegion<'_>,
317        pos: BlockPos,
318        state: BlockStateId,
319        loot_table: &'static str,
320    ) -> bool {
321        Self::set_loot_table_block_entity(
322            region,
323            pos,
324            &vanilla_block_entity_types::BRUSHABLE_BLOCK,
325            state,
326            loot_table,
327            PackedBlockPos::from(pos).as_raw(),
328        )
329    }
330
331    pub(super) const fn orientation_transform(
332        orientation: Option<Direction>,
333    ) -> (StructureMirror, Rotation) {
334        match orientation {
335            None | Some(Direction::North | Direction::Up | Direction::Down) => {
336                (StructureMirror::None, Rotation::None)
337            }
338            Some(Direction::South) => (StructureMirror::LeftRight, Rotation::None),
339            Some(Direction::West) => (StructureMirror::LeftRight, Rotation::Clockwise90),
340            Some(Direction::East) => (StructureMirror::None, Rotation::Clockwise90),
341        }
342    }
343
344    pub(super) fn needs_structure_shape_postprocessing(state: BlockStateId) -> bool {
345        let block = state.get_block();
346        block == &vanilla_blocks::NETHER_BRICK_FENCE
347            || block == &vanilla_blocks::TORCH
348            || block == &vanilla_blocks::WALL_TORCH
349            || block == &vanilla_blocks::OAK_FENCE
350            || block == &vanilla_blocks::SPRUCE_FENCE
351            || block == &vanilla_blocks::DARK_OAK_FENCE
352            || block == &vanilla_blocks::PALE_OAK_FENCE
353            || block == &vanilla_blocks::ACACIA_FENCE
354            || block == &vanilla_blocks::BIRCH_FENCE
355            || block == &vanilla_blocks::JUNGLE_FENCE
356            || block == &vanilla_blocks::LADDER
357            || block == &vanilla_blocks::IRON_BARS
358    }
359}