Skip to main content

steel_core/behavior/block/
context.rs

1use crate::block_entity::BlockEntity;
2use steel_math::lerp;
3
4use super::{
5    Arc, Axis, BlockLocalAabb, BlockPos, BlockStateId, DVec3, DamageSource, Entity, EntityTypeRef,
6    ItemStack, SharedBlockEntity, SmallVec, SoundEventRef, VoxelShape, World, vanilla_damage_types,
7    vanilla_entities,
8};
9
10use crate::entity::entities::FallingBlockEntity;
11
12pub struct PickupResult {
13    pub filled_bucket: ItemStack,
14    pub sound: Option<SoundEventRef>,
15}
16
17/// Result of invoking a block's Vanilla block-entity factory.
18///
19/// `NoEntity` is distinct from `Unimplemented`: some Vanilla blocks, notably the moving-piston
20/// placeholder, intentionally return no entity from normal block creation even though their
21/// state accepts an explicitly-created block entity.
22pub enum BlockEntityCreation {
23    /// The block factory created its entity.
24    Created(SharedBlockEntity),
25    /// The implemented Vanilla factory intentionally created no entity.
26    NoEntity,
27    /// Steel has not implemented this block's factory yet.
28    Unimplemented,
29}
30
31impl BlockEntityCreation {
32    /// Converts an optional registered implementation into a factory result.
33    #[must_use]
34    pub fn from_registered_factory(entity: Option<SharedBlockEntity>) -> Self {
35        entity.map_or(Self::Unimplemented, Self::Created)
36    }
37
38    /// Returns the created entity, if the factory produced one.
39    #[must_use]
40    pub fn into_created(self) -> Option<SharedBlockEntity> {
41        match self {
42            Self::Created(entity) => Some(entity),
43            Self::NoEntity | Self::Unimplemented => None,
44        }
45    }
46}
47
48/// Shared behavior exposed by blocks in vanilla's `BaseRailBlock` hierarchy.
49///
50/// Rail topology uses this capability in addition to the `minecraft:rails` tag.
51/// This keeps class-hierarchy checks extensible without relying on concrete
52/// downcasts.
53pub trait RailBehavior: Send + Sync {
54    /// Returns whether this rail forbids curved shapes.
55    fn is_straight(&self) -> bool;
56}
57
58/// Shared behavior exposed by blocks implementing vanilla's `Fallable` interface.
59///
60/// Falling entities use this capability for landing, failed-placement, and
61/// damage-source callbacks without depending on one concrete Rust block type.
62pub trait Fallable: Send + Sync {
63    /// Called after a falling entity successfully places its carried state.
64    fn on_land(
65        &self,
66        _world: &Arc<World>,
67        _pos: BlockPos,
68        _state: BlockStateId,
69        _replaced_state: BlockStateId,
70        _entity: &FallingBlockEntity,
71    ) {
72    }
73
74    /// Called when a falling entity breaks instead of placing its carried state.
75    fn on_broken_after_fall(
76        &self,
77        _world: &Arc<World>,
78        _pos: BlockPos,
79        _entity: &FallingBlockEntity,
80    ) {
81    }
82
83    /// Returns the damage source used when this falling block hurts entities.
84    fn get_fall_damage_source(&self, entity: &Arc<FallingBlockEntity>) -> DamageSource {
85        DamageSource::direct(
86            &vanilla_damage_types::FALLING_BLOCK,
87            Arc::<FallingBlockEntity>::clone(entity),
88        )
89    }
90
91    /// Returns whether this behavior is in vanilla's `ConcretePowderBlock` hierarchy.
92    fn is_concrete_powder(&self) -> bool {
93        false
94    }
95}
96
97/// Resolved block-local collision boxes for a live block state.
98///
99/// Most blocks materialize their extracted static voxel shape here. Dynamic
100/// blocks such as moving pistons can instead return boxes computed from live
101/// world data without forcing runtime shapes into the static registry.
102pub type BlockCollisionBoxes = SmallVec<[BlockLocalAabb; 4]>;
103
104/// Live parameters used to resolve a block's loot.
105///
106/// This is the Steel counterpart to vanilla's block `LootParams`. Behaviors can
107/// override loot generation while retaining the original tool, entity, luck,
108/// and position when delegating to another block state.
109pub struct BlockLootContext<'a> {
110    world: &'a Arc<World>,
111    pos: BlockPos,
112    entity: Option<&'a dyn Entity>,
113    block_entity: Option<&'a dyn BlockEntity>,
114    tool: Option<&'a ItemStack>,
115    explosion_radius: Option<f32>,
116    luck: f32,
117}
118
119impl<'a> BlockLootContext<'a> {
120    /// Creates a no-tool block loot context.
121    #[must_use]
122    pub const fn new(world: &'a Arc<World>, pos: BlockPos) -> Self {
123        Self {
124            world,
125            pos,
126            entity: None,
127            block_entity: None,
128            tool: None,
129            explosion_radius: None,
130            luck: 0.0,
131        }
132    }
133
134    /// Adds the entity responsible for destroying the block.
135    #[must_use]
136    pub const fn with_entity(mut self, entity: Option<&'a dyn Entity>) -> Self {
137        self.entity = entity;
138        self
139    }
140
141    /// Adds the block entity at the broken position.
142    #[must_use]
143    pub const fn with_block_entity(mut self, block_entity: Option<&'a dyn BlockEntity>) -> Self {
144        self.block_entity = block_entity;
145        self
146    }
147
148    /// Adds the tool used to destroy the block.
149    #[must_use]
150    pub const fn with_tool(mut self, tool: &'a ItemStack) -> Self {
151        self.tool = Some(tool);
152        self
153    }
154
155    /// Adds the explosion radius used by explosion-decay loot functions.
156    #[must_use]
157    pub const fn with_explosion(mut self, radius: f32) -> Self {
158        self.explosion_radius = Some(radius);
159        self
160    }
161
162    /// Adds the luck used to evaluate the loot table.
163    #[must_use]
164    pub const fn with_luck(mut self, luck: f32) -> Self {
165        self.luck = luck;
166        self
167    }
168
169    /// Returns the world containing the block.
170    #[must_use]
171    pub const fn world(&self) -> &'a Arc<World> {
172        self.world
173    }
174
175    /// Returns the block position whose loot is being resolved.
176    #[must_use]
177    pub const fn pos(&self) -> BlockPos {
178        self.pos
179    }
180
181    /// Resolves loot for another state with the same vanilla loot parameters.
182    #[must_use]
183    pub fn get_drops(&self, state: BlockStateId) -> Vec<ItemStack> {
184        World::block_drops(state, self)
185    }
186
187    pub(crate) const fn entity(&self) -> Option<&'a dyn Entity> {
188        self.entity
189    }
190
191    pub(crate) const fn block_entity(&self) -> Option<&'a dyn BlockEntity> {
192        self.block_entity
193    }
194
195    pub(crate) const fn tool(&self) -> Option<&'a ItemStack> {
196        self.tool
197    }
198
199    pub(crate) const fn explosion_radius(&self) -> Option<f32> {
200        self.explosion_radius
201    }
202
203    pub(crate) const fn luck(&self) -> f32 {
204        self.luck
205    }
206}
207
208const COLLISION_CONTEXT_ABOVE_EPSILON: f64 = 1.0e-5;
209
210/// Entity facts used by vanilla `CollisionContext` for block collision shapes.
211#[derive(Debug, Clone, Copy, PartialEq)]
212pub struct BlockCollisionContext {
213    entity_bottom: Option<f64>,
214    fall_distance: f64,
215    can_walk_on_powder_snow: bool,
216    is_falling_block: bool,
217    descending: bool,
218    placement: bool,
219}
220
221impl BlockCollisionContext {
222    /// Collision context for source-less collision queries.
223    #[must_use]
224    pub const fn empty() -> Self {
225        Self {
226            entity_bottom: None,
227            fall_distance: 0.0,
228            can_walk_on_powder_snow: false,
229            is_falling_block: false,
230            descending: false,
231            placement: false,
232        }
233    }
234
235    /// Collision context for normal entity movement.
236    #[must_use]
237    pub const fn entity(entity_bottom: f64, descending: bool) -> Self {
238        Self {
239            entity_bottom: Some(entity_bottom),
240            fall_distance: 0.0,
241            can_walk_on_powder_snow: false,
242            is_falling_block: false,
243            descending,
244            placement: false,
245        }
246    }
247
248    /// Collision context for vanilla `CollisionContext.withPosition(entity, position)`.
249    ///
250    /// In Steel's reduced representation this also matches
251    /// `CollisionContext.placementContext`.
252    #[must_use]
253    pub const fn with_position(entity_bottom: f64, descending: bool) -> Self {
254        Self {
255            entity_bottom: Some(entity_bottom),
256            fall_distance: 0.0,
257            can_walk_on_powder_snow: false,
258            is_falling_block: false,
259            descending,
260            placement: true,
261        }
262    }
263
264    /// Placement obstruction context when no entity initiated the placement.
265    ///
266    /// This matches vanilla `CollisionContext.placementContext(null)`.
267    #[must_use]
268    pub const fn placement_without_entity() -> Self {
269        Self::with_position(f64::MIN, false)
270    }
271
272    /// Non-placement collision context anchored at a fixed Y position, with
273    /// no descent or fall distance.
274    #[must_use]
275    pub const fn position_context(y: f64) -> Self {
276        Self {
277            entity_bottom: Some(y),
278            fall_distance: 0.0,
279            can_walk_on_powder_snow: false,
280            is_falling_block: false,
281            descending: false,
282            placement: false,
283        }
284    }
285
286    /// Returns a copy with vanilla accumulated fall distance.
287    #[must_use]
288    pub const fn with_fall_distance(mut self, fall_distance: f64) -> Self {
289        self.fall_distance = fall_distance;
290        self
291    }
292
293    /// Returns a copy with vanilla powder-snow walkability.
294    #[must_use]
295    pub const fn with_can_walk_on_powder_snow(mut self, can_walk_on_powder_snow: bool) -> Self {
296        self.can_walk_on_powder_snow = can_walk_on_powder_snow;
297        self
298    }
299
300    /// Returns a copy with vanilla falling-block collision context.
301    #[must_use]
302    pub const fn with_falling_block(mut self, is_falling_block: bool) -> Self {
303        self.is_falling_block = is_falling_block;
304        self
305    }
306
307    /// Returns accumulated vanilla fall distance for context-sensitive block collision.
308    #[must_use]
309    pub const fn fall_distance(self) -> f64 {
310        self.fall_distance
311    }
312
313    /// Returns whether the source entity can walk on powder snow.
314    #[must_use]
315    pub const fn can_walk_on_powder_snow(self) -> bool {
316        self.can_walk_on_powder_snow
317    }
318
319    /// Returns whether the source entity is a vanilla falling block.
320    #[must_use]
321    pub const fn is_falling_block(self) -> bool {
322        self.is_falling_block
323    }
324
325    /// Returns whether the source entity is descending through context-sensitive blocks.
326    #[must_use]
327    pub const fn is_descending(self) -> bool {
328        self.descending
329    }
330
331    /// Returns whether this context is used for placement-style collision checks.
332    #[must_use]
333    pub const fn is_placement(self) -> bool {
334        self.placement
335    }
336
337    /// Returns whether the entity is above the given shape at the position.
338    #[must_use]
339    pub fn is_above(self, shape: VoxelShape, pos: BlockPos, default_value: bool) -> bool {
340        let Some(entity_bottom) = self.entity_bottom else {
341            return default_value;
342        };
343
344        entity_bottom > f64::from(pos.y()) + shape.max(Axis::Y) - COLLISION_CONTEXT_ABOVE_EPSILON
345    }
346}
347
348/// Entity facts needed by `Block.updateEntityMovementAfterFallOn`.
349#[derive(Debug, Clone, Copy, PartialEq)]
350pub struct EntityLandingContext {
351    /// Entity velocity before the block landing hook adjusts it (`currentMovement` in vanilla).
352    pub velocity: DVec3,
353    /// Vertical movement actually achieved this tick after collision clipping
354    pub achieved_movement_y: f64,
355    /// Whether the entity uses vanilla living-entity bounce behavior.
356    pub is_living_entity: bool,
357    /// Whether vanilla bounce behavior should be suppressed.
358    pub suppresses_bounce: bool,
359    /// The gravity applied to the entity.
360    pub gravity: f64,
361    /// The air drag of the entity.
362    pub air_drag: f64,
363}
364
365/// Entity facts needed by `Block.fallOn`.
366#[derive(Debug, Clone, Copy, PartialEq)]
367pub struct EntityFallOnFacts {
368    /// Vanilla entity type of the landing entity.
369    pub entity_type: EntityTypeRef,
370    /// Whether the landing entity implements vanilla living-entity behavior.
371    pub is_living_entity: bool,
372    /// Current entity bounding-box X/Z width.
373    pub bounding_box_width: f64,
374    /// Current entity bounding-box height.
375    pub bounding_box_height: f64,
376    /// Vanilla small and big living-entity fall sounds.
377    pub fall_sounds: (SoundEventRef, SoundEventRef),
378}
379
380impl EntityFallOnFacts {
381    /// Creates fall-on facts from explicit entity values.
382    #[must_use]
383    pub const fn new(
384        entity_type: EntityTypeRef,
385        is_living_entity: bool,
386        bounding_box_width: f64,
387        bounding_box_height: f64,
388        fall_sounds: (SoundEventRef, SoundEventRef),
389    ) -> Self {
390        Self {
391            entity_type,
392            is_living_entity,
393            bounding_box_width,
394            bounding_box_height,
395            fall_sounds,
396        }
397    }
398
399    /// Creates fall-on facts from an entity.
400    #[must_use]
401    pub fn from_entity(entity: &dyn Entity) -> Self {
402        let bounding_box = entity.bounding_box();
403        Self::new(
404            entity.entity_type(),
405            entity.is_living_entity(),
406            bounding_box.width(),
407            bounding_box.height(),
408            entity.fall_sounds(),
409        )
410    }
411
412    /// Returns true for vanilla players.
413    #[must_use]
414    pub fn is_player(self) -> bool {
415        self.entity_type == &vanilla_entities::PLAYER
416    }
417
418    /// Vanilla farmland trampling size check:
419    /// `getBbWidth() * getBbWidth() * getBbHeight()`.
420    #[must_use]
421    pub fn bounding_box_width_squared_height(self) -> f64 {
422        self.bounding_box_width * self.bounding_box_width * self.bounding_box_height
423    }
424}
425
426/// Entity facts needed by `Block.fallOn`.
427#[derive(Clone, Copy)]
428pub struct EntityFallOnContext<'a> {
429    /// Accumulated vanilla fall distance at landing time.
430    pub fall_distance: f64,
431    /// Whether vanilla bounce behavior should be suppressed.
432    pub suppresses_bounce: bool,
433    /// Entity facts available to vanilla fall-on hooks.
434    pub entity: EntityFallOnFacts,
435    /// Source entity for vanilla side effects such as game events.
436    pub source_entity: Option<&'a dyn Entity>,
437}
438
439impl<'a> EntityFallOnContext<'a> {
440    /// Creates a fall-on context for a ground collision.
441    #[must_use]
442    pub const fn new(
443        fall_distance: f64,
444        suppresses_bounce: bool,
445        entity: EntityFallOnFacts,
446        source_entity: Option<&'a dyn Entity>,
447    ) -> Self {
448        Self {
449            fall_distance,
450            suppresses_bounce,
451            entity,
452            source_entity,
453        }
454    }
455
456    /// Creates a fall-on context from a landing entity.
457    #[must_use]
458    pub fn from_entity(fall_distance: f64, entity: &'a dyn Entity) -> Self {
459        Self::new(
460            fall_distance,
461            entity.is_suppressing_bounce(),
462            EntityFallOnFacts::from_entity(entity),
463            Some(entity),
464        )
465    }
466
467    /// Returns this context with a transformed fall distance.
468    #[must_use]
469    pub const fn with_fall_distance(mut self, fall_distance: f64) -> Self {
470        self.fall_distance = fall_distance;
471        self
472    }
473
474    /// Returns the source entity for vanilla side effects.
475    #[must_use]
476    pub const fn source_entity(self) -> Option<&'a dyn Entity> {
477        self.source_entity
478    }
479}
480
481/// Fall damage requested by a block landing hook.
482#[derive(Debug, Clone)]
483pub struct EntityFallDamage {
484    /// Fall distance to pass to `Entity.causeFallDamage`.
485    pub fall_distance: f64,
486    /// Block-specific damage multiplier.
487    pub damage_modifier: f32,
488    /// Damage source for this landing.
489    pub source: DamageSource,
490}
491
492impl EntityFallDamage {
493    /// Creates a fall-damage action.
494    #[must_use]
495    pub const fn new(fall_distance: f64, damage_modifier: f32, source: DamageSource) -> Self {
496        Self {
497            fall_distance,
498            damage_modifier,
499            source,
500        }
501    }
502}
503
504impl EntityLandingContext {
505    /// Creates a landing context for a vertical movement collision.
506    #[must_use]
507    pub const fn new(
508        velocity: DVec3,
509        achieved_movement_y: f64,
510        is_living_entity: bool,
511        suppresses_bounce: bool,
512        gravity: f64,
513        air_drag: f64,
514    ) -> Self {
515        Self {
516            velocity,
517            achieved_movement_y,
518            is_living_entity,
519            suppresses_bounce,
520            gravity,
521            air_drag,
522        }
523    }
524
525    /// Vanilla default `Block.updateEntityMovementAfterFallOn` result.
526    #[must_use]
527    pub const fn default_velocity_after_fall_on(self) -> DVec3 {
528        DVec3::new(self.velocity.x, 0.0, self.velocity.z)
529    }
530
531    /// Computes the Y velocity after bouncing on a block.
532    /// doesn't test if the bounce is valid
533    #[must_use]
534    pub fn velocity_after_bounce(&self, restitution: f32) -> DVec3 {
535        let portion_with_movement = if self.velocity.y == 0.0 {
536            1.0
537        } else {
538            self.achieved_movement_y / self.velocity.y
539        };
540
541        let gravity_compensation = portion_with_movement * self.gravity;
542        let effective_drag = lerp(portion_with_movement, 1.0, self.air_drag);
543
544        let entity_factor: f32 = if self.is_living_entity { 1.0 } else { 0.8 };
545        let restitution = f64::from(restitution * entity_factor);
546
547        DVec3::new(
548            self.velocity.x,
549            (gravity_compensation - self.velocity.y) * effective_drag * restitution,
550            self.velocity.z,
551        )
552    }
553}
554
555#[cfg(test)]
556mod tests {
557    use super::{DVec3, EntityLandingContext};
558
559    fn landing(velocity_y: f64, achieved_movement_y: f64) -> EntityLandingContext {
560        EntityLandingContext::new(
561            DVec3::new(0.25, velocity_y, -0.5),
562            achieved_movement_y,
563            true,
564            false,
565            0.08,
566            0.98,
567        )
568    }
569
570    #[test]
571    fn bounce_uses_fraction_of_requested_movement_actually_achieved() {
572        // portionWithMovement = -0.75 / -1.5 = 0.5
573        // gravityCompensation = 0.5 * 0.08 = 0.04
574        // effectiveDrag = lerp(0.5, 1.0, 0.98) = 0.99
575        // result.y = (0.04 - (-1.5)) * 0.99 * 1.0 = 1.5246
576        let context = landing(-1.5, -0.75);
577        let result = context.velocity_after_bounce(1.0);
578        assert!((result.y - 1.5246).abs() < 1.0e-6, "got {}", result.y);
579    }
580
581    #[test]
582    fn non_living_entities_bounce_lower() {
583        let living = landing(-1.5, -0.75).velocity_after_bounce(1.0);
584        let non_living = EntityLandingContext::new(
585            DVec3::new(0.25, -1.5, -0.5),
586            -0.75,
587            false,
588            false,
589            0.08,
590            0.98,
591        )
592        .velocity_after_bounce(1.0);
593
594        assert!(
595            (non_living.y - living.y * 0.8).abs() < 1.0e-6,
596            "non-living {} vs living {}",
597            non_living.y,
598            living.y
599        );
600    }
601}