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