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;