Skip to main content

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;