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