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::{
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            // TODO: Add family-specific data marker dispatch before enabling these pieces.
47            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                // TODO: Add family-specific data marker dispatch before enabling these pieces.
321                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(&center_clip, &template_box))
648            }
649            TemplatePlacementClip::CenterChunkContainsTemplateCenterExpandedToTemplate => {
650                if center_clip.contains(template_box.center()) {
651                    Some(BoundingBox::encapsulating(&center_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(&center_clip, &centered_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}