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