Skip to main content

steel_core/worldgen/structure/piece_placer/
template_piece.rs

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            // TODO: Add family-specific data marker dispatch before enabling these pieces.
45            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                // TODO: Add family-specific data marker dispatch before enabling these pieces.
319                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(&center_clip, &template_box))
633            }
634            TemplatePlacementClip::CenterChunkContainsTemplateCenterExpandedToTemplate => {
635                if center_clip.contains(template_box.center()) {
636                    Some(BoundingBox::encapsulating(&center_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(&center_clip, &centered_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}