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 std::sync::Weak;
23
24use glam::DVec3;
25use simdnbt::owned::{NbtCompound, NbtList, NbtTag};
26use steel_registry::block_entity_type::BlockEntityTypeRef;
27use steel_registry::blocks::block_state_ext::BlockStateExt as _;
28use steel_registry::blocks::properties::BlockStateProperties;
29use steel_registry::entity_type::EntityTypeRef;
30use steel_registry::structure::StructureRef;
31use steel_registry::{Registry, vanilla_block_entity_types, vanilla_blocks};
32use steel_utils::random::Random;
33use steel_utils::random::worldgen_random::WorldgenRandom;
34use steel_utils::{
35    BlockPos, BlockStateId, BoundingBox, Direction, Identifier, PackedBlockPos, Rotation,
36    types::UpdateFlags,
37};
38
39use crate::entity::{ENTITIES, SharedEntity, next_entity_id};
40use crate::world::World;
41use crate::worldgen::region::WorldGenRegion;
42use steel_worldgen::structure::{
43    ProceduralPieceData, StructureMirror, StructurePiece, StructurePiecePayload,
44};
45
46pub(crate) struct StructurePiecePlacer;
47
48impl StructurePiecePlacer {
49    /// Vanilla jigsaw pool-element placement flags: `UPDATE_CLIENTS | UPDATE_KNOWN_SHAPE`.
50    pub(crate) const JIGSAW_UPDATE_FLAGS: UpdateFlags =
51        UpdateFlags::UPDATE_CLIENTS.union(UpdateFlags::UPDATE_KNOWN_SHAPE);
52    /// Vanilla template-piece placement flags: `UPDATE_CLIENTS`.
53    pub(crate) const TEMPLATE_UPDATE_FLAGS: UpdateFlags = UpdateFlags::UPDATE_CLIENTS;
54
55    /// Creates a structure mob when Steel implements its concrete type.
56    ///
57    /// Unsupported types are skipped instead of leaving behaviorless entities in generated
58    /// chunks. Completed structure placement does not retry these skipped spawns.
59    /// Structure spawn finalization remains tracked in `SteelMC` issue #564.
60    fn create_mob(
61        entity_type: EntityTypeRef,
62        position: DVec3,
63        world: Weak<World>,
64        persistence_required: bool,
65    ) -> Option<SharedEntity> {
66        let Some(entity) = ENTITIES.create(entity_type, next_entity_id(), position, world) else {
67            log::warn!(
68                "Skipping unsupported structure entity spawn for {} at {position:?}",
69                entity_type.key
70            );
71            return None;
72        };
73        let Some(mob) = entity.as_mob() else {
74            log::warn!(
75                "Skipping structure mob spawn for {} at {position:?}: registered implementation is not a mob",
76                entity_type.key
77            );
78            return None;
79        };
80
81        if persistence_required {
82            mob.set_persistence_required();
83        }
84        entity.snap_to(position, 0.0, 0.0);
85        Some(entity)
86    }
87
88    /// Places one already-clipped structure piece.
89    ///
90    /// Returns whether the vanilla placement call succeeded. Later milestones
91    /// must implement each remaining payload variant completely before it can
92    /// return `true`.
93    #[expect(
94        clippy::too_many_lines,
95        reason = "single dispatch boundary for all structure piece payload families"
96    )]
97    pub(crate) fn place_piece(
98        region: &mut WorldGenRegion<'_>,
99        registry: &Registry,
100        piece: &mut StructurePiece,
101        reference_pos: BlockPos,
102        clip: BoundingBox,
103        random: &mut WorldgenRandom,
104        biome_zoom_seed: i64,
105    ) -> bool {
106        let mut piece_bounding_box = piece.bounding_box;
107        let piece_orientation = piece.orientation;
108        let placed = match &mut piece.payload {
109            StructurePiecePayload::Jigsaw(data) => Self::place_pool_element(
110                region,
111                registry,
112                &data.pool_element,
113                BlockPos::new(data.position.x, data.position.y, data.position.z),
114                reference_pos,
115                data.rotation,
116                clip,
117                random,
118                data.liquid_settings,
119                biome_zoom_seed,
120            ),
121            StructurePiecePayload::Template(data) => Self::place_template_piece(
122                region,
123                registry,
124                data,
125                &mut piece_bounding_box,
126                reference_pos,
127                clip,
128                random,
129            ),
130            StructurePiecePayload::Procedural(ProceduralPieceData::Mineshaft(data)) => {
131                Self::place_mineshaft_piece(
132                    region,
133                    registry,
134                    piece_bounding_box,
135                    piece_orientation,
136                    data,
137                    clip,
138                    random,
139                    biome_zoom_seed,
140                )
141            }
142            StructurePiecePayload::Procedural(ProceduralPieceData::NetherFortress(data)) => {
143                Self::place_nether_fortress_piece(
144                    region,
145                    registry,
146                    piece_bounding_box,
147                    piece_orientation,
148                    data,
149                    clip,
150                    random,
151                )
152            }
153            StructurePiecePayload::Procedural(ProceduralPieceData::OceanMonument(data)) => {
154                Self::place_ocean_monument_piece(
155                    region,
156                    registry,
157                    piece_bounding_box,
158                    piece_orientation,
159                    data,
160                    clip,
161                    random,
162                )
163            }
164            StructurePiecePayload::Procedural(ProceduralPieceData::Stronghold(data)) => {
165                Self::place_stronghold_piece(
166                    region,
167                    registry,
168                    piece_bounding_box,
169                    piece_orientation,
170                    data,
171                    clip,
172                    random,
173                )
174            }
175            StructurePiecePayload::Procedural(ProceduralPieceData::BuriedTreasure) => {
176                Self::place_buried_treasure_piece(region, &mut piece_bounding_box, clip, random)
177            }
178            StructurePiecePayload::Procedural(ProceduralPieceData::DesertPyramid(data)) => {
179                Self::place_desert_pyramid_piece(
180                    region,
181                    registry,
182                    &mut piece_bounding_box,
183                    piece_orientation,
184                    data,
185                    clip,
186                    random,
187                )
188            }
189            StructurePiecePayload::Procedural(ProceduralPieceData::JungleTemple(data)) => {
190                Self::place_jungle_temple_piece(
191                    region,
192                    registry,
193                    &mut piece_bounding_box,
194                    piece_orientation,
195                    data,
196                    clip,
197                    random,
198                )
199            }
200            StructurePiecePayload::Procedural(ProceduralPieceData::SwampHut(data)) => {
201                Self::place_swamp_hut_piece(
202                    region,
203                    registry,
204                    &mut piece_bounding_box,
205                    piece_orientation,
206                    data,
207                    clip,
208                    random,
209                )
210            }
211            StructurePiecePayload::Procedural(ProceduralPieceData::Unimplemented) => false,
212        };
213        piece.bounding_box = piece_bounding_box;
214        placed
215    }
216
217    pub(crate) fn after_place_structure(
218        region: &mut WorldGenRegion<'_>,
219        structure: StructureRef,
220        pieces: &mut [StructurePiece],
221        clip: BoundingBox,
222    ) {
223        if structure.structure_type == Identifier::new_static("minecraft", "desert_pyramid") {
224            Self::after_place_desert_pyramid(region, pieces, clip);
225        }
226    }
227
228    const VANILLA_HORIZONTAL_DIRECTIONS: [Direction; 4] = [
229        Direction::North,
230        Direction::East,
231        Direction::South,
232        Direction::West,
233    ];
234
235    pub(super) fn reorient_chest(
236        region: &WorldGenRegion<'_>,
237        pos: BlockPos,
238        state: BlockStateId,
239    ) -> BlockStateId {
240        let mut solid_neighbor = None;
241
242        for direction in Self::VANILLA_HORIZONTAL_DIRECTIONS {
243            let relative_pos = pos.relative(direction);
244            let neighbor = region.block_state(relative_pos);
245            if neighbor.get_block() == &vanilla_blocks::CHEST {
246                return state;
247            }
248
249            if neighbor.is_solid_render() {
250                if solid_neighbor.is_some() {
251                    solid_neighbor = None;
252                    break;
253                }
254                solid_neighbor = Some(direction);
255            }
256        }
257
258        if let Some(direction) = solid_neighbor {
259            return state.set_value(
260                &BlockStateProperties::HORIZONTAL_FACING,
261                direction.opposite(),
262            );
263        }
264
265        let mut lock_dir = state.get_value(&BlockStateProperties::HORIZONTAL_FACING);
266        let mut relative_pos = pos.relative(lock_dir);
267        if region.block_state(relative_pos).is_solid_render() {
268            lock_dir = lock_dir.opposite();
269            relative_pos = pos.relative(lock_dir);
270        }
271        if region.block_state(relative_pos).is_solid_render() {
272            lock_dir = lock_dir.rotate_y_clockwise();
273            relative_pos = pos.relative(lock_dir);
274        }
275        if region.block_state(relative_pos).is_solid_render() {
276            lock_dir = lock_dir.opposite();
277        }
278        state.set_value(&BlockStateProperties::HORIZONTAL_FACING, lock_dir)
279    }
280
281    pub(super) fn create_loot_chest(
282        region: &mut WorldGenRegion<'_>,
283        clip: BoundingBox,
284        random: &mut WorldgenRandom,
285        pos: BlockPos,
286        loot_table: &'static str,
287    ) -> bool {
288        if !clip.contains_blockpos(pos)
289            || region.block_state(pos).get_block() == &vanilla_blocks::CHEST
290        {
291            return false;
292        }
293
294        let state = Self::reorient_chest(region, pos, vanilla_blocks::CHEST.default_state());
295        if !region.set_block_state(pos, state, UpdateFlags::UPDATE_CLIENTS) {
296            return false;
297        }
298
299        Self::set_loot_table_block_entity(
300            region,
301            pos,
302            &vanilla_block_entity_types::CHEST,
303            state,
304            loot_table,
305            random.next_i64(),
306        )
307    }
308
309    pub(super) fn set_loot_table_block_entity(
310        region: &WorldGenRegion<'_>,
311        pos: BlockPos,
312        block_entity_type: BlockEntityTypeRef,
313        state: BlockStateId,
314        loot_table: &'static str,
315        seed: i64,
316    ) -> bool {
317        let mut nbt = NbtCompound::new();
318        nbt.insert("LootTable", loot_table);
319        if seed != 0 {
320            nbt.insert("LootTableSeed", seed);
321        }
322        region.set_block_entity_data(pos, block_entity_type, state, nbt)
323    }
324
325    pub(super) fn set_spawner_entity(
326        region: &WorldGenRegion<'_>,
327        pos: BlockPos,
328        state: BlockStateId,
329        entity_id: &'static str,
330    ) -> bool {
331        let mut entity = NbtCompound::new();
332        entity.insert("id", entity_id);
333
334        let mut spawn_data = NbtCompound::new();
335        spawn_data.insert("entity", NbtTag::Compound(entity));
336
337        let mut nbt = NbtCompound::new();
338        nbt.insert("Delay", 20_i16);
339        nbt.insert("MinSpawnDelay", 200_i16);
340        nbt.insert("MaxSpawnDelay", 800_i16);
341        nbt.insert("SpawnCount", 4_i16);
342        nbt.insert("MaxNearbyEntities", 6_i16);
343        nbt.insert("RequiredPlayerRange", 16_i16);
344        nbt.insert("SpawnRange", 4_i16);
345        nbt.insert("SpawnData", NbtTag::Compound(spawn_data));
346        nbt.insert(
347            "SpawnPotentials",
348            NbtTag::List(NbtList::Compound(Vec::new())),
349        );
350
351        region.set_block_entity_data(pos, &vanilla_block_entity_types::MOB_SPAWNER, state, nbt)
352    }
353
354    pub(super) fn set_brushable_loot_table(
355        region: &WorldGenRegion<'_>,
356        pos: BlockPos,
357        state: BlockStateId,
358        loot_table: &'static str,
359    ) -> bool {
360        Self::set_loot_table_block_entity(
361            region,
362            pos,
363            &vanilla_block_entity_types::BRUSHABLE_BLOCK,
364            state,
365            loot_table,
366            PackedBlockPos::from(pos).as_raw(),
367        )
368    }
369
370    pub(super) const fn orientation_transform(
371        orientation: Option<Direction>,
372    ) -> (StructureMirror, Rotation) {
373        match orientation {
374            None | Some(Direction::North | Direction::Up | Direction::Down) => {
375                (StructureMirror::None, Rotation::None)
376            }
377            Some(Direction::South) => (StructureMirror::LeftRight, Rotation::None),
378            Some(Direction::West) => (StructureMirror::LeftRight, Rotation::Clockwise90),
379            Some(Direction::East) => (StructureMirror::None, Rotation::Clockwise90),
380        }
381    }
382
383    pub(super) fn needs_structure_shape_postprocessing(state: BlockStateId) -> bool {
384        let block = state.get_block();
385        block == &vanilla_blocks::NETHER_BRICK_FENCE
386            || block == &vanilla_blocks::TORCH
387            || block == &vanilla_blocks::WALL_TORCH
388            || block == &vanilla_blocks::OAK_FENCE
389            || block == &vanilla_blocks::SPRUCE_FENCE
390            || block == &vanilla_blocks::DARK_OAK_FENCE
391            || block == &vanilla_blocks::PALE_OAK_FENCE
392            || block == &vanilla_blocks::ACACIA_FENCE
393            || block == &vanilla_blocks::BIRCH_FENCE
394            || block == &vanilla_blocks::JUNGLE_FENCE
395            || block == &vanilla_blocks::LADDER
396            || block == &vanilla_blocks::IRON_BARS
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use steel_registry::vanilla_entities;
403
404    use super::*;
405    use crate::bootstrap::init_globals;
406
407    #[test]
408    fn registered_non_mob_is_skipped() {
409        init_globals();
410
411        assert!(
412            StructurePiecePlacer::create_mob(
413                &vanilla_entities::ITEM,
414                DVec3::ZERO,
415                Weak::new(),
416                true,
417            )
418            .is_none()
419        );
420    }
421
422    #[test]
423    fn supported_structure_mob_preserves_placement_and_persistence() {
424        init_globals();
425        let position = DVec3::new(12.5, 64.0, -7.5);
426
427        for persistence_required in [false, true] {
428            let Some(entity) = StructurePiecePlacer::create_mob(
429                &vanilla_entities::PIG,
430                position,
431                Weak::new(),
432                persistence_required,
433            ) else {
434                panic!("registered pig should spawn");
435            };
436            let Some(mob) = entity.as_mob() else {
437                panic!("pig should be a mob");
438            };
439
440            assert_eq!(mob.is_persistence_required(), persistence_required);
441            assert_eq!(entity.position(), position);
442            assert_eq!(entity.base().old_position(), position);
443            assert_eq!(entity.rotation(), (0.0, 0.0));
444        }
445    }
446
447    #[test]
448    fn unsupported_structure_mob_is_skipped() {
449        init_globals();
450
451        assert!(
452            StructurePiecePlacer::create_mob(
453                &vanilla_entities::WITCH,
454                DVec3::ZERO,
455                Weak::new(),
456                true,
457            )
458            .is_none()
459        );
460    }
461}