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