Skip to main content

steel_core/worldgen/template/
placement.rs

1use super::*;
2use steel_math::{DEGREE_90, DEGREE_180, DEGREE_270, wrap_degrees};
3use steel_utils::nbt::NbtNumeric;
4
5impl StructureTemplate {
6    pub(crate) fn bounding_box(&self, pos: BlockPos, rotation: Rotation) -> BoundingBox {
7        rotation.get_bounding_box(pos.0, self.size)
8    }
9
10    pub(crate) fn bounding_box_with_transform(
11        &self,
12        position: BlockPos,
13        rotation: Rotation,
14        mirror: StructureMirror,
15        pivot: BlockPos,
16    ) -> BoundingBox {
17        let corner1 = Self::calculate_relative_position(BlockPos::ZERO, mirror, rotation, pivot);
18        let corner2 =
19            Self::calculate_relative_position(BlockPos(self.size - 1), mirror, rotation, pivot);
20        BoundingBox::new(position.0 + corner1.0, position.0 + corner2.0)
21    }
22
23    pub(crate) const fn calculate_relative_position(
24        pos: BlockPos,
25        mirror: StructureMirror,
26        rotation: Rotation,
27        pivot: BlockPos,
28    ) -> BlockPos {
29        let (x, z) = match mirror {
30            StructureMirror::None => (pos.x(), pos.z()),
31            StructureMirror::FrontBack => (-pos.x(), pos.z()),
32            StructureMirror::LeftRight => (pos.x(), -pos.z()),
33        };
34        let pos = rotation.transform_pos(IVec3::new(x, pos.y(), z), pivot.0);
35        BlockPos(pos)
36    }
37
38    pub(super) fn transform_entity_position(
39        pos: DVec3,
40        mirror: StructureMirror,
41        rotation: Rotation,
42        pivot: BlockPos,
43    ) -> DVec3 {
44        let mut x = pos.x;
45        let y = pos.y;
46        let mut z = pos.z;
47        match mirror {
48            StructureMirror::LeftRight => z = 1.0 - z,
49            StructureMirror::FrontBack => x = 1.0 - x,
50            StructureMirror::None => {}
51        }
52
53        let pivot_x = f64::from(pivot.x());
54        let pivot_z = f64::from(pivot.z());
55        match rotation {
56            Rotation::CounterClockwise90 => {
57                DVec3::new(pivot_x - pivot_z + z, y, pivot_x + pivot_z + 1.0 - x)
58            }
59            Rotation::Clockwise90 => {
60                DVec3::new(pivot_x + pivot_z + 1.0 - z, y, pivot_z - pivot_x + x)
61            }
62            Rotation::Clockwise180 => {
63                DVec3::new(pivot_x + pivot_x + 1.0 - x, y, pivot_z + pivot_z + 1.0 - z)
64            }
65            Rotation::None => DVec3::new(x, y, z),
66        }
67    }
68
69    pub(super) fn transform_entity_rotation(
70        (yaw, pitch): (f32, f32),
71        mirror: StructureMirror,
72        rotation: Rotation,
73    ) -> (f32, f32) {
74        let yaw = wrap_degrees(yaw);
75        let rotated = match rotation {
76            Rotation::Clockwise180 => yaw + DEGREE_180,
77            Rotation::CounterClockwise90 => yaw + DEGREE_270,
78            Rotation::Clockwise90 => yaw + DEGREE_90,
79            Rotation::None => yaw,
80        };
81        let mirrored = match mirror {
82            StructureMirror::FrontBack => -yaw,
83            StructureMirror::LeftRight => DEGREE_180 - yaw,
84            StructureMirror::None => yaw,
85        };
86        (rotated + mirrored - yaw, pitch)
87    }
88
89    pub(super) fn transform_entity_additional_nbt(
90        nbt: &mut NbtCompound,
91        mirror: StructureMirror,
92        rotation: Rotation,
93    ) {
94        let Some(facing) = Self::entity_facing(nbt) else {
95            return;
96        };
97        let facing = Self::mirror_direction(rotation.rotate(facing), mirror);
98        let _ = nbt.remove("Facing");
99        nbt.insert("Facing", Self::entity_facing_value(facing));
100    }
101
102    pub(super) fn entity_facing(nbt: &NbtCompound) -> Option<Direction> {
103        nbt.get("Facing")
104            .and_then(NbtNumeric::codec_i8)
105            .map(|value| Direction::from_3d_data_value(i32::from(value)))
106    }
107
108    pub(super) const fn entity_facing_value(direction: Direction) -> i8 {
109        match direction {
110            Direction::Down => 0,
111            Direction::Up => 1,
112            Direction::North => 2,
113            Direction::South => 3,
114            Direction::West => 4,
115            Direction::East => 5,
116        }
117    }
118
119    #[expect(
120        clippy::too_many_arguments,
121        reason = "structure placement call mirrors vanilla template placement context"
122    )]
123    #[expect(
124        clippy::too_many_lines,
125        reason = "template placement follows vanilla's single-pass block placement flow"
126    )]
127    pub(crate) fn place_in_world(
128        &self,
129        region: &mut WorldGenRegion<'_>,
130        registry: &Registry,
131        position: BlockPos,
132        reference_pos: BlockPos,
133        settings: &StructurePlaceSettings<'_>,
134        random: &mut WorldgenRandom,
135        flags: UpdateFlags,
136    ) -> bool {
137        let Some(palette) = self.palette(settings, position, random) else {
138            return false;
139        };
140        if (palette.blocks.is_empty() && self.entities.is_empty())
141            || [self.size.x, self.size.y, self.size.z]
142                .iter()
143                .any(|&axis| axis < 1)
144        {
145            return false;
146        }
147        let mut original_blocks = Vec::with_capacity(palette.blocks.len());
148        let mut processed_blocks = Vec::with_capacity(palette.blocks.len());
149
150        Self::palette_blocks_for_placement(
151            &palette.blocks,
152            position,
153            settings,
154            |block, world_pos| {
155                let original = ProcessedBlockInfo {
156                    template_pos: block.pos,
157                    world_pos: block.pos,
158                    state: block.state,
159                    nbt: block.nbt.clone(),
160                };
161                let processed = ProcessedBlockInfo {
162                    template_pos: block.pos,
163                    world_pos,
164                    state: block.state,
165                    nbt: block.nbt.clone(),
166                };
167
168                if let Some(processed) = Self::process_block(
169                    region,
170                    registry,
171                    &original,
172                    processed,
173                    settings,
174                    reference_pos,
175                    random,
176                ) {
177                    original_blocks.push(original);
178                    processed_blocks.push(processed);
179                }
180            },
181        );
182
183        let processed_blocks = Self::finalize_processing(
184            region,
185            registry,
186            position,
187            reference_pos,
188            settings,
189            &original_blocks,
190            processed_blocks,
191            random,
192        );
193
194        let mut placed_any = false;
195        let mut placed_positions = Vec::with_capacity(processed_blocks.len());
196        let mut min_x = i32::MAX;
197        let mut min_y = i32::MAX;
198        let mut min_z = i32::MAX;
199        let mut max_x = i32::MIN;
200        let mut max_y = i32::MIN;
201        let mut max_z = i32::MIN;
202        let mut to_fill = Vec::new();
203        let mut locked_fluids = Vec::new();
204        let apply_waterlogging = settings.liquid_settings == LiquidSettingsData::ApplyWaterlogging;
205        for processed in processed_blocks {
206            // Always guard placement: the vanilla fallback may enqueue a block outside
207            // `bounding_box` for processor/finalize parity without intending a write here.
208            if !settings.bounding_box.contains_blockpos(processed.world_pos) {
209                continue;
210            }
211
212            let final_state = Self::transform_state(
213                registry,
214                processed.state,
215                settings.mirror,
216                settings.rotation,
217            );
218            let previous_fluid_state =
219                apply_waterlogging.then(|| Self::fluid_state_at(region, processed.world_pos));
220            if processed.nbt.is_some() {
221                let barrier_flags = UpdateFlags::UPDATE_INVISIBLE
222                    | UpdateFlags::UPDATE_KNOWN_SHAPE
223                    | UpdateFlags::UPDATE_SUPPRESS_DROPS
224                    | UpdateFlags::UPDATE_SKIP_BLOCK_ENTITY_SIDEEFFECTS
225                    | UpdateFlags::UPDATE_SKIP_ON_PLACE;
226                let _ = region.set_block_state(
227                    processed.world_pos,
228                    vanilla_blocks::BARRIER.default_state(),
229                    barrier_flags,
230                );
231            }
232
233            if !region.set_block_state(processed.world_pos, final_state, flags) {
234                continue;
235            }
236            placed_any = true;
237            min_x = min_x.min(processed.world_pos.x());
238            min_y = min_y.min(processed.world_pos.y());
239            min_z = min_z.min(processed.world_pos.z());
240            max_x = max_x.max(processed.world_pos.x());
241            max_y = max_y.max(processed.world_pos.y());
242            max_z = max_z.max(processed.world_pos.z());
243            placed_positions.push(processed.world_pos);
244
245            if let Some(mut nbt) = processed.nbt {
246                let block_entity_type =
247                    Self::block_entity_type_for_nbt_or_state(registry, final_state, &nbt);
248                if Self::should_reseed_template_loot(block_entity_type, &nbt) {
249                    nbt.insert("LootTableSeed", NbtTag::Long(random.next_i64()));
250                }
251                Self::place_block_entity(
252                    region,
253                    processed.world_pos,
254                    final_state,
255                    block_entity_type,
256                    nbt,
257                );
258            } else {
259                let _ = region.remove_block_entity(processed.world_pos);
260            }
261
262            if let Some(previous_fluid_state) = previous_fluid_state {
263                if Self::fluid_state_for_block(final_state).is_source() {
264                    locked_fluids.push(processed.world_pos);
265                } else if Self::is_liquid_block_container(final_state) {
266                    let _ = Self::place_liquid(
267                        region,
268                        processed.world_pos,
269                        final_state,
270                        previous_fluid_state,
271                    );
272                    if !previous_fluid_state.is_source() {
273                        to_fill.push(processed.world_pos);
274                    }
275                }
276            }
277        }
278
279        Self::fill_neighbor_source_liquids(region, &mut to_fill, &locked_fluids);
280
281        if placed_any && !flags.contains(UpdateFlags::UPDATE_KNOWN_SHAPE) {
282            Self::update_shape_at_edge(
283                region,
284                flags,
285                &placed_positions,
286                BlockPos::new(min_x, min_y, min_z),
287                BlockPos::new(max_x, max_y, max_z),
288            );
289
290            let placed_update_flags =
291                (flags & !UpdateFlags::UPDATE_NEIGHBORS) | UpdateFlags::UPDATE_KNOWN_SHAPE;
292            for pos in placed_positions {
293                let state = region.block_state(pos);
294                let new_state = Self::update_from_neighbor_shapes(region, state, pos);
295                if state != new_state {
296                    let _ = region.set_block_state(pos, new_state, placed_update_flags);
297                }
298            }
299        }
300
301        self.place_entities(region, position, settings);
302
303        true
304    }
305
306    pub(super) fn place_entities(
307        &self,
308        region: &mut WorldGenRegion<'_>,
309        position: BlockPos,
310        settings: &StructurePlaceSettings<'_>,
311    ) {
312        if self.entities.is_empty() {
313            return;
314        }
315
316        let world_offset = DVec3::new(
317            f64::from(position.x()),
318            f64::from(position.y()),
319            f64::from(position.z()),
320        );
321        for entity in &self.entities {
322            let block_pos = Self::calculate_relative_position(
323                entity.block_pos,
324                settings.mirror,
325                settings.rotation,
326                settings.rotation_pivot,
327            )
328            .offset(position.x(), position.y(), position.z());
329            if !settings.bounding_box.contains_blockpos(block_pos) {
330                continue;
331            }
332
333            let pos = Self::transform_entity_position(
334                entity.pos,
335                settings.mirror,
336                settings.rotation,
337                settings.rotation_pivot,
338            ) + world_offset;
339            let rotation = Self::transform_entity_rotation(
340                entity.rotation,
341                settings.mirror,
342                settings.rotation,
343            );
344            let mut nbt = entity.nbt.clone();
345            Self::transform_entity_additional_nbt(&mut nbt, settings.mirror, settings.rotation);
346
347            let mut nbt_bytes = Vec::new();
348            nbt.write(&mut nbt_bytes);
349            let Ok(nbt) = read_borrowed_compound(&mut Cursor::new(&nbt_bytes)) else {
350                log::warn!(
351                    "failed to reborrow owned NBT for structure template entity {}",
352                    entity.entity_type.key
353                );
354                continue;
355            };
356
357            let Some(runtime_entity) = ENTITIES.create_and_load(
358                EntityLoadRequest {
359                    entity_type: entity.entity_type,
360                    position: pos,
361                    uuid: Uuid::new_v4(),
362                    velocity: entity.velocity,
363                    rotation,
364                    fall_distance: entity.fall_distance,
365                    fire_freeze: entity.fire_freeze,
366                    on_ground: entity.on_ground,
367                    save_data: entity.save_data.clone(),
368                    world: region.weak_world(),
369                },
370                &nbt,
371            ) else {
372                continue;
373            };
374            let _ = region.add_fresh_entity(runtime_entity);
375        }
376    }
377
378    pub(crate) fn replace_jigsaw_final_states(
379        &self,
380        region: &mut WorldGenRegion<'_>,
381        registry: &Registry,
382        position: BlockPos,
383        settings: &StructurePlaceSettings<'_>,
384        random: &mut WorldgenRandom,
385    ) {
386        let Some(palette) = self.palette(settings, position, random) else {
387            return;
388        };
389
390        for block in &palette.blocks {
391            if Self::block_for_state(registry, block.state) != &vanilla_blocks::JIGSAW {
392                continue;
393            }
394            let world_pos = Self::transformed_position(position, block.pos, settings);
395            if !settings.bounding_box.contains_blockpos(world_pos) {
396                continue;
397            }
398            let Some(nbt) = block.nbt.as_ref() else {
399                continue;
400            };
401            let final_state = nbt
402                .string("final_state")
403                .map_or_else(|| "minecraft:air".into(), |value| value.to_str());
404            let state = Self::parse_block_state_string(registry, final_state.as_ref())
405                .unwrap_or_else(|| vanilla_blocks::AIR.default_state());
406            let _ = region.set_block_state(world_pos, state, UpdateFlags::UPDATE_ALL);
407        }
408    }
409
410    pub(crate) fn data_markers(
411        &self,
412        registry: &Registry,
413        position: BlockPos,
414        settings: &StructurePlaceSettings<'_>,
415        random: &mut WorldgenRandom,
416    ) -> Vec<StructureDataMarker> {
417        let Some(palette) = self.palette(settings, position, random) else {
418            return Vec::new();
419        };
420
421        let mut markers = Vec::new();
422        for block in &palette.blocks {
423            if Self::block_for_state(registry, block.state) != &vanilla_blocks::STRUCTURE_BLOCK {
424                continue;
425            }
426            let world_pos = Self::transformed_position(position, block.pos, settings);
427            if !settings.bounding_box.contains_blockpos(world_pos) {
428                continue;
429            }
430            let Some(nbt) = block.nbt.as_ref() else {
431                continue;
432            };
433            if nbt
434                .string("mode")
435                .is_none_or(|mode| mode.to_str().as_ref() != "DATA")
436            {
437                continue;
438            }
439            let metadata = nbt
440                .string("metadata")
441                .map_or_default(|metadata| metadata.to_str().into_owned());
442            markers.push(StructureDataMarker {
443                metadata,
444                pos: world_pos,
445            });
446        }
447        markers
448    }
449
450    pub(super) fn update_shape_at_edge(
451        region: &WorldGenRegion<'_>,
452        flags: UpdateFlags,
453        placed_positions: &[BlockPos],
454        min: BlockPos,
455        max: BlockPos,
456    ) {
457        let filled = placed_positions
458            .iter()
459            .map(|pos| (pos.x() - min.x(), pos.y() - min.y(), pos.z() - min.z()))
460            .collect::<BTreeSet<_>>();
461        let x_size = max.x() - min.x() + 1;
462        let y_size = max.y() - min.y() + 1;
463        let z_size = max.z() - min.z() + 1;
464        let edge_flags = flags & !UpdateFlags::UPDATE_NEIGHBORS;
465
466        Self::for_all_shape_faces(
467            x_size,
468            y_size,
469            z_size,
470            |x, y, z| filled.contains(&(x, y, z)),
471            |direction, x, y, z| {
472                let pos = min.offset(x, y, z);
473                let neighbor_pos = pos.relative(direction);
474                let state = region.block_state(pos);
475                let neighbor_state = region.block_state(neighbor_pos);
476                let new_state = BLOCK_BEHAVIORS
477                    .get_behavior(state.get_block())
478                    .update_shape(state, region, pos, direction, neighbor_pos, neighbor_state);
479                if state != new_state {
480                    let _ = region.set_block_state(pos, new_state, edge_flags);
481                }
482
483                let new_neighbor_state = BLOCK_BEHAVIORS
484                    .get_behavior(neighbor_state.get_block())
485                    .update_shape(
486                        neighbor_state,
487                        region,
488                        neighbor_pos,
489                        direction.opposite(),
490                        pos,
491                        new_state,
492                    );
493                if neighbor_state != new_neighbor_state {
494                    let _ = region.set_block_state(neighbor_pos, new_neighbor_state, edge_flags);
495                }
496            },
497        );
498    }
499
500    pub(super) fn update_from_neighbor_shapes(
501        region: &WorldGenRegion<'_>,
502        state: BlockStateId,
503        pos: BlockPos,
504    ) -> BlockStateId {
505        let mut updated = state;
506        for direction in Direction::UPDATE_SHAPE_ORDER {
507            let neighbor_pos = pos.relative(direction);
508            let neighbor_state = region.block_state(neighbor_pos);
509            updated = BLOCK_BEHAVIORS
510                .get_behavior(updated.get_block())
511                .update_shape(
512                    updated,
513                    region,
514                    pos,
515                    direction,
516                    neighbor_pos,
517                    neighbor_state,
518                );
519        }
520        updated
521    }
522
523    pub(super) fn fill_neighbor_source_liquids(
524        region: &WorldGenRegion<'_>,
525        to_fill: &mut Vec<BlockPos>,
526        locked_fluids: &[BlockPos],
527    ) {
528        const DIRECTIONS: [Direction; 5] = [
529            Direction::Up,
530            Direction::North,
531            Direction::East,
532            Direction::South,
533            Direction::West,
534        ];
535
536        let mut filled = true;
537        while filled && !to_fill.is_empty() {
538            filled = false;
539            let mut index = 0;
540            while index < to_fill.len() {
541                let pos = to_fill[index];
542                let mut to_place = Self::fluid_state_at(region, pos);
543                for direction in DIRECTIONS {
544                    if to_place.is_source() {
545                        break;
546                    }
547                    let neighbor_pos = pos.relative(direction);
548                    let neighbor = Self::fluid_state_at(region, neighbor_pos);
549                    if neighbor.is_source() && !locked_fluids.contains(&neighbor_pos) {
550                        to_place = neighbor;
551                    }
552                }
553
554                if to_place.is_source() {
555                    let state = region.block_state(pos);
556                    if Self::is_liquid_block_container(state) {
557                        let _ = Self::place_liquid(region, pos, state, to_place);
558                        filled = true;
559                        to_fill.remove(index);
560                        continue;
561                    }
562                }
563
564                index += 1;
565            }
566        }
567    }
568
569    pub(super) fn fluid_state_at(region: &WorldGenRegion<'_>, pos: BlockPos) -> FluidState {
570        Self::fluid_state_for_block(region.block_state(pos))
571    }
572
573    pub(super) fn fluid_state_for_block(state: BlockStateId) -> FluidState {
574        state.get_fluid_state()
575    }
576
577    pub(super) fn is_liquid_block_container(state: BlockStateId) -> bool {
578        BLOCK_BEHAVIORS
579            .get_behavior(state.get_block())
580            .is_liquid_container(state)
581    }
582
583    pub(super) fn place_liquid(
584        region: &WorldGenRegion<'_>,
585        pos: BlockPos,
586        state: BlockStateId,
587        fluid_state: FluidState,
588    ) -> bool {
589        let behavior = BLOCK_BEHAVIORS.get_behavior(state.get_block());
590        behavior.place_liquid(region, pos, state, fluid_state)
591    }
592
593    pub(super) fn for_all_shape_faces(
594        x_size: i32,
595        y_size: i32,
596        z_size: i32,
597        is_full: impl Fn(i32, i32, i32) -> bool,
598        mut consumer: impl FnMut(Direction, i32, i32, i32),
599    ) {
600        for x in 0..x_size {
601            for y in 0..y_size {
602                let mut last_full = false;
603                for z in 0..=z_size {
604                    let full = z != z_size && is_full(x, y, z);
605                    if !last_full && full {
606                        consumer(Direction::North, x, y, z);
607                    }
608                    if last_full && !full {
609                        consumer(Direction::South, x, y, z - 1);
610                    }
611                    last_full = full;
612                }
613            }
614        }
615
616        for z in 0..z_size {
617            for x in 0..x_size {
618                let mut last_full = false;
619                for y in 0..=y_size {
620                    let full = y != y_size && is_full(x, y, z);
621                    if !last_full && full {
622                        consumer(Direction::Down, x, y, z);
623                    }
624                    if last_full && !full {
625                        consumer(Direction::Up, x, y - 1, z);
626                    }
627                    last_full = full;
628                }
629            }
630        }
631
632        for y in 0..y_size {
633            for z in 0..z_size {
634                let mut last_full = false;
635                for x in 0..=x_size {
636                    let full = x != x_size && is_full(x, y, z);
637                    if !last_full && full {
638                        consumer(Direction::West, x, y, z);
639                    }
640                    if last_full && !full {
641                        consumer(Direction::East, x - 1, y, z);
642                    }
643                    last_full = full;
644                }
645            }
646        }
647    }
648
649    pub(super) fn palette(
650        &self,
651        settings: &StructurePlaceSettings<'_>,
652        position: BlockPos,
653        random: &mut WorldgenRandom,
654    ) -> Option<&StructureTemplatePalette> {
655        if self.palettes.is_empty() {
656            return None;
657        }
658        let Ok(bound) = i32::try_from(self.palettes.len()) else {
659            panic!(
660                "structure template palette count {} exceeds i32 range",
661                self.palettes.len()
662            );
663        };
664        let index = match settings.processor_random {
665            StructureProcessorRandom::Placement => random.next_i32_bounded(bound),
666            StructureProcessorRandom::Positional => {
667                let mut random = LegacyRandom::from_seed(Self::block_pos_seed(position) as u64);
668                random.next_i32_bounded(bound)
669            }
670        };
671        Some(&self.palettes[index as usize])
672    }
673
674    pub(super) fn place_block_entity(
675        region: &mut WorldGenRegion<'_>,
676        pos: BlockPos,
677        state: BlockStateId,
678        block_entity_type: Option<BlockEntityTypeRef>,
679        nbt: NbtCompound,
680    ) {
681        let Some(block_entity_type) = block_entity_type else {
682            return;
683        };
684        let _ = region.set_block_entity_data(pos, block_entity_type, state, nbt);
685    }
686
687    pub(super) fn block_entity_type_for_nbt_or_state(
688        registry: &Registry,
689        state: BlockStateId,
690        nbt: &NbtCompound,
691    ) -> Option<BlockEntityTypeRef> {
692        let block = Self::block_for_state(registry, state);
693        if let Some(id) = nbt.string("id") {
694            let id = Identifier::from_str(id.to_str().as_ref()).ok()?;
695            return registry
696                .block_entity_types
697                .by_key(&id)
698                .filter(|block_entity_type| block_entity_type.is_valid(block));
699        }
700        Self::block_entity_type_for_state(registry, state)
701    }
702
703    pub(super) fn block_entity_type_for_state(
704        registry: &Registry,
705        state: BlockStateId,
706    ) -> Option<BlockEntityTypeRef> {
707        let block = Self::block_for_state(registry, state);
708        if block == &vanilla_blocks::SUSPICIOUS_SAND || block == &vanilla_blocks::SUSPICIOUS_GRAVEL
709        {
710            return Some(&vanilla_block_entity_types::BRUSHABLE_BLOCK);
711        }
712        None
713    }
714
715    pub(super) fn should_reseed_template_loot(
716        block_entity_type: Option<BlockEntityTypeRef>,
717        nbt: &NbtCompound,
718    ) -> bool {
719        nbt.contains("LootTable")
720            && block_entity_type.is_some_and(Self::is_randomizable_container_block_entity)
721    }
722
723    pub(super) fn is_randomizable_container_block_entity(
724        block_entity_type: BlockEntityTypeRef,
725    ) -> bool {
726        let key = &block_entity_type.key;
727        key == &vanilla_block_entity_types::BARREL.key
728            || key == &vanilla_block_entity_types::CHEST.key
729            || key == &vanilla_block_entity_types::TRAPPED_CHEST.key
730            || key == &vanilla_block_entity_types::DISPENSER.key
731            || key == &vanilla_block_entity_types::DROPPER.key
732            || key == &vanilla_block_entity_types::HOPPER.key
733            || key == &vanilla_block_entity_types::SHULKER_BOX.key
734            || key == &vanilla_block_entity_types::CRAFTER.key
735    }
736}