1mod 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 pub(crate) const JIGSAW_UPDATE_FLAGS: UpdateFlags =
51 UpdateFlags::UPDATE_CLIENTS.union(UpdateFlags::UPDATE_KNOWN_SHAPE);
52 pub(crate) const TEMPLATE_UPDATE_FLAGS: UpdateFlags = UpdateFlags::UPDATE_CLIENTS;
54
55 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 #[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}