1use std::sync::Arc;
2
3use glam::{DVec3, IVec3};
4use simdnbt::owned::NbtCompound;
5use steel_registry::blocks::block_state_ext::BlockStateExt;
6use steel_registry::blocks::properties::BlockStateProperties;
7use steel_registry::entity_type::EntityTypeRef;
8use steel_registry::item_stack::ItemStack;
9use steel_registry::vanilla_block_tags::BlockTag;
10use steel_registry::{
11 Registry, vanilla_block_entity_types, vanilla_blocks, vanilla_entities, vanilla_items,
12};
13use steel_utils::random::legacy_random::LegacyRandom;
14use steel_utils::random::worldgen_random::WorldgenRandom;
15use steel_utils::random::{PositionalRandom, Random};
16use steel_utils::{BlockPos, BlockStateId, BoundingBox, Direction, Rotation, types::UpdateFlags};
17
18use crate::chunk::heightmap::HeightmapType;
19use crate::entity::{
20 entities::{ItemFrameEntity, RawEntity},
21 next_entity_id,
22};
23use crate::fluid::FluidStateExt as _;
24use crate::worldgen::region::WorldGenRegion;
25use crate::worldgen::template::{
26 StructureDataMarker, StructurePlaceSettings, StructureProcessorRandom, StructureTemplate,
27};
28use steel_worldgen::structure::{
29 StructureMirror, TemplateMarkerHandling, TemplatePieceData, TemplatePlacementAdjustment,
30 TemplatePlacementClip, TemplatePostProcess, TemplateProcessorList,
31};
32
33use super::StructurePiecePlacer;
34
35impl StructurePiecePlacer {
36 pub(super) fn place_template_piece(
37 region: &mut WorldGenRegion<'_>,
38 registry: &Registry,
39 data: &mut TemplatePieceData,
40 piece_bounding_box: &mut BoundingBox,
41 reference_pos: BlockPos,
42 clip: BoundingBox,
43 random: &mut WorldgenRandom,
44 ) -> bool {
45 if data.marker_handling == TemplateMarkerHandling::DataMarkers {
46 return false;
48 }
49
50 let template = match StructureTemplate::load_vanilla(registry, &data.template_id) {
51 Ok(template) => template,
52 Err(err) => panic!("{err}"),
53 };
54 let position = Self::adjusted_template_position(region, &template, data, random);
55 let mut hardcoded_processors = Vec::new();
56 let processor_list =
57 Self::template_processors(registry, &data.processors, &mut hardcoded_processors);
58 let settings = StructurePlaceSettings {
59 mirror: data.mirror,
60 rotation: data.rotation,
61 rotation_pivot: BlockPos(data.rotation_pivot),
62 bounding_box: clip,
63 processors: processor_list,
64 block_ignore: data.block_ignore,
65 late_block_ignore: data.late_block_ignore,
66 replace_jigsaws: false,
67 projection: None,
68 processor_random: StructureProcessorRandom::Positional,
69 liquid_settings: data.liquid_settings,
70 };
71 let template_box = template.bounding_box_with_transform(
72 position,
73 data.rotation,
74 data.mirror,
75 settings.rotation_pivot,
76 );
77 *piece_bounding_box = template_box;
78 let Some(placement_clip) =
79 Self::template_placement_clip(data.placement_clip, clip, template_box)
80 else {
81 return false;
82 };
83 let settings = StructurePlaceSettings {
84 bounding_box: placement_clip,
85 ..settings
86 };
87 if !template_box.intersects(placement_clip) {
88 return false;
89 }
90
91 let placed = template.place_in_world(
92 region,
93 registry,
94 position,
95 reference_pos,
96 &settings,
97 random,
98 Self::TEMPLATE_UPDATE_FLAGS,
99 );
100 if placed {
101 if !Self::handle_template_data_markers(
102 region,
103 registry,
104 &template,
105 data.marker_handling,
106 position,
107 &settings,
108 random,
109 ) {
110 return false;
111 }
112 template.replace_jigsaw_final_states(region, registry, position, &settings, random);
113 Self::post_process_template_piece(
114 region,
115 registry,
116 data.post_process,
117 &data.processors,
118 position,
119 &settings,
120 template_box,
121 placement_clip,
122 random,
123 );
124 }
125 placed
126 }
127
128 fn adjusted_template_position(
129 region: &WorldGenRegion<'_>,
130 template: &StructureTemplate,
131 data: &mut TemplatePieceData,
132 random: &mut WorldgenRandom,
133 ) -> BlockPos {
134 match &mut data.placement_adjustment {
135 TemplatePlacementAdjustment::None => BlockPos(data.template_position),
136 TemplatePlacementAdjustment::Shipwreck {
137 is_beached,
138 height_adjusted,
139 } => {
140 if !*height_adjusted && !Self::shipwreck_is_too_big_to_fit(template) {
141 let new_y = Self::adjusted_shipwreck_y(
142 region,
143 template,
144 data.template_position,
145 *is_beached,
146 random,
147 );
148 data.template_position.y = new_y;
149 *height_adjusted = true;
150 }
151 BlockPos(data.template_position)
152 }
153 TemplatePlacementAdjustment::Igloo { template_offset } => {
154 Self::adjusted_igloo_position(
155 region,
156 data.template_position,
157 data.mirror,
158 data.rotation,
159 BlockPos(data.rotation_pivot),
160 IVec3::new(template_offset.0, template_offset.1, template_offset.2),
161 )
162 }
163 TemplatePlacementAdjustment::OceanRuin => {
164 Self::adjusted_ocean_ruin_position(region, template, data)
165 }
166 }
167 }
168
169 const fn shipwreck_is_too_big_to_fit(template: &StructureTemplate) -> bool {
170 let size = template.size(Rotation::None);
171 size.x > 32 || size.y > 32
172 }
173
174 fn adjusted_shipwreck_y(
175 region: &WorldGenRegion<'_>,
176 template: &StructureTemplate,
177 position: IVec3,
178 is_beached: bool,
179 random: &mut WorldgenRandom,
180 ) -> i32 {
181 let size = template.size(Rotation::None);
182 let heightmap_type = if is_beached {
183 HeightmapType::WorldSurfaceWg
184 } else {
185 HeightmapType::OceanFloorWg
186 };
187 let base_area = size.x * size.z;
188 if base_area == 0 {
189 return region.height_at(heightmap_type, position.x, position.z);
190 }
191
192 let mut min_y = region.max_y_exclusive();
193 let mut mean = 0;
194 for z in position.z..position.z + size.z {
195 for x in position.x..position.x + size.x {
196 let height = region.height_at(heightmap_type, x, z);
197 mean += height;
198 min_y = min_y.min(height);
199 }
200 }
201 mean /= base_area;
202
203 if is_beached {
204 min_y - size.y / 2 - random.next_i32_bounded(3)
205 } else {
206 mean
207 }
208 }
209
210 fn adjusted_igloo_position(
211 region: &WorldGenRegion<'_>,
212 position: IVec3,
213 mirror: StructureMirror,
214 rotation: Rotation,
215 pivot: BlockPos,
216 template_offset: IVec3,
217 ) -> BlockPos {
218 const IGLOO_GENERATION_HEIGHT: i32 = 90;
219
220 let raw_position = BlockPos(position);
221 let entrance_relative = StructureTemplate::calculate_relative_position(
222 BlockPos(IVec3::new(3 - template_offset.x, 0, -template_offset.z)),
223 mirror,
224 rotation,
225 pivot,
226 );
227 let entrance_pos = raw_position.offset(
228 entrance_relative.x(),
229 entrance_relative.y(),
230 entrance_relative.z(),
231 );
232 let height = region.height_at(
233 HeightmapType::WorldSurfaceWg,
234 entrance_pos.x(),
235 entrance_pos.z(),
236 );
237 raw_position.offset(0, height - IGLOO_GENERATION_HEIGHT - 1, 0)
238 }
239
240 fn adjusted_ocean_ruin_position(
241 region: &WorldGenRegion<'_>,
242 template: &StructureTemplate,
243 data: &mut TemplatePieceData,
244 ) -> BlockPos {
245 let ocean_floor_y = region.height_at(
246 HeightmapType::OceanFloorWg,
247 data.template_position.x,
248 data.template_position.z,
249 );
250 let base = BlockPos(data.template_position.with_y(ocean_floor_y));
251 let size = template.size(Rotation::None);
252 let corner_iv = data
253 .rotation
254 .transform_pos(IVec3::new(size.x - 1, 0, size.z - 1), IVec3::ZERO);
255 let corner = base.offset(corner_iv.x, 0, corner_iv.z);
256 let y = Self::adjusted_ocean_ruin_height(region, base, corner);
257 data.template_position.y = y;
258 BlockPos(data.template_position)
259 }
260
261 fn adjusted_ocean_ruin_height(
262 region: &WorldGenRegion<'_>,
263 base: BlockPos,
264 corner: BlockPos,
265 ) -> i32 {
266 let mut new_y = base.y();
267 let mut min_y = 512;
268 let top_y = new_y - 1;
269 let mut area = 0;
270 let x0 = base.x().min(corner.x());
271 let x1 = base.x().max(corner.x());
272 let z0 = base.z().min(corner.z());
273 let z1 = base.z().max(corner.z());
274
275 for z in z0..=z1 {
276 for x in x0..=x1 {
277 let mut floor_y = base.y() - 1;
278 let mut pos = BlockPos::new(x, floor_y, z);
279 let mut state = region.block_state(pos);
280 while (state.is_air()
281 || Self::is_water_state(state)
282 || state.get_block().has_tag(&BlockTag::ICE))
283 && floor_y > region.min_y() + 1
284 {
285 floor_y -= 1;
286 pos = BlockPos::new(x, floor_y, z);
287 state = region.block_state(pos);
288 }
289
290 min_y = min_y.min(floor_y);
291 if floor_y < top_y - 2 {
292 area += 1;
293 }
294 }
295 }
296
297 let width = (base.x() - corner.x()).abs();
298 if top_y - min_y > 2 && area > width - 2 {
299 new_y = min_y + 1;
300 }
301 new_y
302 }
303
304 fn is_water_state(state: BlockStateId) -> bool {
305 state.get_fluid_state().is_water()
306 }
307
308 fn handle_template_data_markers(
309 region: &mut WorldGenRegion<'_>,
310 registry: &Registry,
311 template: &StructureTemplate,
312 marker_handling: TemplateMarkerHandling,
313 position: BlockPos,
314 settings: &StructurePlaceSettings<'_>,
315 random: &mut WorldgenRandom,
316 ) -> bool {
317 match marker_handling {
318 TemplateMarkerHandling::Ignore => true,
319 TemplateMarkerHandling::DataMarkers => {
320 false
322 }
323 TemplateMarkerHandling::OceanRuin { is_large } => {
324 for marker in template.data_markers(registry, position, settings, random) {
325 Self::handle_ocean_ruin_marker(region, is_large, &marker, random);
326 }
327 true
328 }
329 TemplateMarkerHandling::Shipwreck => {
330 for marker in template.data_markers(registry, position, settings, random) {
331 Self::handle_shipwreck_marker(region, &marker, random);
332 }
333 true
334 }
335 TemplateMarkerHandling::Igloo => {
336 for marker in template.data_markers(registry, position, settings, random) {
337 Self::handle_igloo_marker(region, &marker, random);
338 }
339 true
340 }
341 TemplateMarkerHandling::EndCity => {
342 for marker in template.data_markers(registry, position, settings, random) {
343 Self::handle_end_city_marker(region, settings, &marker, random);
344 }
345 true
346 }
347 TemplateMarkerHandling::WoodlandMansion => {
348 for marker in template.data_markers(registry, position, settings, random) {
349 Self::handle_mansion_marker(region, settings, &marker, random);
350 }
351 true
352 }
353 }
354 }
355
356 fn handle_ocean_ruin_marker(
357 region: &mut WorldGenRegion<'_>,
358 is_large: bool,
359 marker: &StructureDataMarker,
360 random: &mut WorldgenRandom,
361 ) {
362 match marker.metadata.as_str() {
363 "chest" => Self::place_ocean_ruin_marker_chest(region, is_large, marker.pos, random),
364 "drowned" => Self::spawn_ocean_ruin_drowned(region, marker.pos),
365 _ => {}
366 }
367 }
368
369 fn place_ocean_ruin_marker_chest(
370 region: &mut WorldGenRegion<'_>,
371 is_large: bool,
372 pos: BlockPos,
373 random: &mut WorldgenRandom,
374 ) {
375 let waterlogged = Self::is_water_state(region.block_state(pos));
376 let state = vanilla_blocks::CHEST
377 .default_state()
378 .set_value(&BlockStateProperties::WATERLOGGED, waterlogged);
379 let _ = region.set_block_state(pos, state, UpdateFlags::UPDATE_CLIENTS);
380
381 let loot_table = if is_large {
382 "minecraft:chests/underwater_ruin_big"
383 } else {
384 "minecraft:chests/underwater_ruin_small"
385 };
386 let mut nbt = NbtCompound::new();
387 nbt.insert("LootTable", loot_table);
388 nbt.insert("LootTableSeed", random.next_i64());
389 let _ = region.set_block_entity_data(pos, &vanilla_block_entity_types::CHEST, state, nbt);
390 }
391
392 fn spawn_ocean_ruin_drowned(region: &mut WorldGenRegion<'_>, pos: BlockPos) {
393 let entity_pos = DVec3::new(
394 f64::from(pos.x()) + 0.5,
395 f64::from(pos.y()),
396 f64::from(pos.z()) + 0.5,
397 );
398 let entity = Arc::new(RawEntity::new(
399 next_entity_id(),
400 entity_pos,
401 region.weak_world(),
402 &vanilla_entities::DROWNED,
403 ));
404 entity.set_persistence_required();
405 entity.snap_to(entity_pos, 0.0, 0.0);
406 let _ = region.add_fresh_entity(entity);
407
408 let replacement = if pos.y() > region.sea_level() {
409 vanilla_blocks::AIR.default_state()
410 } else {
411 vanilla_blocks::WATER.default_state()
412 };
413 let _ = region.set_block_state(pos, replacement, UpdateFlags::UPDATE_CLIENTS);
414 }
415
416 fn handle_shipwreck_marker(
417 region: &mut WorldGenRegion<'_>,
418 marker: &StructureDataMarker,
419 random: &mut WorldgenRandom,
420 ) {
421 let loot_table = match marker.metadata.as_str() {
422 "map_chest" => "minecraft:chests/shipwreck_map",
423 "treasure_chest" => "minecraft:chests/shipwreck_treasure",
424 "supply_chest" => "minecraft:chests/shipwreck_supply",
425 _ => return,
426 };
427 let chest_pos = marker.pos.below();
428 let state = region.block_state(chest_pos);
429 if state.get_block() != &vanilla_blocks::CHEST {
430 return;
431 }
432
433 let mut nbt = NbtCompound::new();
434 nbt.insert("LootTable", loot_table);
435 nbt.insert("LootTableSeed", random.next_i64());
436 let _ =
437 region.set_block_entity_data(chest_pos, &vanilla_block_entity_types::CHEST, state, nbt);
438 }
439
440 fn handle_igloo_marker(
441 region: &mut WorldGenRegion<'_>,
442 marker: &StructureDataMarker,
443 random: &mut WorldgenRandom,
444 ) {
445 if marker.metadata != "chest" {
446 return;
447 }
448
449 let _ = region.set_block_state(
450 marker.pos,
451 vanilla_blocks::AIR.default_state(),
452 UpdateFlags::UPDATE_ALL,
453 );
454 let chest_pos = marker.pos.below();
455 let state = region.block_state(chest_pos);
456 if state.get_block() != &vanilla_blocks::CHEST {
457 return;
458 }
459
460 let mut nbt = NbtCompound::new();
461 nbt.insert("LootTable", "minecraft:chests/igloo_chest");
462 nbt.insert("LootTableSeed", random.next_i64());
463 let _ =
464 region.set_block_entity_data(chest_pos, &vanilla_block_entity_types::CHEST, state, nbt);
465 }
466
467 fn handle_end_city_marker(
468 region: &mut WorldGenRegion<'_>,
469 settings: &StructurePlaceSettings<'_>,
470 marker: &StructureDataMarker,
471 random: &mut WorldgenRandom,
472 ) {
473 if marker.metadata.starts_with("Chest") {
474 Self::place_end_city_marker_chest(region, marker.pos.below(), random);
475 return;
476 }
477 if !Self::is_in_spawnable_bounds(marker.pos) {
478 return;
479 }
480 if marker.metadata.starts_with("Sentry") {
481 Self::spawn_end_city_shulker(region, marker.pos);
482 } else if marker.metadata.starts_with("Elytra") {
483 let direction = settings.rotation.rotate(Direction::South);
484 Self::spawn_end_city_elytra_frame(region, marker.pos, direction);
485 }
486 }
487
488 fn place_end_city_marker_chest(
489 region: &mut WorldGenRegion<'_>,
490 chest_pos: BlockPos,
491 random: &mut WorldgenRandom,
492 ) {
493 let state = region.block_state(chest_pos);
494 if state.get_block() != &vanilla_blocks::CHEST {
495 return;
496 }
497
498 let mut nbt = NbtCompound::new();
499 nbt.insert("LootTable", "minecraft:chests/end_city_treasure");
500 nbt.insert("LootTableSeed", random.next_i64());
501 let _ =
502 region.set_block_entity_data(chest_pos, &vanilla_block_entity_types::CHEST, state, nbt);
503 }
504
505 fn spawn_end_city_shulker(region: &mut WorldGenRegion<'_>, pos: BlockPos) {
506 let entity_pos = DVec3::new(
507 f64::from(pos.x()) + 0.5,
508 f64::from(pos.y()),
509 f64::from(pos.z()) + 0.5,
510 );
511 let entity = Arc::new(RawEntity::new(
512 next_entity_id(),
513 entity_pos,
514 region.weak_world(),
515 &vanilla_entities::SHULKER,
516 ));
517 entity.snap_to(entity_pos, 0.0, 0.0);
518 let _ = region.add_fresh_entity(entity);
519 }
520
521 fn spawn_end_city_elytra_frame(
522 region: &mut WorldGenRegion<'_>,
523 pos: BlockPos,
524 direction: Direction,
525 ) {
526 let entity = Arc::new(ItemFrameEntity::new_attached(
527 &vanilla_entities::ITEM_FRAME,
528 next_entity_id(),
529 pos,
530 direction,
531 region.weak_world(),
532 ));
533 entity.set_item_with_update(ItemStack::new(&vanilla_items::ELYTRA), false);
534 let _ = region.add_fresh_entity(entity);
535 }
536
537 fn handle_mansion_marker(
538 region: &mut WorldGenRegion<'_>,
539 settings: &StructurePlaceSettings<'_>,
540 marker: &StructureDataMarker,
541 random: &mut WorldgenRandom,
542 ) {
543 if marker.metadata.starts_with("Chest") {
544 let state =
545 Self::mansion_marker_chest_state(settings.rotation, marker.metadata.as_str());
546 Self::place_mansion_marker_chest(region, marker.pos, state, random);
547 return;
548 }
549
550 let (entity_type, count) = match marker.metadata.as_str() {
551 "Mage" => (&vanilla_entities::EVOKER, 1),
552 "Warrior" => (&vanilla_entities::VINDICATOR, 1),
553 "Group of Allays" => (
554 &vanilla_entities::ALLAY,
555 region.random_mut().next_i32_bounded(3) + 1,
556 ),
557 _ => return,
558 };
559 for _ in 0..count {
560 Self::spawn_mansion_marker_mob(region, marker.pos, entity_type);
561 }
562 let _ = region.set_block_state(
563 marker.pos,
564 vanilla_blocks::AIR.default_state(),
565 UpdateFlags::UPDATE_CLIENTS,
566 );
567 }
568
569 fn mansion_marker_chest_state(rotation: Rotation, marker: &str) -> BlockStateId {
570 let facing = match marker {
571 "ChestWest" => Some(rotation.rotate(Direction::West)),
572 "ChestEast" => Some(rotation.rotate(Direction::East)),
573 "ChestSouth" => Some(rotation.rotate(Direction::South)),
574 "ChestNorth" => Some(rotation.rotate(Direction::North)),
575 _ => None,
576 };
577 let state = vanilla_blocks::CHEST.default_state();
578 if let Some(facing) = facing {
579 state.set_value(&BlockStateProperties::HORIZONTAL_FACING, facing)
580 } else {
581 state
582 }
583 }
584
585 fn place_mansion_marker_chest(
586 region: &mut WorldGenRegion<'_>,
587 pos: BlockPos,
588 state: BlockStateId,
589 random: &mut WorldgenRandom,
590 ) {
591 if region.block_state(pos).get_block() == &vanilla_blocks::CHEST {
592 return;
593 }
594 if !region.set_block_state(pos, state, UpdateFlags::UPDATE_CLIENTS) {
595 return;
596 }
597
598 let _ = region.set_block_entity_data(
599 pos,
600 &vanilla_block_entity_types::CHEST,
601 state,
602 Self::loot_table_nbt("minecraft:chests/woodland_mansion", random.next_i64()),
603 );
604 }
605
606 fn spawn_mansion_marker_mob(
607 region: &mut WorldGenRegion<'_>,
608 pos: BlockPos,
609 entity_type: EntityTypeRef,
610 ) {
611 let entity_pos = DVec3::new(f64::from(pos.x()), f64::from(pos.y()), f64::from(pos.z()));
612 let entity = Arc::new(RawEntity::new(
613 next_entity_id(),
614 entity_pos,
615 region.weak_world(),
616 entity_type,
617 ));
618 entity.set_persistence_required();
619 entity.snap_to(entity_pos, 0.0, 0.0);
620 let _ = region.add_fresh_entity(entity);
621 }
622
623 fn loot_table_nbt(loot_table: &'static str, seed: i64) -> NbtCompound {
624 let mut nbt = NbtCompound::new();
625 nbt.insert("LootTable", loot_table);
626 nbt.insert("LootTableSeed", seed);
627 nbt
628 }
629
630 const fn is_in_spawnable_bounds(pos: BlockPos) -> bool {
631 pos.y() >= -20_000_000
632 && pos.y() < 20_000_000
633 && pos.x() >= -30_000_000
634 && pos.z() >= -30_000_000
635 && pos.x() < 30_000_000
636 && pos.z() < 30_000_000
637 }
638
639 fn template_placement_clip(
640 placement_clip: TemplatePlacementClip,
641 center_clip: BoundingBox,
642 template_box: BoundingBox,
643 ) -> Option<BoundingBox> {
644 match placement_clip {
645 TemplatePlacementClip::CenterChunk => Some(center_clip),
646 TemplatePlacementClip::CenterChunkExpandedToTemplate => {
647 Some(BoundingBox::encapsulating(¢er_clip, &template_box))
648 }
649 TemplatePlacementClip::CenterChunkContainsTemplateCenterExpandedToTemplate => {
650 if center_clip.contains(template_box.center()) {
651 Some(BoundingBox::encapsulating(¢er_clip, &template_box))
652 } else {
653 None
654 }
655 }
656 }
657 }
658
659 #[expect(
660 clippy::too_many_arguments,
661 reason = "postprocess needs the same placement context as vanilla TemplateStructurePiece after block placement"
662 )]
663 fn post_process_template_piece(
664 region: &mut WorldGenRegion<'_>,
665 registry: &Registry,
666 post_process: TemplatePostProcess,
667 processors: &TemplateProcessorList,
668 position: BlockPos,
669 settings: &StructurePlaceSettings<'_>,
670 template_box: BoundingBox,
671 placement_clip: BoundingBox,
672 random: &mut WorldgenRandom,
673 ) {
674 match post_process {
675 TemplatePostProcess::None => {}
676 TemplatePostProcess::NetherFossil => {
677 Self::place_nether_fossil_dried_ghast(
678 region,
679 registry,
680 template_box,
681 placement_clip,
682 );
683 }
684 TemplatePostProcess::IglooTop => {
685 Self::post_process_igloo_top(region, position, settings);
686 }
687 TemplatePostProcess::RuinedPortal => {
688 let TemplateProcessorList::RuinedPortal {
689 vertical_placement,
690 properties,
691 } = processors
692 else {
693 panic!("ruined portal postprocess requires ruined portal processors");
694 };
695 Self::post_process_ruined_portal(
696 region,
697 *vertical_placement,
698 *properties,
699 template_box,
700 random,
701 );
702 }
703 }
704 }
705
706 fn post_process_igloo_top(
707 region: &mut WorldGenRegion<'_>,
708 position: BlockPos,
709 settings: &StructurePlaceSettings<'_>,
710 ) {
711 let trapdoor_relative = StructureTemplate::calculate_relative_position(
712 BlockPos(IVec3::new(3, 0, 5)),
713 settings.mirror,
714 settings.rotation,
715 settings.rotation_pivot,
716 );
717 let trapdoor_pos = position.offset(
718 trapdoor_relative.x(),
719 trapdoor_relative.y(),
720 trapdoor_relative.z(),
721 );
722 let below_state = region.block_state(trapdoor_pos.below());
723 if below_state.is_air() || below_state.get_block() == &vanilla_blocks::LADDER {
724 return;
725 }
726
727 let _ = region.set_block_state(
728 trapdoor_pos,
729 vanilla_blocks::SNOW_BLOCK.default_state(),
730 UpdateFlags::UPDATE_ALL,
731 );
732 }
733
734 fn place_nether_fossil_dried_ghast(
735 region: &mut WorldGenRegion<'_>,
736 registry: &Registry,
737 fossil_box: BoundingBox,
738 placement_clip: BoundingBox,
739 ) {
740 let center = fossil_box.center();
741 let mut seed_random = LegacyRandom::from_seed(region.seed() as u64);
742 let splitter = seed_random.next_positional();
743 let mut positional_random = splitter.at(center.x, center.y, center.z);
744 if positional_random.next_f32() >= 0.5 {
745 return;
746 }
747
748 let pos = BlockPos::new(
749 fossil_box.min_x() + positional_random.next_i32_bounded(fossil_box.width()),
750 fossil_box.min_y(),
751 fossil_box.min_z() + positional_random.next_i32_bounded(fossil_box.depth()),
752 );
753 if !placement_clip.contains_blockpos(pos) {
754 return;
755 }
756 if !region.block_state(pos).is_air() {
757 return;
758 }
759
760 let rotation = Rotation::get_random(&mut positional_random);
761 let state = Self::dried_ghast_state(registry, rotation);
762 let _ = region.set_block_state(pos, state, Self::TEMPLATE_UPDATE_FLAGS);
763 }
764
765 fn dried_ghast_state(registry: &Registry, rotation: Rotation) -> BlockStateId {
766 let facing = rotation.rotate(Direction::North);
767 let Some(state) = registry.blocks.state_id_from_block_defaulted_properties(
768 &vanilla_blocks::DRIED_GHAST,
769 [("facing", facing.as_str())],
770 ) else {
771 panic!("dried_ghast missing vanilla facing property");
772 };
773 state
774 }
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780 use crate::behavior::init_behaviors;
781 use steel_registry::init_vanilla_registry;
782
783 #[test]
784 fn center_gated_expanded_clip_requires_template_center_inside_center_chunk() {
785 let center_clip = BoundingBox::new(IVec3::new(0, -64, 0), IVec3::new(15, 319, 15));
786 let centered_template = BoundingBox::new(IVec3::new(0, 70, 0), IVec3::new(15, 80, 15));
787 let outside_template = BoundingBox::new(IVec3::new(16, 70, 8), IVec3::new(31, 80, 23));
788
789 assert_eq!(
790 StructurePiecePlacer::template_placement_clip(
791 TemplatePlacementClip::CenterChunkContainsTemplateCenterExpandedToTemplate,
792 center_clip,
793 centered_template,
794 ),
795 Some(BoundingBox::encapsulating(¢er_clip, ¢ered_template)),
796 );
797 assert_eq!(
798 StructurePiecePlacer::template_placement_clip(
799 TemplatePlacementClip::CenterChunkContainsTemplateCenterExpandedToTemplate,
800 center_clip,
801 outside_template,
802 ),
803 None,
804 );
805 }
806
807 #[test]
808 fn marker_water_check_uses_fluid_state_for_seagrass() {
809 init_vanilla_registry();
810 init_behaviors();
811
812 assert!(StructurePiecePlacer::is_water_state(
813 vanilla_blocks::SEAGRASS.default_state()
814 ));
815 }
816}