steel_core/behavior/block/mod.rs
1//! Block behavior trait and registry.
2
3use std::sync::{Arc, Weak};
4
5use glam::DVec3;
6use rand::rngs::ThreadRng;
7use smallvec::SmallVec;
8use steel_registry::DyeColor;
9use steel_registry::block_entity_type::BlockEntityTypeRef;
10use steel_registry::blocks::BlockRef;
11use steel_registry::blocks::block_state_ext::BlockStateExt;
12use steel_registry::blocks::properties::{BlockStateProperties, Direction};
13use steel_registry::blocks::shapes::{
14 BooleanOp, ShapeChannel, SupportType, VoxelShape, is_block_local_face_sturdy,
15 is_shape_full_block, join_unoptimized_boxes,
16};
17use steel_registry::enchantment_effect::EnchantmentEffectComponent;
18use steel_registry::entity_type::EntityTypeRef;
19use steel_registry::fluid::{FluidRef, FluidState};
20use steel_registry::item_stack::ItemStack;
21use steel_registry::loot_table::{LootContext, LootTableRef};
22use steel_registry::sound_event::SoundEventRef;
23use steel_registry::vanilla_block_tags::BlockTag;
24use steel_registry::vanilla_entities;
25use steel_registry::{REGISTRY, RegistryEntry, RegistryExt, sound_events, vanilla_blocks};
26use steel_registry::{vanilla_damage_types, vanilla_items};
27use steel_utils::random::legacy_random::LegacyRandom;
28use steel_utils::types::{GameType, InteractionHand, UpdateFlags};
29use steel_utils::value_providers::IntProvider;
30use steel_utils::{BlockLocalAabb, BlockPos, BlockStateId, Identifier, WorldAabb, axis::Axis};
31
32use crate::behavior::BLOCK_BEHAVIORS;
33use crate::behavior::blocks::vegetation::GrowingPlantHeadBehavior;
34use crate::behavior::blocks::vegetation::bonemealable::Bonemealable;
35use crate::behavior::context::{BlockHitResult, BlockPlaceContext, InteractionResult};
36use crate::behavior::{InventoryAccess, PlacementSource};
37use crate::block_entity::{BlockEntity, BlockEntityTicker, SharedBlockEntity};
38use crate::entity::ai::path::PathComputationType;
39use crate::entity::projectile::Projectile;
40use crate::entity::{
41 Entity, InsideBlockEffectCollector, damage::DamageSource, entity_loot_ref, loot_equipment,
42};
43use crate::fluid::is_water_fluid;
44use crate::physics::collide;
45use crate::player::Player;
46use crate::world::game_event::SharedGameEventListener;
47use crate::world::{
48 BlockInteraction, ClipHitResult, ConditionalBlockSetResult, Explosion, LevelAccessor,
49 LevelReader, ScheduledTickAccess, SignalQueryContext, World,
50};
51use steel_registry::vanilla_fluids;
52
53/// Exclusive block-emission limit for the default mob spawn predicate.
54pub(crate) const MAX_MOB_SPAWN_LIGHT_EMISSION: u8 = 14;
55
56pub(crate) fn default_can_be_replaced(
57 state: BlockStateId,
58 context: &BlockPlaceContext<'_>,
59) -> bool {
60 state.is_replaceable()
61 && context.with_item(|item| {
62 item.is_empty() || item.item() != REGISTRY.items.by_block(state.get_block())
63 })
64}
65
66/// Gets random loot from a given loot table reference and other factors, and returns
67/// each item from it in a [`Vec`].
68#[must_use]
69pub(crate) fn drop_from_block_interact_loot_table(
70 key: LootTableRef,
71 interacted_block_state: BlockStateId,
72 _interacted_block_entity: Option<SharedBlockEntity>,
73 tool: Option<&ItemStack>,
74 interacting_entity: Option<&dyn Entity>,
75 rng: &mut ThreadRng,
76) -> Vec<ItemStack> {
77 let mut ctx = LootContext::new(rng).with_block_state(interacted_block_state);
78
79 // TODO: Add the block entity to the context when it can be done.
80
81 let interacting_equipment = interacting_entity.and_then(loot_equipment);
82 if let Some(interacting_entity) = interacting_entity {
83 ctx = ctx.with_interacting_entity(entity_loot_ref(
84 interacting_entity,
85 interacting_equipment.as_ref(),
86 ));
87 }
88
89 if let Some(tool) = tool {
90 ctx = ctx.with_tool(tool);
91 }
92
93 key.get_random_items(&mut ctx)
94}
95
96/// Samples and applies enchantment effects to a block experience drop.
97///
98/// Mirrors vanilla `Block.tryDropExperience`. Mining experience is incidental
99/// live-gameplay randomness, so Steel samples it from an unseeded runtime source.
100pub(crate) fn try_drop_experience(
101 world: &Arc<World>,
102 pos: BlockPos,
103 tool: &ItemStack,
104 experience: &IntProvider,
105) {
106 let mut random = LegacyRandom::from_seed(rand::random());
107 let base_experience = experience.sample(&mut random);
108 let experience = tool.apply_unconditional_enchantment_value_effects(
109 EnchantmentEffectComponent::BlockExperience,
110 base_experience as f32,
111 ) as i32;
112 if experience > 0 {
113 world.pop_experience(pos, experience);
114 }
115}
116
117mod context;
118
119pub use context::{
120 BlockCollisionBoxes, BlockCollisionContext, BlockEntityCreation, BlockLootContext,
121 EntityFallDamage, EntityFallOnContext, EntityFallOnFacts, EntityLandingContext, Fallable,
122 PickupResult, RailBehavior,
123};
124
125/// Data exposed by blocks that support vanilla archaeology brushing.
126#[derive(Clone, Copy, Debug)]
127pub struct BrushableData {
128 /// Block produced after brushing completes.
129 pub turns_into: BlockRef,
130 /// Sound played during a successful brush stroke.
131 pub brush_sound: SoundEventRef,
132 /// Sound played when brushing completes.
133 pub brush_completed_sound: SoundEventRef,
134}
135
136mod waterlogging;
137
138#[cfg(test)]
139use waterlogging::{can_pick_up_drained_waterlogged_state, drained_waterlogged_state};
140pub(crate) use waterlogging::{
141 pickup_waterlogged_block, place_simple_waterlogged_liquid, schedule_placed_liquid_tick,
142 schedule_water_tick_if_waterlogged, simple_waterlogged_is_liquid_container,
143};
144
145mod collision;
146
147pub(crate) use collision::push_entities_up;
148#[cfg(test)]
149use collision::world_aabb_bounds;
150
151/// Trait defining the behavior of a block.
152///
153/// This trait handles all dynamic/functional aspects of blocks:
154/// - Placement logic
155/// - Neighbor updates
156/// - Player interactions
157/// - State changes
158pub trait BlockBehavior: Send + Sync {
159 /// Returns the Rust type name of the concrete behavior implementation.
160 #[cfg(feature = "test-framework")]
161 #[must_use]
162 #[expect(clippy::absolute_paths, reason = "easier for features")]
163 fn type_name(&self) -> &'static str {
164 std::any::type_name::<Self>()
165 }
166
167 /// Called when a player uses an empty bucket on this block.
168 ///
169 /// Should:
170 /// - Remove or modify the block
171 /// - Return the filled bucket stack to give
172 ///
173 /// Return None if pickup failed.
174 #[expect(
175 unused_variables,
176 reason = "default trait implementation ignores all params"
177 )]
178 fn pickup_block(
179 &self,
180 world: &Arc<World>,
181 pos: BlockPos,
182 state: BlockStateId,
183 player: Option<&Player>,
184 ) -> Option<PickupResult> {
185 None
186 }
187 /// Called when a neighboring block changes shape.
188 /// Returns the new state for this block after considering the neighbor change.
189 /// Implementations also own any block or fluid ticks that Vanilla schedules
190 /// from `updateShape`; the world dispatcher does not infer them from the result.
191 fn update_shape(
192 &self,
193 state: BlockStateId,
194 _world: &dyn ScheduledTickAccess,
195 _pos: BlockPos,
196 _direction: Direction,
197 _neighbor_pos: BlockPos,
198 _neighbor_state: BlockStateId,
199 ) -> BlockStateId {
200 state
201 }
202
203 /// Queues indirect neighbor-shape updates after this state changes.
204 ///
205 /// Vanilla's default is a no-op. Redstone wire overrides this for vertical
206 /// corner connections.
207 #[expect(
208 unused_variables,
209 reason = "default trait implementation ignores all params"
210 )]
211 fn update_indirect_neighbour_shapes(
212 &self,
213 state: BlockStateId,
214 world: &Arc<World>,
215 pos: BlockPos,
216 flags: UpdateFlags,
217 update_limit: i32,
218 ) {
219 }
220
221 /// Returns whether this block can survive at the given position.
222 ///
223 /// Vanilla parity: `BlockBehavior.canSurvive(BlockState, LevelReader, BlockPos)`.
224 ///
225 /// Used during placement validation, shape updates (to break unsupported
226 /// blocks), and when removing water from waterlogged blocks. The default
227 /// returns `true`; override for blocks that require physical support
228 /// (torches, buttons, candles, cactus, etc.).
229 fn can_survive(&self, _state: BlockStateId, _world: &dyn LevelReader, _pos: BlockPos) -> bool {
230 true
231 }
232
233 /// Whether this block accepts a mob spawning on or inside it.
234 /// Defaults to full UP support and emission below [`MAX_MOB_SPAWN_LIGHT_EMISSION`].
235 fn is_valid_spawn(
236 &self,
237 state: BlockStateId,
238 world: &dyn LevelReader,
239 pos: BlockPos,
240 _entity_type: EntityTypeRef,
241 ) -> bool {
242 // TODO: extract the correct rules from block spawn rule
243 world.is_face_sturdy_for(state, pos, Direction::Up, SupportType::Full)
244 && state.get_light_emission() < MAX_MOB_SPAWN_LIGHT_EMISSION
245 }
246
247 /// Returns whether this block can be occupied by a forced respawn position
248 fn is_possible_to_respawn_in_this(&self, state: BlockStateId) -> bool {
249 !state.is_solid() && !state.get_block().config.liquid
250 }
251
252 /// Returns whether this block can be replaced by the held item during placement.
253 ///
254 /// Vanilla parity: `BlockState.canBeReplaced(BlockPlaceContext)`.
255 ///
256 /// Default behavior mirrors `BlockBehaviour.canBeReplaced`.
257 fn can_be_replaced(&self, state: BlockStateId, context: &BlockPlaceContext<'_>) -> bool {
258 default_can_be_replaced(state, context)
259 }
260
261 /// Returns the block state to use when placing this block.
262 fn get_state_for_placement(&self, context: &BlockPlaceContext<'_>) -> Option<BlockStateId>;
263
264 /// Called when this block is placed in the world.
265 ///
266 /// # Arguments
267 /// * `state` - The new block state that was placed
268 /// * `world` - The world the block was placed in
269 /// * `pos` - The position where the block was placed
270 /// * `old_state` - The previous block state at this position
271 /// * `moved_by_piston` - Whether the block was moved by a piston
272 #[expect(
273 unused_variables,
274 reason = "default trait implementation ignores all params"
275 )]
276 fn on_place(
277 &self,
278 state: BlockStateId,
279 world: &Arc<World>,
280 pos: BlockPos,
281 old_state: BlockStateId,
282 moved_by_piston: bool,
283 ) {
284 // Default: no-op
285 }
286
287 /// Called by block items after this block has been placed by an entity.
288 ///
289 /// Vanilla parity: `Block.setPlacedBy(Level, BlockPos, BlockState, LivingEntity, ItemStack)`.
290 /// Steel passes the placement source instead of a borrowed stack so the
291 /// caller does not hold the inventory lock while dispatching block behavior
292 /// and synthetic placements can retain a directly supplied stack.
293 /// This is intentionally separate from [`on_place`], which fires for any
294 /// world block mutation.
295 #[expect(
296 unused_variables,
297 reason = "default trait implementation ignores all params"
298 )]
299 fn set_placed_by(
300 &self,
301 state: BlockStateId,
302 world: &Arc<World>,
303 pos: BlockPos,
304 source: &PlacementSource<'_>,
305 ) {
306 // Default: no-op
307 }
308
309 /// Called when a player starts attacking this block.
310 ///
311 /// Vanilla parity: `Block.attack(BlockState, Level, BlockPos, Player)`.
312 #[expect(
313 unused_variables,
314 reason = "default trait implementation ignores all params"
315 )]
316 fn attack(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos, player: &Player) {}
317
318 /// Called after a player destroys this block and drops/effects are processed.
319 ///
320 /// Vanilla parity: `Block.playerDestroy(Level, Player, BlockPos, BlockState, BlockEntity, ItemStack)`.
321 #[expect(
322 unused_variables,
323 reason = "default trait implementation ignores all params"
324 )]
325 fn player_destroy(
326 &self,
327 world: &Arc<World>,
328 player: &Player,
329 pos: BlockPos,
330 state: BlockStateId,
331 block_entity: Option<&SharedBlockEntity>,
332 tool: &ItemStack,
333 ) {
334 // Default: no-op
335 }
336
337 /// Called before a player removes this block.
338 ///
339 /// Vanilla parity: `Block.playerWillDestroy(Level, BlockPos, BlockState, Player)`.
340 /// The returned state is the state used for tool damage and loot after the
341 /// block is removed.
342 #[expect(
343 unused_variables,
344 reason = "default trait implementation ignores all params"
345 )]
346 fn player_will_destroy(
347 &self,
348 state: BlockStateId,
349 world: &Arc<World>,
350 pos: BlockPos,
351 player: &Player,
352 ) -> BlockStateId {
353 state
354 }
355
356 /// Called after a player successfully removes this block.
357 #[expect(
358 unused_variables,
359 reason = "default trait implementation ignores all params"
360 )]
361 fn destroy(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
362 // Default: no-op
363 }
364
365 /// Overrides the loot generated for this block state.
366 ///
367 /// Returning `None` evaluates the state's normal loot table. Returning
368 /// `Some` uses the provided items, including an empty list. This mirrors
369 /// vanilla's per-block `getDrops` override point.
370 #[expect(
371 unused_variables,
372 reason = "default trait implementation ignores all params"
373 )]
374 fn get_drops(
375 &self,
376 state: BlockStateId,
377 context: &BlockLootContext<'_>,
378 ) -> Option<Vec<ItemStack>> {
379 None
380 }
381
382 /// Called for post-break effects such as experience drops.
383 ///
384 /// Vanilla parity: `Block.spawnAfterBreak(BlockState, ServerLevel, BlockPos,
385 /// ItemStack, boolean)`. Normal block destruction invokes this after loot;
386 /// other destruction paths retain their Vanilla-specific ordering. Ore
387 /// experience and similar non-item drops belong here rather than in the
388 /// loot-table override.
389 #[expect(
390 unused_variables,
391 reason = "default trait implementation ignores all params"
392 )]
393 fn spawn_after_break(
394 &self,
395 state: BlockStateId,
396 world: &Arc<World>,
397 pos: BlockPos,
398 tool: &ItemStack,
399 drop_experience: bool,
400 ) {
401 }
402
403 /// Applies Vanilla's default block response to an explosion.
404 fn on_explosion_hit(
405 &self,
406 state: BlockStateId,
407 world: &Arc<World>,
408 pos: BlockPos,
409 explosion: &dyn Explosion,
410 on_hit: &mut dyn FnMut(ItemStack, BlockPos),
411 ) {
412 self.default_on_explosion_hit(state, world, pos, explosion, on_hit);
413 }
414
415 /// Applies the default response from an overriding explosion hook.
416 fn default_on_explosion_hit(
417 &self,
418 state: BlockStateId,
419 world: &Arc<World>,
420 pos: BlockPos,
421 explosion: &dyn Explosion,
422 on_hit: &mut dyn FnMut(ItemStack, BlockPos),
423 ) {
424 if state.is_air() {
425 return;
426 }
427
428 if self.drop_from_explosion(explosion) {
429 let empty_tool = ItemStack::empty();
430 let block_entity = if state.has_block_entity() {
431 world.get_block_entity(pos)
432 } else {
433 None
434 };
435 let context = BlockLootContext::new(world, pos)
436 .with_block_entity(block_entity.as_deref())
437 .with_entity(explosion.direct_source_entity())
438 .with_tool(&empty_tool);
439 let context = if explosion.block_interaction() == BlockInteraction::DestroyWithDecay {
440 context.with_explosion(explosion.radius())
441 } else {
442 context
443 };
444 let drop_experience = explosion
445 .indirect_source_entity()
446 .is_some_and(|entity| entity.as_player().is_some());
447 self.spawn_after_break(state, world, pos, &empty_tool, drop_experience);
448 for stack in World::block_drops(state, &context) {
449 on_hit(stack, pos);
450 }
451 }
452
453 world.set_block(
454 pos,
455 vanilla_blocks::AIR.default_state(),
456 UpdateFlags::UPDATE_ALL,
457 );
458 self.was_exploded(state, world, pos, explosion);
459 }
460
461 /// Called after this block is removed by an explosion.
462 fn was_exploded(
463 &self,
464 _state: BlockStateId,
465 _world: &Arc<World>,
466 _pos: BlockPos,
467 _explosion: &dyn Explosion,
468 ) {
469 }
470
471 /// Returns whether this block contributes normal explosion loot.
472 fn drop_from_explosion(&self, _explosion: &dyn Explosion) -> bool {
473 true
474 }
475
476 /// Called after this block is removed from the world, to affect neighbors.
477 ///
478 /// This is used for things like rails notifying neighbors when removed.
479 ///
480 /// # Arguments
481 /// * `state` - The block state that was removed
482 /// * `world` - The world the block was removed from
483 /// * `pos` - The position where the block was removed
484 /// * `moved_by_piston` - Whether the block was moved by a piston
485 #[expect(
486 unused_variables,
487 reason = "default trait implementation ignores all params"
488 )]
489 fn affect_neighbors_after_removal(
490 &self,
491 state: BlockStateId,
492 world: &Arc<World>,
493 pos: BlockPos,
494 moved_by_piston: bool,
495 ) {
496 // Default: no-op
497 }
498
499 /// Called when a player uses an item on this block.
500 ///
501 /// Returns `TryEmptyHandInteraction` by default to fall through to item use.
502 /// Override this to handle block-specific interactions (e.g., opening chests,
503 /// using buttons, etc.).
504 #[expect(
505 unused_variables,
506 clippy::too_many_arguments,
507 reason = "default trait implementation ignores all params; argument count matches vanilla signature"
508 )]
509 fn use_item_on(
510 &self,
511 state: BlockStateId,
512 world: &Arc<World>,
513 pos: BlockPos,
514 player: &Player,
515 hand: InteractionHand,
516 hit_result: &BlockHitResult,
517 inv: &mut InventoryAccess,
518 ) -> InteractionResult {
519 InteractionResult::TryEmptyHandInteraction
520 }
521
522 /// Called when a player uses this block without an item (or as a fallback
523 /// when `use_item_on` returns `TryEmptyHandInteraction`).
524 ///
525 /// Returns `Pass` by default. Override this for blocks that have interactions
526 /// without needing an item (e.g., buttons, levers, repeaters).
527 #[expect(
528 unused_variables,
529 reason = "default trait implementation ignores all params"
530 )]
531 fn use_without_item(
532 &self,
533 state: BlockStateId,
534 world: &Arc<World>,
535 pos: BlockPos,
536 player: &Player,
537 hit_result: &BlockHitResult,
538 inv: &mut InventoryAccess,
539 ) -> InteractionResult {
540 InteractionResult::Pass
541 }
542
543 /// Called when a neighboring block changes (not shape-related).
544 ///
545 /// This is the Rust equivalent of vanilla's `BlockState.handleNeighborChanged()`.
546 /// Used by redstone components, doors, and other blocks that react to neighbor changes.
547 ///
548 /// # Arguments
549 /// * `state` - The current block state
550 /// * `world` - The world
551 /// * `pos` - Position of this block
552 /// * `source_block` - The block type that changed
553 /// * `moved_by_piston` - Whether the change was caused by a piston
554 #[expect(
555 unused_variables,
556 reason = "default trait implementation ignores all params"
557 )]
558 fn handle_neighbor_changed(
559 &self,
560 state: BlockStateId,
561 world: &Arc<World>,
562 pos: BlockPos,
563 source_block: BlockRef,
564 moved_by_piston: bool,
565 ) {
566 // Default: no-op
567 // Override for redstone components, doors, etc.
568 }
569
570 /// Returns whether this state is a redstone signal source.
571 fn is_signal_source(&self, _state: BlockStateId, _context: SignalQueryContext) -> bool {
572 false
573 }
574
575 /// Returns whether this behavior is a vanilla diode block.
576 ///
577 /// Vanilla uses its `DiodeBlock` class hierarchy for side-input filtering.
578 fn is_diode(&self) -> bool {
579 false
580 }
581
582 /// Returns whether this behavior implements vanilla `TrapDoorBlock` semantics.
583 ///
584 /// Redstone wire uses this class-hierarchy check when deciding whether it
585 /// can climb onto a neighboring block.
586 fn is_trapdoor(&self) -> bool {
587 false
588 }
589
590 /// Returns whether this behavior implements vanilla `BaseRailBlock` semantics.
591 fn is_rail(&self) -> bool {
592 self.as_rail().is_some()
593 }
594
595 /// Returns whether this behavior implements vanilla `PistonBaseBlock` semantics.
596 fn is_piston_base(&self) -> bool {
597 false
598 }
599
600 /// Returns this state's direction-independent redstone signal strength.
601 fn get_own_signal(
602 &self,
603 _state: BlockStateId,
604 _world: &dyn LevelReader,
605 _pos: BlockPos,
606 _context: SignalQueryContext,
607 ) -> i32 {
608 0
609 }
610
611 /// Returns the weak redstone signal emitted toward `direction`.
612 fn get_signal(
613 &self,
614 state: BlockStateId,
615 world: &dyn LevelReader,
616 pos: BlockPos,
617 _direction: Direction,
618 context: SignalQueryContext,
619 ) -> i32 {
620 self.get_own_signal(state, world, pos, context)
621 }
622
623 /// Returns the direct redstone signal emitted toward `direction`.
624 fn get_direct_signal(
625 &self,
626 _state: BlockStateId,
627 _world: &dyn LevelReader,
628 _pos: BlockPos,
629 _direction: Direction,
630 _context: SignalQueryContext,
631 ) -> i32 {
632 0
633 }
634
635 /// Returns whether this state conducts direct redstone power through itself.
636 ///
637 /// Most blocks use extracted state data. Dynamic blocks can override this with
638 /// a live level/position query, matching vanilla's state predicate surface.
639 fn is_redstone_conductor(
640 &self,
641 state: BlockStateId,
642 _world: &dyn LevelReader,
643 _pos: BlockPos,
644 ) -> bool {
645 state.is_static_redstone_conductor()
646 }
647
648 /// Handles a queued server block event.
649 ///
650 /// Mirrors Vanilla `BlockBehaviour.triggerEvent`. Returning `true` publishes
651 /// the corresponding event packet to nearby clients.
652 #[expect(
653 unused_variables,
654 reason = "default trait implementation ignores all params"
655 )]
656 fn trigger_event(
657 &self,
658 state: BlockStateId,
659 world: &Arc<World>,
660 pos: BlockPos,
661 param_a: i32,
662 param_b: i32,
663 ) -> bool {
664 false
665 }
666
667 /// Returns the item stack to give when a player picks this block (middle click).
668 ///
669 /// The default implementation uses the block's registered item association.
670 /// Blocks without an associated item return an empty stack. Override this when
671 /// Vanilla selects the clone item from block state, block entity data, or another rule.
672 ///
673 /// # Arguments
674 /// * `block` - The block being picked
675 /// * `_state` - The block state (some blocks vary pick item based on state)
676 /// * `_block_entity` - An optional block entity (some blocks vary pick item based on block entity data)
677 /// * `_include_data` - Whether to include block entity data (creative + Ctrl)
678 #[expect(
679 unused_variables,
680 reason = "default implementation only uses `block`; state/block_entity/include_data are for overrides"
681 )]
682 fn get_clone_item_stack(
683 &self,
684 block: BlockRef,
685 state: BlockStateId,
686 block_entity: Option<SharedBlockEntity>,
687 include_data: bool,
688 ) -> Option<ItemStack> {
689 Some(ItemStack::new(REGISTRY.items.by_block(block)))
690 }
691
692 /// Returns whether this block state is pathfindable for the supplied vanilla path computation.
693 ///
694 /// Vanilla baseline for `BlockBehaviour.isPathfindable`.
695 fn is_pathfindable(&self, state: BlockStateId, computation_type: PathComputationType) -> bool {
696 match computation_type {
697 PathComputationType::Land | PathComputationType::Air => {
698 !is_shape_full_block(state.get_static_collision_shape())
699 }
700 PathComputationType::Water => is_water_fluid(state.get_fluid_state().fluid_id),
701 }
702 }
703
704 /// Returns whether this behavior implements `BedBlock`
705 fn is_bed(&self) -> bool {
706 false
707 }
708
709 /// Sleep height
710 ///
711 /// `None` rejects sleeping
712 #[expect(unused_variables, reason = "only bed behaviors provide a sleep height")]
713 fn get_sleep_height(
714 &self,
715 state: BlockStateId,
716 world: &dyn LevelReader,
717 pos: BlockPos,
718 ) -> Option<f64> {
719 None
720 }
721
722 /// Sleep exit hook
723 #[expect(
724 unused_variables,
725 reason = "only blocks with leave-sleep behavior override this hook"
726 )]
727 fn on_stop_sleeping(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {}
728
729 /// Returns whether this behavior implements vanilla `LiquidBlock`.
730 fn is_liquid_block(&self) -> bool {
731 false
732 }
733
734 /// Mirrors vanilla `DoorBlock.isWoodenDoor`.
735 ///
736 /// Despite the vanilla name, this returns true for any door block type that
737 /// can be opened by hand.
738 #[expect(
739 unused_variables,
740 reason = "default trait implementation ignores all params"
741 )]
742 fn is_wooden_door(&self, state: BlockStateId) -> bool {
743 false
744 }
745
746 /// Mirrors vanilla `DoorBlock.setOpen` for AI door goals.
747 #[expect(
748 unused_variables,
749 reason = "default trait implementation ignores all params"
750 )]
751 fn set_door_open(
752 &self,
753 state: BlockStateId,
754 world: &Arc<World>,
755 pos: BlockPos,
756 source_entity: Option<&dyn Entity>,
757 open: bool,
758 ) -> bool {
759 false
760 }
761
762 /// Returns this block state's collision shape for the supplied collision context.
763 ///
764 /// Vanilla baseline for `BlockState.getCollisionShape(BlockGetter, BlockPos, CollisionContext)`.
765 #[expect(
766 unused_variables,
767 reason = "default trait implementation uses static registry shape"
768 )]
769 fn default_get_collision_shape(
770 &self,
771 state: BlockStateId,
772 world: &dyn LevelReader,
773 pos: BlockPos,
774 context: BlockCollisionContext,
775 ) -> VoxelShape {
776 state.get_static_collision_shape()
777 }
778
779 /// Returns this block state's collision shape for the supplied collision context.
780 ///
781 /// Overrides that mirror vanilla `super.getCollisionShape(...)` should call
782 /// [`Self::default_get_collision_shape`].
783 fn get_collision_shape(
784 &self,
785 state: BlockStateId,
786 world: &dyn LevelReader,
787 pos: BlockPos,
788 context: BlockCollisionContext,
789 ) -> VoxelShape {
790 self.default_get_collision_shape(state, world, pos, context)
791 }
792
793 /// Returns a block-local translation for this block state's collision shape.
794 ///
795 /// Vanilla baseline for `BlockState.getOffset(BlockPos)` where
796 /// `getCollisionShape` delegates to the offset outline shape.
797 #[expect(
798 unused_variables,
799 reason = "default trait implementation ignores world and collision context"
800 )]
801 fn get_collision_shape_offset(
802 &self,
803 state: BlockStateId,
804 world: &dyn LevelReader,
805 pos: BlockPos,
806 context: BlockCollisionContext,
807 ) -> DVec3 {
808 if state
809 .get_block()
810 .shape_offsets
811 .uses_offset(ShapeChannel::Collision)
812 {
813 return state.get_offset(pos);
814 }
815
816 DVec3::ZERO
817 }
818
819 /// Resolves this block state's collision shape to owned block-local boxes.
820 ///
821 /// Vanilla dynamic-shape blocks may override this directly. Static blocks
822 /// inherit the collision shape and positional offset hooks above.
823 fn get_collision_boxes(
824 &self,
825 state: BlockStateId,
826 world: &dyn LevelReader,
827 pos: BlockPos,
828 context: BlockCollisionContext,
829 ) -> BlockCollisionBoxes {
830 let shape = self.get_collision_shape(state, world, pos, context);
831 if shape.is_empty() {
832 return BlockCollisionBoxes::new();
833 }
834
835 let offset = self.get_collision_shape_offset(state, world, pos, context);
836 shape
837 .into_iter()
838 .map(|aabb| aabb.translate(offset))
839 .collect()
840 }
841
842 /// Resolves vanilla `BlockState.getBlockSupportShape` to owned block-local boxes.
843 ///
844 /// Most states use extracted support shapes. Dynamic blocks can override this
845 /// hook to consult live world data, as vanilla does when its state cache is
846 /// disabled by `dynamicShape()`.
847 #[expect(
848 unused_variables,
849 reason = "the default support shape is extracted and independent of world data"
850 )]
851 fn get_block_support_boxes(
852 &self,
853 state: BlockStateId,
854 world: &dyn LevelReader,
855 pos: BlockPos,
856 ) -> BlockCollisionBoxes {
857 let shape = state.get_static_support_shape();
858 if shape.is_empty() {
859 return BlockCollisionBoxes::new();
860 }
861
862 let offset = if state
863 .get_block()
864 .shape_offsets
865 .uses_offset(ShapeChannel::Support)
866 {
867 state.get_offset(pos)
868 } else {
869 DVec3::ZERO
870 };
871 shape
872 .into_iter()
873 .map(|aabb| aabb.translate(offset))
874 .collect()
875 }
876
877 /// Mirrors vanilla `BlockState.isFaceSturdy(level, pos, direction, supportType)`.
878 ///
879 /// Static states retain their registry fast path. Dynamic states evaluate
880 /// the live support boxes so block entities and other world-dependent
881 /// shapes remain observable to attachment logic.
882 fn is_face_sturdy(
883 &self,
884 state: BlockStateId,
885 world: &dyn LevelReader,
886 pos: BlockPos,
887 direction: Direction,
888 support_type: SupportType,
889 ) -> bool {
890 if !state.get_block().config.dynamic_shape {
891 return state.is_face_sturdy_for_at(pos, direction, support_type);
892 }
893
894 is_block_local_face_sturdy(
895 &self.get_block_support_boxes(state, world, pos),
896 direction,
897 support_type,
898 )
899 }
900
901 /// Returns this block state's shape used by vanilla entity-inside effects.
902 ///
903 /// Vanilla baseline for
904 /// `BlockState.getEntityInsideCollisionShape(BlockGetter, BlockPos, Entity)`.
905 #[expect(
906 unused_variables,
907 reason = "vanilla default is a full block independent of state, world, position, and entity"
908 )]
909 fn default_get_entity_inside_collision_shape(
910 &self,
911 state: BlockStateId,
912 world: &dyn LevelReader,
913 pos: BlockPos,
914 entity: &dyn Entity,
915 ) -> VoxelShape {
916 VoxelShape::FULL_BLOCK
917 }
918
919 /// Returns this block state's shape used by vanilla entity-inside effects.
920 fn get_entity_inside_collision_shape(
921 &self,
922 state: BlockStateId,
923 world: &dyn LevelReader,
924 pos: BlockPos,
925 entity: &dyn Entity,
926 ) -> VoxelShape {
927 self.default_get_entity_inside_collision_shape(state, world, pos, entity)
928 }
929
930 /// Called on random tick for blocks that support random ticking.
931 ///
932 /// This is only called when the block state's extracted metadata marks it as randomly ticking.
933 /// Used for crop growth, grass spread, ice melting, fire behavior, etc.
934 ///
935 /// # Arguments
936 /// * `state` - The current block state
937 /// * `world` - The world the block is in
938 /// * `pos` - The position of the block
939 #[expect(
940 unused_variables,
941 reason = "default trait implementation ignores all params"
942 )]
943 fn random_tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
944 // Default: no-op
945 }
946
947 /// Called when a scheduled tick fires for this block.
948 ///
949 /// Unlike `random_tick`, scheduled ticks are deterministic — they fire after
950 /// a precise delay set by `World::schedule_block_tick`. Used for buttons
951 /// unpressing, repeaters firing, fluids flowing, etc.
952 ///
953 /// # Arguments
954 /// * `state` - The current block state
955 /// * `world` - The world the block is in
956 /// * `pos` - The position of the block
957 #[expect(
958 unused_variables,
959 reason = "default trait implementation ignores all params"
960 )]
961 fn tick(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos) {
962 // Default: no-op
963 }
964
965 /// Called when a projectile hits this block.
966 ///
967 /// Vanilla parity: `BlockState.onProjectileHit(Level, BlockState,
968 /// BlockHitResult, Projectile)`.
969 fn on_projectile_hit(
970 &self,
971 _state: BlockStateId,
972 _world: &Arc<World>,
973 _hit: &ClipHitResult,
974 _projectile: &dyn Projectile,
975 ) {
976 }
977
978 /// Default entity-inside hook.
979 ///
980 /// Overrides that mirror vanilla `super.entityInside(...)` should call
981 /// [`Self::default_entity_inside`].
982 #[expect(
983 unused_variables,
984 reason = "default trait implementation ignores all params"
985 )]
986 fn default_entity_inside(
987 &self,
988 state: BlockStateId,
989 world: &Arc<World>,
990 pos: BlockPos,
991 entity: &dyn Entity,
992 effect_collector: &mut InsideBlockEffectCollector,
993 is_precise: bool,
994 ) {
995 }
996
997 /// Called when an entity is inside this block's collision area.
998 ///
999 /// Used by cactus (damage), fire (ignite), sweet berry bush (slow + damage), etc.
1000 ///
1001 /// # Arguments
1002 /// * `state` - The current block state
1003 /// * `world` - The world
1004 /// * `pos` - The position of the block
1005 /// * `entity` - The entity inside the block
1006 fn entity_inside(
1007 &self,
1008 state: BlockStateId,
1009 world: &Arc<World>,
1010 pos: BlockPos,
1011 entity: &dyn Entity,
1012 effect_collector: &mut InsideBlockEffectCollector,
1013 is_precise: bool,
1014 ) {
1015 self.default_entity_inside(state, world, pos, entity, effect_collector, is_precise);
1016 }
1017
1018 /// Default fall-on hook.
1019 ///
1020 /// Overrides that mirror vanilla `super.fallOn(...)` should call
1021 /// [`Self::default_fall_on`].
1022 #[expect(
1023 unused_variables,
1024 reason = "default trait implementation ignores state, world, and pos"
1025 )]
1026 fn default_fall_on(
1027 &self,
1028 state: BlockStateId,
1029 world: &Arc<World>,
1030 pos: BlockPos,
1031 context: EntityFallOnContext<'_>,
1032 ) -> Option<EntityFallDamage> {
1033 Some(EntityFallDamage::new(
1034 context.fall_distance
1035 * f64::from(1f32 - state.get_block().config.fall_distance_reduction),
1036 1.0,
1037 DamageSource::environment(&vanilla_damage_types::FALL),
1038 ))
1039 }
1040
1041 /// Called when an entity lands on this block.
1042 ///
1043 /// Vanilla parity: `Block.fallOn(Level, BlockState, BlockPos, Entity, double)`.
1044 fn fall_on(
1045 &self,
1046 state: BlockStateId,
1047 world: &Arc<World>,
1048 pos: BlockPos,
1049 context: EntityFallOnContext<'_>,
1050 ) -> Option<EntityFallDamage> {
1051 self.default_fall_on(state, world, pos, context)
1052 }
1053
1054 /// called when an entity bounce on the block.
1055 /// only works if the block has a restitution > 0.0 and the entity is not suppressing bounce
1056 #[expect(
1057 unused_variables,
1058 reason = "default trait implementation ignores all params"
1059 )]
1060 fn bounce_on(
1061 &self,
1062 state: BlockStateId,
1063 world: &Arc<World>,
1064 pos: BlockPos,
1065 context: EntityFallOnContext<'_>,
1066 ) {
1067 }
1068
1069 /// Called after fall damage requested by [`BlockBehavior::fall_on`] is applied.
1070 ///
1071 /// Vanilla parity hook for block-specific fall side effects that depend on
1072 /// whether `Entity.causeFallDamage` returned true.
1073 #[expect(
1074 unused_variables,
1075 reason = "default trait implementation ignores all params"
1076 )]
1077 fn after_fall_on_damage(
1078 &self,
1079 state: BlockStateId,
1080 world: &Arc<World>,
1081 pos: BlockPos,
1082 entity: &dyn Entity,
1083 fall_damage: &EntityFallDamage,
1084 damage_applied: bool,
1085 ) {
1086 }
1087
1088 /// Default post-fall movement hook.
1089 ///
1090 /// Overrides that mirror vanilla `super.updateEntityMovementAfterFallOn(...)`
1091 /// should call [`Self::default_update_entity_movement_after_fall_on`].
1092 #[expect(
1093 unused_variables,
1094 reason = "default trait implementation ignores state, world, and pos"
1095 )]
1096 fn default_update_entity_movement_after_fall_on(
1097 &self,
1098 state: BlockStateId,
1099 world: &Arc<World>,
1100 pos: BlockPos,
1101 context: EntityLandingContext,
1102 ) -> DVec3 {
1103 context.default_velocity_after_fall_on()
1104 }
1105
1106 /// Updates entity velocity after a vertical movement collision with this block.
1107 ///
1108 /// Vanilla mutates the entity in `Block.updateEntityMovementAfterFallOn`.
1109 /// Steel returns the velocity to apply so movement resolution keeps entity
1110 /// state changes centralized in [`Entity::move_entity`].
1111 fn update_entity_movement_after_fall_on(
1112 &self,
1113 state: BlockStateId,
1114 world: &Arc<World>,
1115 pos: BlockPos,
1116 context: EntityLandingContext,
1117 ) -> (DVec3, bool) {
1118 let restitution = state.get_block().config.bounce_restitution;
1119 if context.velocity.y >= 0.0
1120 || restitution <= 0.0
1121 || context.suppresses_bounce
1122 || state.get_block().has_tag(&BlockTag::SUPPRESSES_BOUNCE)
1123 || -context.velocity.y <= context.gravity
1124 {
1125 (
1126 self.default_update_entity_movement_after_fall_on(state, world, pos, context),
1127 false,
1128 )
1129 } else {
1130 (context.velocity_after_bounce(restitution), true)
1131 }
1132 }
1133
1134 /// Default step-on hook.
1135 ///
1136 /// Overrides that mirror vanilla `super.stepOn(...)` should call
1137 /// [`Self::default_step_on`].
1138 #[expect(
1139 unused_variables,
1140 reason = "default trait implementation ignores all params"
1141 )]
1142 fn default_step_on(
1143 &self,
1144 state: BlockStateId,
1145 world: &Arc<World>,
1146 pos: BlockPos,
1147 entity: &dyn Entity,
1148 ) {
1149 }
1150
1151 /// Called when an entity steps on this block while on ground.
1152 ///
1153 /// Vanilla parity: `Block.stepOn(Level, BlockPos, BlockState, Entity)`.
1154 fn step_on(&self, state: BlockStateId, world: &Arc<World>, pos: BlockPos, entity: &dyn Entity) {
1155 self.default_step_on(state, world, pos, entity);
1156 }
1157
1158 /// Creates a new block entity for this block.
1159 ///
1160 /// Structural block-entity presence comes from the extracted block-entity type registry.
1161 /// The result distinguishes a missing Steel implementation from a Vanilla factory that
1162 /// intentionally returns no entity.
1163 ///
1164 /// # Arguments
1165 /// * `level` - Weak reference to the world
1166 /// * `pos` - The position where the block entity will be placed
1167 /// * `state` - The block state for this block entity
1168 #[expect(
1169 unused_variables,
1170 reason = "default trait implementation ignores all params"
1171 )]
1172 fn new_block_entity(
1173 &self,
1174 level: Weak<World>,
1175 pos: BlockPos,
1176 state: BlockStateId,
1177 ) -> BlockEntityCreation {
1178 BlockEntityCreation::Unimplemented
1179 }
1180
1181 /// Returns the beam tint this block contributes to a beacon beam passing through it.
1182 ///
1183 /// Steel models Vanilla's `BeaconBeamBlock` marker interface as a trait method so third
1184 /// party blocks can join a beacon beam without a new interface: `None` means the block is
1185 /// not a beam block, matching a Vanilla class that does not implement `BeaconBeamBlock`.
1186 ///
1187 /// Vanilla parity: `BeaconBeamBlock.getColor()`.
1188 fn beacon_beam_color(&self, _state: BlockStateId) -> Option<DyeColor> {
1189 None
1190 }
1191
1192 /// Returns the server ticker selected by this live block state and entity type.
1193 ///
1194 /// Mirrors Vanilla `EntityBlock.getTicker`. Selection runs without chunk,
1195 /// section, or block-entity storage locks.
1196 #[expect(
1197 unused_variables,
1198 reason = "default trait implementation has no block-entity ticker"
1199 )]
1200 fn get_block_entity_ticker(
1201 &self,
1202 world: &Arc<World>,
1203 state: BlockStateId,
1204 block_entity_type: BlockEntityTypeRef,
1205 ) -> Option<BlockEntityTicker> {
1206 None
1207 }
1208
1209 /// Returns the game-event listener exposed by this block entity.
1210 ///
1211 /// Mirrors Vanilla `EntityBlock.getListener`. Block implementations may override the
1212 /// provider result; the default delegates to the block entity's listener capability.
1213 #[expect(
1214 unused_variables,
1215 reason = "default trait implementation only delegates to the block entity"
1216 )]
1217 fn get_game_event_listener(
1218 &self,
1219 world: &Arc<World>,
1220 block_entity: &dyn BlockEntity,
1221 ) -> Option<SharedGameEventListener> {
1222 block_entity.game_event_listener()
1223 }
1224
1225 /// Returns whether this new block should keep the old state's block entity.
1226 ///
1227 /// Vanilla defaults to `false`; copper chests and copper golem statues explicitly
1228 /// keep their entity across transformations within their respective block family.
1229 /// Steel checks those two extracted tags here until those block classes have their
1230 /// own complete behaviors, rather than registering partial block implementations.
1231 /// Non-Vanilla behaviors must opt in explicitly even if a plugin extends either tag.
1232 ///
1233 /// # Arguments
1234 /// * `old_state` - The previous block state
1235 /// * `new_state` - The requested replacement state
1236 fn should_keep_block_entity(&self, old_state: BlockStateId, new_state: BlockStateId) -> bool {
1237 let old_block = old_state.get_block();
1238 let new_block = new_state.get_block();
1239 new_block.key.namespace == Identifier::VANILLA_NAMESPACE
1240 && ((old_block.has_tag(&BlockTag::COPPER_CHESTS)
1241 && new_block.has_tag(&BlockTag::COPPER_CHESTS))
1242 || (old_block.has_tag(&BlockTag::COPPER_GOLEM_STATUES)
1243 && new_block.has_tag(&BlockTag::COPPER_GOLEM_STATUES)))
1244 }
1245
1246 /// Returns brushable-block data for archaeology brushing
1247 ///
1248 /// Vanilla keeps this on `BrushableBlock`; exposing it through block behavior lets
1249 /// `BrushItem` stay generic without matching concrete vanilla blocks
1250 fn brushable_data(&self, _state: BlockStateId) -> Option<BrushableData> {
1251 None
1252 }
1253
1254 /// Returns whether this block can provide an analog output signal to comparators.
1255 ///
1256 /// Override to return `true` for containers (chests, barrels, hoppers, etc.)
1257 /// and other blocks that comparators can read (composters, beehives, etc.).
1258 #[expect(
1259 unused_variables,
1260 reason = "default trait implementation ignores all params"
1261 )]
1262 fn has_analog_output_signal(&self, state: BlockStateId) -> bool {
1263 false
1264 }
1265
1266 /// Returns the analog output signal strength (0-15) for comparators.
1267 ///
1268 /// Only called if `has_analog_output_signal()` returns `true`.
1269 /// For containers, this is typically based on how full they are.
1270 ///
1271 /// # Arguments
1272 /// * `state` - The current block state
1273 /// * `world` - The world
1274 /// * `pos` - The position of the block
1275 /// * `direction` - The face from which the comparator reads the block
1276 #[expect(
1277 unused_variables,
1278 reason = "default trait implementation ignores all params"
1279 )]
1280 fn get_analog_output_signal(
1281 &self,
1282 state: BlockStateId,
1283 world: &World,
1284 pos: BlockPos,
1285 direction: Direction,
1286 ) -> i32 {
1287 0
1288 }
1289
1290 /// Vanilla parity: whether this block implements `LiquidBlockContainer`.
1291 ///
1292 /// This is a block behavior capability, not just a state property. Most
1293 /// simple waterlogged blocks expose it through `WATERLOGGED`, but vanilla
1294 /// also has liquid containers without that property, such as kelp and
1295 /// seagrass.
1296 fn is_liquid_container(&self, state: BlockStateId) -> bool {
1297 simple_waterlogged_is_liquid_container(state)
1298 }
1299
1300 /// Vanilla parity: `LiquidBlockContainer.canPlaceLiquid()`.
1301 ///
1302 /// Returns `true` if the given fluid type may be placed into this block at the
1303 /// given state. Called by the fluid-spread logic; there is no player context
1304 /// here (fluid spreading has no associated player).
1305 ///
1306 /// Default (`SimpleWaterloggedBlock`): accepts source water for blocks with
1307 /// a `WATERLOGGED` property. Override for blocks that need different
1308 /// restrictions (e.g. double-slabs, barriers).
1309 ///
1310 /// Vanilla signature: `canPlaceLiquid(@Nullable LivingEntity, BlockGetter, BlockPos, BlockState, Fluid)`
1311 /// — the Fluid parameter is a type, not a state.
1312 fn can_place_liquid(&self, state: BlockStateId, fluid: FluidRef) -> bool {
1313 state
1314 .try_get_value(&BlockStateProperties::WATERLOGGED)
1315 .is_some()
1316 && fluid == &vanilla_fluids::WATER
1317 }
1318
1319 /// Vanilla parity: `LiquidBlockContainer.canPlaceLiquid()` with a user.
1320 ///
1321 /// Runtime bucket placement supplies the acting player; fluid spread and
1322 /// other no-user callers should use [`can_place_liquid`].
1323 ///
1324 /// [`can_place_liquid`]: BlockBehavior::can_place_liquid
1325 fn can_place_liquid_with_player(
1326 &self,
1327 state: BlockStateId,
1328 fluid: FluidRef,
1329 _player: Option<&Player>,
1330 ) -> bool {
1331 self.can_place_liquid(state, fluid)
1332 }
1333
1334 /// Vanilla parity: `BlockBehaviour.BlockStateBase.canBeReplaced(Fluid)`.
1335 ///
1336 /// This is a behavior hook because vanilla block subclasses can override the
1337 /// base replacement rule. The default mirrors `Block.canBeReplaced(Fluid)`.
1338 fn can_be_replaced_by_fluid(&self, state: BlockStateId, _fluid: FluidRef) -> bool {
1339 if state.is_air() {
1340 return true;
1341 }
1342
1343 let block = state.get_block();
1344 block.config.replaceable || !state.is_solid()
1345 }
1346
1347 /// Vanilla parity: `LiquidBlockContainer.placeLiquid()`.
1348 ///
1349 /// Attempts to place `fluid_state` into this block. Returns `true` on success,
1350 /// `false` if placement was rejected.
1351 ///
1352 /// Default (`SimpleWaterloggedBlock`): sets `WATERLOGGED = true` and schedules
1353 /// a fluid tick. Vanilla's default `placeLiquid` directly accepts source
1354 /// water and does not delegate to `canPlaceLiquid`.
1355 fn place_liquid(
1356 &self,
1357 level: &dyn LevelAccessor,
1358 pos: BlockPos,
1359 state: BlockStateId,
1360 fluid_state: FluidState,
1361 ) -> bool {
1362 place_simple_waterlogged_liquid(level, pos, state, fluid_state)
1363 }
1364
1365 /// Returns the trait object for Blocks that have the Bonemealable trait implemented.
1366 fn as_bonemealable(&self) -> Option<&dyn Bonemealable> {
1367 None
1368 }
1369
1370 /// Returns the shared vanilla `GrowingPlantHeadBlock` capability.
1371 fn as_growing_plant_head(&self) -> Option<&dyn GrowingPlantHeadBehavior> {
1372 None
1373 }
1374
1375 /// Returns the shared vanilla `Fallable` capability implemented by this block.
1376 fn as_fallable(&self) -> Option<&dyn Fallable> {
1377 None
1378 }
1379
1380 /// Returns the shared vanilla rail capability implemented by this block.
1381 fn as_rail(&self) -> Option<&dyn RailBehavior> {
1382 None
1383 }
1384
1385 /// Whether this block's item may be stored inside container items such as
1386 /// shulker boxes and bundles.
1387 ///
1388 /// Vanilla gates this on the item class, but shulker boxes share
1389 /// `BlockItem`, so the rule lives on the block instead.
1390 fn fits_inside_container_items(&self) -> bool {
1391 true
1392 }
1393}
1394
1395mod registry;
1396
1397pub use registry::{BlockBehaviorRegistry, DefaultBlockBehavior};
1398
1399#[cfg(test)]
1400mod tests;