Skip to main content

steel_core/entity/
block_attached_entity.rs

1//! Shared Vanilla `BlockAttachedEntity` state and hooks.
2//!
3//! These entities store an additional counter, incrementing each tick,
4//! checking whether it can survive when it reaches the maximum value.
5//! They are also bound to a position, and cannot move.
6
7use crate::entity::damage::DamageSource;
8use crate::entity::mob::block_pos_distance_sqr;
9use crate::entity::{BorrowedNbtCompoundView, EntityMoveError, SharedEntity};
10use crate::entity::{Entity, RemovalReason};
11use crate::physics::{MoveResult, MoverType};
12use crate::world::World;
13use glam::DVec3;
14use simdnbt::owned::{NbtCompound, NbtTag};
15use std::sync::Arc;
16use steel_registry::vanilla_damage_types;
17use steel_registry::vanilla_game_events;
18use steel_registry::vanilla_game_rules::MOB_GRIEFING;
19use steel_utils::BlockPos;
20use steel_utils::locks::SyncMutex;
21
22pub const VALID_READ_DISTANCE_SQR: f64 = 16.0 * 16.0;
23
24/// The time interval between two consecutive survival checks for block-attached entities.
25///
26/// This is equal to 5 seconds in ticks, or `100`.
27pub const CHECK_SURVIVAL_FREQUENCY_TICKS: u32 = 100;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30struct BlockAttachedEntityState {
31    check_interval: u32,
32    pos: BlockPos,
33}
34
35impl BlockAttachedEntityState {
36    pub const fn new(pos: BlockPos) -> Self {
37        Self {
38            check_interval: 0,
39            pos,
40        }
41    }
42}
43
44/// Runtime fields shared by Vanilla's block-attached entities (those inheriting `BlockAttachedEntity` in Vanilla).
45#[derive(Debug)]
46pub struct BlockAttachedEntityBase {
47    state: SyncMutex<BlockAttachedEntityState>,
48}
49
50impl BlockAttachedEntityBase {
51    /// Creates a new [`BlockAttachedEntityBase`] with the given position.
52    pub const fn new(pos: BlockPos) -> Self {
53        BlockAttachedEntityBase {
54            state: SyncMutex::new(BlockAttachedEntityState::new(pos)),
55        }
56    }
57
58    /// Gets the position of this base's entity.
59    pub fn pos(&self) -> BlockPos {
60        self.state.lock().pos
61    }
62
63    /// Sets the position of this base's entity to the given position.
64    pub fn set_pos(&self, pos: BlockPos) {
65        self.state.lock().pos = pos;
66    }
67
68    /// Increments the internal counter of this base, returning whether to check for survival.
69    /// - If it is the maximum value of [`CHECK_SURVIVAL_FREQUENCY_TICKS`], this returns `true`, and the counter is reset.
70    /// - If not, this returns `false`, and increments the counter.
71    pub fn should_check_survival(&self) -> bool {
72        let mut state = self.state.lock();
73
74        if state.check_interval == CHECK_SURVIVAL_FREQUENCY_TICKS {
75            state.check_interval = 0;
76            true
77        } else {
78            state.check_interval += 1;
79            false
80        }
81    }
82}
83
84pub trait BlockAttachedEntity: Entity {
85    /// Returns the shared block-attached entity runtime state.
86    fn block_attached_entity_base(&self) -> &BlockAttachedEntityBase;
87
88    fn survives(&self) -> bool;
89
90    fn drop_item(&self, caused_by: Option<&dyn Entity>);
91
92    fn recalculate_bounding_box(&self) -> Result<(), EntityMoveError>;
93
94    fn tick_block_attached_entity(self: Arc<Self>) {
95        self.check_below_world();
96        if self.block_attached_entity_base().should_check_survival() {
97            // Check for survival.
98            if !self.is_removed() && !self.survives() {
99                self.set_removed(RemovalReason::Discarded);
100                self.drop_item(None);
101            }
102        }
103    }
104
105    fn is_pickable_block_attached_entity(&self) -> bool {
106        true
107    }
108
109    fn skip_attack_interaction_block_attached_entity(&self, source: &SharedEntity) -> bool {
110        if let Some(player) = source.as_player()
111            && let Some(world) = source.level()
112        {
113            let damage_source = DamageSource::environment(&vanilla_damage_types::PLAYER_ATTACK)
114                .with_causing_entity(Arc::clone(source))
115                .with_direct_entity(Arc::clone(source));
116
117            !world.may_interact(player, self.block_attached_entity_base().pos())
118                || self.hurt(&world, &damage_source, 0.0)
119        } else {
120            false
121        }
122    }
123
124    /// Removes this entity as killed, attributing the death game event to `attributed_to`
125    /// (or to this entity when `None`).
126    fn kill_attributed(&self, attributed_to: Option<&dyn Entity>) {
127        self.set_removed(RemovalReason::Killed);
128        self.game_event_with_source_entity(
129            &vanilla_game_events::ENTITY_DIE,
130            attributed_to.or(Some(self.as_entity_event_source())),
131        );
132    }
133
134    fn hurt_block_attached_entity(
135        &self,
136        world: &World,
137        source: &DamageSource,
138        _amount: f32,
139    ) -> bool {
140        if self.is_invulnerable_to_base(source) {
141            return false;
142        }
143
144        let causing_entity = source.causing_entity();
145
146        if !world.get_game_rule(&MOB_GRIEFING)
147            && let Some(causing_entity) = &causing_entity
148            && causing_entity.is_mob()
149        {
150            return false;
151        }
152
153        if !self.is_removed() {
154            self.kill_attributed(causing_entity.map(AsRef::as_ref));
155            self.mark_hurt();
156            self.drop_item(causing_entity.map(AsRef::as_ref));
157        }
158
159        true
160    }
161
162    fn drop_if_dvec3_is_nonzero(&self, impulse: DVec3) {
163        if let Some(world) = self.level()
164            && !self.is_removed()
165            && impulse.length_squared() > 0.0
166        {
167            self.kill(&world);
168            self.drop_item(None);
169        }
170    }
171
172    fn move_entity_block_attached_entity(
173        self: Arc<Self>,
174        _mover_type: MoverType,
175        delta: DVec3,
176    ) -> Option<MoveResult> {
177        self.drop_if_dvec3_is_nonzero(delta);
178        None
179    }
180
181    fn push_impulse_block_attached_entity(&self, impulse: DVec3) {
182        self.drop_if_dvec3_is_nonzero(impulse);
183    }
184
185    fn save_block_attached_entity(&self, nbt: &mut NbtCompound) {
186        let block_pos = self.block_attached_entity_base().pos();
187        nbt.insert(
188            "block_pos",
189            NbtTag::IntArray(vec![block_pos.x(), block_pos.y(), block_pos.z()]),
190        );
191    }
192
193    fn load_block_attached_entity(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
194        if let Some(block_pos_vec) = nbt.int_array("block_pos")
195            && block_pos_vec.len() == 3
196        {
197            let block_pos = BlockPos::new(block_pos_vec[0], block_pos_vec[1], block_pos_vec[2]);
198            if block_pos_distance_sqr(block_pos, self.block_position()) >= VALID_READ_DISTANCE_SQR {
199                log::warn!("Block-attached entity at invalid position: {block_pos:?}");
200                return;
201            }
202            self.block_attached_entity_base().set_pos(block_pos);
203        }
204    }
205
206    fn refresh_dimensions_block_attached_entity(&self) {}
207
208    fn try_set_position_block_attached_entity(&self, pos: DVec3) -> Result<(), EntityMoveError> {
209        self.block_attached_entity_base()
210            .set_pos(BlockPos::containing(pos.x, pos.y, pos.z));
211        self.recalculate_bounding_box()?;
212        self.base().mark_velocity_sync();
213        Ok(())
214    }
215
216    // TODO: Add a default implementation for `thunderHit` and `ignoreExplosion`
217}