Skip to main content

steel_core/entity/entities/objects/items/
falling_block.rs

1//! Vanilla falling-block entity.
2
3use std::io::Cursor;
4use std::sync::{Arc, Weak};
5
6use glam::DVec3;
7use simdnbt::borrow::{NbtCompound as BorrowedNbtCompoundView, read_compound};
8use simdnbt::owned::{NbtCompound, NbtTag};
9use steel_macros::entity_behavior;
10use steel_protocol::packets::game::CBlockUpdate;
11use steel_registry::blocks::BlockRef;
12use steel_registry::blocks::block_state_ext::BlockStateExt as _;
13use steel_registry::blocks::properties::{BlockStateProperties, Direction};
14use steel_registry::entity_type::EntityTypeRef;
15use steel_registry::fluid::FluidStateExt as _;
16use steel_registry::item_stack::ItemStack;
17use steel_registry::vanilla_block_tags::BlockTag;
18use steel_registry::vanilla_entity_data::FallingBlockEntityData;
19use steel_registry::vanilla_game_rules::ENTITY_DROPS;
20use steel_registry::{
21    REGISTRY, vanilla_blocks, vanilla_damage_types, vanilla_entities, vanilla_fluids,
22};
23use steel_utils::locks::SyncMutex;
24use steel_utils::{BlockPos, BlockStateId, DowncastType, DowncastTypeKey, types::UpdateFlags};
25
26use crate::behavior::blocks::{AnvilBlock, FallingBlock};
27use crate::behavior::{BLOCK_BEHAVIORS, BlockPlaceContext, Fallable};
28use crate::block_entity::block_state_nbt;
29use crate::entity::damage::DamageSource;
30use crate::entity::{
31    Entity, EntityBase, EntityBaseLoad, EntityMovementEmission, EntitySyncedData, RemovalReason,
32    next_entity_id,
33};
34use crate::fluid::fluid_state_to_block;
35use crate::physics::MoverType;
36use crate::world::{ClipBlockShape, ClipFluid, World};
37
38const DEFAULT_FALL_DAMAGE_PER_DISTANCE: f32 = 0.0;
39const DEFAULT_MAX_FALL_DAMAGE: i32 = 40;
40const DEFAULT_GRAVITY: f64 = 0.04;
41const AIR_DRAG: f64 = 0.98;
42const LANDED_HORIZONTAL_DRAG: f64 = 0.7;
43const LANDED_VERTICAL_BOUNCE: f64 = -0.5;
44
45struct FallingBlockState {
46    block_state: BlockStateId,
47    time: i32,
48    drop_item: bool,
49    cancel_drop: bool,
50    hurt_entities: bool,
51    fall_damage_max: i32,
52    fall_damage_per_distance: f32,
53    block_data: Option<NbtCompound>,
54}
55
56impl FallingBlockState {
57    const fn new(block_state: BlockStateId) -> Self {
58        Self {
59            block_state,
60            time: 0,
61            drop_item: true,
62            cancel_drop: false,
63            hurt_entities: false,
64            fall_damage_max: DEFAULT_MAX_FALL_DAMAGE,
65            fall_damage_per_distance: DEFAULT_FALL_DAMAGE_PER_DISTANCE,
66            block_data: None,
67        }
68    }
69}
70
71/// Entity carrying a block state while it falls under vanilla physics.
72#[entity_behavior(class = "FallingBlockEntity")]
73pub struct FallingBlockEntity {
74    base: EntityBase,
75    entity_type: EntityTypeRef,
76    entity_data: SyncMutex<FallingBlockEntityData>,
77    state: SyncMutex<FallingBlockState>,
78}
79
80// SAFETY: This Steel-owned key uniquely identifies `FallingBlockEntity`.
81unsafe impl DowncastType for FallingBlockEntity {
82    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/falling_block");
83}
84
85impl FallingBlockEntity {
86    /// Creates the default sand-backed entity used by vanilla entity factories.
87    #[must_use]
88    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
89        Self {
90            base: EntityBase::new(id, position, entity_type.dimensions, world),
91            entity_type,
92            entity_data: SyncMutex::new(FallingBlockEntityData::new()),
93            state: SyncMutex::new(FallingBlockState::new(vanilla_blocks::SAND.default_state())),
94        }
95    }
96
97    fn with_block_state(
98        entity_type: EntityTypeRef,
99        id: i32,
100        position: DVec3,
101        block_state: BlockStateId,
102        world: Weak<World>,
103    ) -> Self {
104        let mut entity_data = FallingBlockEntityData::new();
105        entity_data.start_pos.set(BlockPos::new(
106            position.x.floor() as i32,
107            position.y.floor() as i32,
108            position.z.floor() as i32,
109        ));
110        Self {
111            base: EntityBase::new(id, position, entity_type.dimensions, world),
112            entity_type,
113            entity_data: SyncMutex::new(entity_data),
114            state: SyncMutex::new(FallingBlockState::new(block_state)),
115        }
116    }
117
118    /// Creates a falling block from saved base data.
119    #[must_use]
120    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
121        Self {
122            base: EntityBase::from_load(load, entity_type.dimensions),
123            entity_type,
124            entity_data: SyncMutex::new(FallingBlockEntityData::new()),
125            state: SyncMutex::new(FallingBlockState::new(vanilla_blocks::SAND.default_state())),
126        }
127    }
128
129    /// Replaces a world block with its legacy fluid and spawns its falling entity.
130    #[must_use]
131    pub fn fall(world: &Arc<World>, pos: BlockPos, state: BlockStateId) -> Arc<Self> {
132        let carried_state = if state
133            .try_get_value(&BlockStateProperties::WATERLOGGED)
134            .is_some()
135        {
136            state.set_value(&BlockStateProperties::WATERLOGGED, false)
137        } else {
138            state
139        };
140        let entity = Arc::new(Self::with_block_state(
141            &vanilla_entities::FALLING_BLOCK,
142            next_entity_id(),
143            DVec3::new(
144                f64::from(pos.x()) + 0.5,
145                f64::from(pos.y()),
146                f64::from(pos.z()) + 0.5,
147            ),
148            carried_state,
149            Arc::downgrade(world),
150        ));
151
152        world.set_block(
153            pos,
154            fluid_state_to_block(state.get_fluid_state()),
155            UpdateFlags::UPDATE_ALL,
156        );
157        if let Err(error) = world.try_add_entity(Arc::clone(&entity) as Arc<dyn Entity>) {
158            log::error!("failed to add falling block entity: {error}");
159        }
160        entity
161    }
162
163    /// Returns the carried block state.
164    #[must_use]
165    pub fn block_state(&self) -> BlockStateId {
166        self.state.lock().block_state
167    }
168
169    /// Returns the number of falling-block ticks elapsed.
170    #[must_use]
171    pub fn time(&self) -> i32 {
172        self.state.lock().time
173    }
174
175    /// Returns the synchronized position where this entity started falling.
176    #[must_use]
177    pub fn start_pos(&self) -> BlockPos {
178        *self.entity_data.lock().start_pos.get()
179    }
180
181    /// Enables vanilla falling-block impact damage.
182    pub fn set_hurts_entities(&self, damage_per_distance: f32, damage_max: i32) {
183        let mut state = self.state.lock();
184        state.hurt_entities = true;
185        state.fall_damage_per_distance = damage_per_distance;
186        state.fall_damage_max = damage_max;
187    }
188
189    /// Prevents the carried block from placing or dropping when it lands.
190    pub fn disable_drop(&self) {
191        self.state.lock().cancel_drop = true;
192    }
193
194    fn is_concrete_powder(&self) -> bool {
195        BLOCK_BEHAVIORS
196            .get_behavior(self.block_state().get_block())
197            .as_fallable()
198            .is_some_and(Fallable::is_concrete_powder)
199    }
200
201    fn is_stuck_in_water(&self, world: &Arc<World>, pos: BlockPos) -> bool {
202        self.is_concrete_powder() && world.get_block_state(pos).get_fluid_state().is_water()
203    }
204
205    fn clip_fast_concrete_into_water(
206        &self,
207        world: &Arc<World>,
208        pos: &mut BlockPos,
209        is_stuck_in_water: &mut bool,
210    ) {
211        if !self.is_concrete_powder() || self.velocity().length_squared() <= 1.0 {
212            return;
213        }
214
215        let hit = world.clip(
216            self.old_position(),
217            self.position(),
218            ClipBlockShape::Collider,
219            ClipFluid::SourceOnly,
220        );
221        if hit.is_miss()
222            || !world
223                .get_block_state(hit.block_pos)
224                .get_fluid_state()
225                .is_water()
226        {
227            return;
228        }
229
230        *pos = hit.block_pos;
231        *is_stuck_in_water = true;
232    }
233
234    fn may_replace(world: &Arc<World>, pos: BlockPos, current: BlockStateId) -> bool {
235        let mut empty = ItemStack::empty();
236        let context =
237            BlockPlaceContext::directional(world, pos, Direction::Down, &mut empty, Direction::Up);
238        BLOCK_BEHAVIORS
239            .get_behavior(current.get_block())
240            .can_be_replaced(current, &context)
241    }
242
243    fn call_on_broken_after_fall(&self, world: &Arc<World>, pos: BlockPos, block: BlockRef) {
244        if let Some(fallable) = BLOCK_BEHAVIORS.get_behavior(block).as_fallable() {
245            fallable.on_broken_after_fall(world, pos, self);
246        }
247    }
248
249    fn should_drop_item(&self, world: &Arc<World>) -> bool {
250        self.state.lock().drop_item && world.get_game_rule(&ENTITY_DROPS)
251    }
252
253    fn drop_block_item(&self, world: &Arc<World>, block: BlockRef) {
254        if !self.should_drop_item(world) {
255            return;
256        }
257        let item = REGISTRY.items.by_block(block);
258        let _ = self.spawn_at_location(ItemStack::new(item), 0.0);
259    }
260
261    fn break_after_failed_placement(&self, world: &Arc<World>, pos: BlockPos, block: BlockRef) {
262        self.set_removed(RemovalReason::Discarded);
263        if self.should_drop_item(world) {
264            self.call_on_broken_after_fall(world, pos, block);
265            self.drop_block_item(world, block);
266        }
267    }
268
269    fn merge_block_entity_data(&self, world: &Arc<World>, pos: BlockPos) {
270        if !self.block_state().has_block_entity() {
271            return;
272        }
273        let block_data = self.state.lock().block_data.clone();
274        let Some(block_data) = block_data else {
275            return;
276        };
277        let Some(block_entity) = world.get_block_entity(pos) else {
278            return;
279        };
280
281        let mut merged = block_entity.save_custom_only();
282        for (name, tag) in block_data {
283            let name_text = name.to_string();
284            while merged.remove(&name_text).is_some() {}
285            merged.insert(name, tag);
286        }
287
288        let mut bytes = Vec::new();
289        merged.write(&mut bytes);
290        let Ok(borrowed) = read_compound(&mut Cursor::new(bytes.as_slice())) else {
291            log::error!("failed to reborrow falling block entity data at {pos:?}");
292            return;
293        };
294        block_entity.load_additional(&borrowed);
295        block_entity.set_changed();
296    }
297
298    fn try_place_carried_block(
299        &self,
300        world: &Arc<World>,
301        pos: BlockPos,
302        current_state: BlockStateId,
303        is_stuck_in_water: bool,
304        block: BlockRef,
305    ) {
306        let carried_state = self.block_state();
307        let continue_falling = FallingBlock::is_free(world.get_block_state(pos.below()))
308            && (!self.is_concrete_powder() || !is_stuck_in_water);
309        let survives = BLOCK_BEHAVIORS
310            .get_behavior(carried_state.get_block())
311            .can_survive(carried_state, world.as_ref(), pos)
312            && !continue_falling;
313
314        if !Self::may_replace(world, pos, current_state) || !survives {
315            self.break_after_failed_placement(world, pos, block);
316            return;
317        }
318
319        let placed_state = if carried_state
320            .try_get_value(&BlockStateProperties::WATERLOGGED)
321            .is_some()
322            && world.get_block_state(pos).get_fluid_state().fluid_id == &vanilla_fluids::WATER
323        {
324            carried_state.set_value(&BlockStateProperties::WATERLOGGED, true)
325        } else {
326            carried_state
327        };
328
329        if !world.set_block(pos, placed_state, UpdateFlags::UPDATE_ALL) {
330            self.break_after_failed_placement(world, pos, block);
331            return;
332        }
333
334        world.broadcast_to_entity_trackers(
335            self.id(),
336            CBlockUpdate {
337                pos,
338                block_state: world.get_block_state(pos),
339            },
340            None,
341        );
342        self.set_removed(RemovalReason::Discarded);
343        if let Some(fallable) = BLOCK_BEHAVIORS.get_behavior(block).as_fallable() {
344            fallable.on_land(world, pos, placed_state, current_state, self);
345        }
346        self.merge_block_entity_data(world, pos);
347    }
348
349    fn tick_server(&self, world: &Arc<World>, block: BlockRef) {
350        let mut pos = self.block_position();
351        let mut is_stuck_in_water = self.is_stuck_in_water(world, pos);
352        self.clip_fast_concrete_into_water(world, &mut pos, &mut is_stuck_in_water);
353
354        if !self.on_ground() && !is_stuck_in_water {
355            let time = self.time();
356            let outside_expiry_height = pos.y() <= world.get_min_y() || pos.y() > world.get_max_y();
357            if time > 600 || time > 100 && outside_expiry_height {
358                self.drop_block_item(world, block);
359                self.set_removed(RemovalReason::Discarded);
360            }
361            return;
362        }
363
364        let current_state = world.get_block_state(pos);
365        let velocity = self.velocity();
366        self.set_velocity(DVec3::new(
367            velocity.x * LANDED_HORIZONTAL_DRAG,
368            velocity.y * LANDED_VERTICAL_BOUNCE,
369            velocity.z * LANDED_HORIZONTAL_DRAG,
370        ));
371        if current_state.get_block() == &vanilla_blocks::MOVING_PISTON {
372            return;
373        }
374
375        if self.state.lock().cancel_drop {
376            self.set_removed(RemovalReason::Discarded);
377            self.call_on_broken_after_fall(world, pos, block);
378            return;
379        }
380        self.try_place_carried_block(world, pos, current_state, is_stuck_in_water, block);
381    }
382}
383
384impl Entity for FallingBlockEntity {
385    fn base(&self) -> &EntityBase {
386        &self.base
387    }
388
389    fn entity_type(&self) -> EntityTypeRef {
390        self.entity_type
391    }
392
393    fn tick(&self) {
394        let block_state = self.block_state();
395        if block_state.is_air() {
396            self.set_removed(RemovalReason::Discarded);
397            return;
398        }
399        let block = block_state.get_block();
400
401        {
402            let mut state = self.state.lock();
403            state.time = state.time.wrapping_add(1);
404        }
405        self.apply_gravity();
406        let _ = self.move_entity(MoverType::SelfMovement, self.velocity());
407        self.apply_effects_from_blocks();
408        self.handle_portal();
409        if let Some(world) = self.level()
410            && self.is_alive()
411        {
412            self.tick_server(&world, block);
413        }
414        self.set_velocity(self.velocity() * AIR_DRAG);
415    }
416
417    fn get_default_gravity(&self) -> f64 {
418        DEFAULT_GRAVITY
419    }
420
421    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
422        Some(&self.entity_data)
423    }
424
425    fn spawn_data(&self) -> i32 {
426        i32::from(self.block_state().0)
427    }
428
429    fn blocks_building(&self) -> bool {
430        true
431    }
432
433    fn is_pickable(&self) -> bool {
434        !self.is_removed()
435    }
436
437    fn attackable(&self) -> bool {
438        false
439    }
440
441    fn movement_emission(&self) -> EntityMovementEmission {
442        EntityMovementEmission::None
443    }
444
445    fn hurt(&self, _world: &World, source: &DamageSource, _amount: f32) -> bool {
446        if !self.is_invulnerable_to_base(source) {
447            self.mark_hurt();
448        }
449        false
450    }
451
452    fn cause_fall_damage(
453        &self,
454        fall_distance: f64,
455        _damage_modifier: f32,
456        _source: &DamageSource,
457    ) -> bool {
458        let (hurt_entities, damage_per_distance, damage_max) = {
459            let state = self.state.lock();
460            (
461                state.hurt_entities,
462                state.fall_damage_per_distance,
463                state.fall_damage_max,
464            )
465        };
466        if !hurt_entities {
467            return false;
468        }
469
470        let fall_distance = (fall_distance - 1.0).ceil() as i32;
471        if fall_distance < 0 {
472            return false;
473        }
474
475        let source = BLOCK_BEHAVIORS
476            .get_behavior(self.block_state().get_block())
477            .as_fallable()
478            .map_or_else(
479                || {
480                    DamageSource::environment(&vanilla_damage_types::FALLING_BLOCK)
481                        .with_direct_entity(self.id())
482                        .with_causing_entity(self.id())
483                },
484                |fallable| fallable.get_fall_damage_source(self),
485            );
486        let damage = (fall_distance as f32 * damage_per_distance)
487            .floor()
488            .min(damage_max as f32);
489        if let Some(world) = self.level() {
490            for entity in world.get_entities_in_aabb_matching(&self.bounding_box(), |entity| {
491                entity.id() != self.id()
492                    && entity.is_living_entity()
493                    && entity.is_alive()
494                    && !entity.is_spectator()
495                    && entity
496                        .as_player()
497                        .is_none_or(|player| !player.has_infinite_materials())
498            }) {
499                entity.hurt(&world, &source, damage);
500            }
501        }
502
503        let block_state = self.block_state();
504        if block_state.get_block().has_tag(&BlockTag::ANVIL)
505            && damage > 0.0
506            && rand::random::<f32>() < 0.05 + fall_distance as f32 * 0.05
507        {
508            let mut state = self.state.lock();
509            if let Some(damaged) = AnvilBlock::damage(state.block_state) {
510                state.block_state = damaged;
511            } else {
512                state.cancel_drop = true;
513            }
514        }
515        false
516    }
517
518    fn save_additional(&self, nbt: &mut NbtCompound) {
519        let state = self.state.lock();
520        nbt.insert(
521            "BlockState",
522            NbtTag::Compound(block_state_nbt::save(state.block_state)),
523        );
524        nbt.insert("Time", state.time);
525        nbt.insert("DropItem", i8::from(state.drop_item));
526        nbt.insert("HurtEntities", i8::from(state.hurt_entities));
527        nbt.insert("FallHurtAmount", state.fall_damage_per_distance);
528        nbt.insert("FallHurtMax", state.fall_damage_max);
529        if let Some(block_data) = &state.block_data {
530            nbt.insert("TileEntityData", NbtTag::Compound(block_data.clone()));
531        }
532        nbt.insert("CancelDrop", i8::from(state.cancel_drop));
533    }
534
535    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
536        let block_state = nbt
537            .compound("BlockState")
538            .and_then(block_state_nbt::load)
539            .unwrap_or_else(|| vanilla_blocks::SAND.default_state());
540        let mut state = self.state.lock();
541        state.block_state = block_state;
542        state.time = nbt.int("Time").unwrap_or(0);
543        state.hurt_entities = nbt.byte("HurtEntities").map_or_else(
544            || block_state.get_block().has_tag(&BlockTag::ANVIL),
545            |value| value != 0,
546        );
547        state.fall_damage_per_distance = nbt
548            .float("FallHurtAmount")
549            .unwrap_or(DEFAULT_FALL_DAMAGE_PER_DISTANCE);
550        state.fall_damage_max = nbt.int("FallHurtMax").unwrap_or(DEFAULT_MAX_FALL_DAMAGE);
551        state.drop_item = nbt.byte("DropItem").is_none_or(|value| value != 0);
552        state.block_data = nbt.compound("TileEntityData").map(|data| data.to_owned());
553        state.cancel_drop = nbt.byte("CancelDrop").is_some_and(|value| value != 0);
554    }
555}
556
557#[cfg(test)]
558#[path = "falling_block/tests.rs"]
559mod tests;