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