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