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;