1use 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_behavior(class = "FallingBlockEntity")]
74pub struct FallingBlockEntity {
75 base: EntityBase,
76 entity_type: EntityTypeRef,
77 entity_data: SyncMutex<FallingBlockEntityData>,
78 state: SyncMutex<FallingBlockState>,
79}
80
81unsafe impl DowncastType for FallingBlockEntity {
83 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/falling_block");
84}
85
86impl FallingBlockEntity {
87 #[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 #[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 #[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 #[must_use]
166 pub fn block_state(&self) -> BlockStateId {
167 self.state.lock().block_state
168 }
169
170 #[must_use]
172 pub fn time(&self) -> i32 {
173 self.state.lock().time
174 }
175
176 #[must_use]
178 pub fn start_pos(&self) -> BlockPos {
179 *self.entity_data.lock().start_pos.get()
180 }
181
182 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 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;