Skip to main content

steel_core/entity/entities/objects/display_ui/
leash_fence_knot.rs

1//! Leash fence knot entity foundation.
2
3use std::sync::{Arc, Weak};
4
5use crate::behavior::InteractionResult;
6use crate::entity::damage::DamageSource;
7use crate::entity::{
8    Entity, EntityBase, EntityBaseLoad, EntityBaseState, RemovalReason, SharedEntity,
9    next_entity_id,
10};
11use crate::player::Player;
12use crate::world::World;
13use glam::DVec3;
14use steel_macros::entity_behavior;
15use steel_registry::blocks::block_state_ext::BlockStateExt as _;
16use steel_registry::entity_type::EntityTypeRef;
17use steel_registry::item_stack::ItemStack;
18use steel_registry::sound_events::ITEM_LEAD_TIED;
19use steel_registry::vanilla_block_tags::BlockTag;
20use steel_registry::vanilla_entities;
21use steel_registry::vanilla_game_events::BLOCK_ATTACH;
22use steel_registry::vanilla_game_rules::MOB_GRIEFING;
23use steel_registry::{sound_events, vanilla_items};
24use steel_utils::locks::SyncMutex;
25use steel_utils::types::InteractionHand;
26use steel_utils::{BlockPos, Downcast as _, DowncastType, DowncastTypeKey, WorldAabb};
27
28/// Vanilla leash knot attached to a fence block.
29#[entity_behavior(class = "LeashFenceKnotEntity")]
30pub struct LeashFenceKnotEntity {
31    base: EntityBase,
32    entity_type: EntityTypeRef,
33    block_pos: SyncMutex<BlockPos>,
34    check_interval: SyncMutex<i32>,
35}
36
37// SAFETY: This key is owned by Steel and uniquely identifies `LeashFenceKnotEntity`.
38unsafe impl DowncastType for LeashFenceKnotEntity {
39    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/leash_fence_knot");
40}
41
42impl LeashFenceKnotEntity {
43    /// Creates a fresh leash knot from the generic entity factory path.
44    #[must_use]
45    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
46        Self::new_attached(
47            entity_type,
48            id,
49            BlockPos::new(
50                position.x.floor() as i32,
51                position.y.floor() as i32,
52                position.z.floor() as i32,
53            ),
54            world,
55        )
56    }
57
58    /// Creates a fresh leash knot attached to `block_pos`.
59    #[must_use]
60    pub fn new_attached(
61        entity_type: EntityTypeRef,
62        id: i32,
63        block_pos: BlockPos,
64        world: Weak<World>,
65    ) -> Self {
66        Self {
67            base: EntityBase::new_with_state(
68                id,
69                EntityBaseState::new_with_bounding_box(
70                    Self::knot_center(block_pos),
71                    entity_type.dimensions,
72                    Self::knot_bounding_box(entity_type, block_pos),
73                ),
74                world,
75            ),
76            entity_type,
77            block_pos: SyncMutex::new(block_pos),
78            check_interval: SyncMutex::new(0),
79        }
80    }
81
82    /// Creates a leash knot from persistent entity data.
83    #[must_use]
84    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
85        let position = load.position;
86        let block_pos = BlockPos::new(
87            position.x.floor() as i32,
88            position.y.floor() as i32,
89            position.z.floor() as i32,
90        );
91        Self {
92            base: EntityBase::from_load(load, entity_type.dimensions),
93            entity_type,
94            block_pos: SyncMutex::new(block_pos),
95            check_interval: SyncMutex::new(0),
96        }
97    }
98
99    /// Returns the fence block this knot is attached to.
100    #[must_use]
101    pub fn block_pos(&self) -> BlockPos {
102        *self.block_pos.lock()
103    }
104
105    /// Returns true when the backing fence block still supports this knot.
106    #[must_use]
107    pub fn survives(&self) -> bool {
108        let Some(world) = self.level() else {
109            return false;
110        };
111        world
112            .get_block_state(self.block_pos())
113            .get_block()
114            .has_tag(&BlockTag::FENCES)
115    }
116
117    /// Finds an existing leash knot at `pos`.
118    #[must_use]
119    pub fn get_knot(world: &World, pos: BlockPos) -> Option<SharedEntity> {
120        let search_box = WorldAabb::new(
121            f64::from(pos.x()) - 1.0,
122            f64::from(pos.y()) - 1.0,
123            f64::from(pos.z()) - 1.0,
124            f64::from(pos.x()) + 1.0,
125            f64::from(pos.y()) + 1.0,
126            f64::from(pos.z()) + 1.0,
127        );
128        world
129            .get_entities_in_aabb_matching(&search_box, |entity| {
130                entity
131                    .downcast_ref::<Self>()
132                    .is_some_and(|knot| knot.block_pos() == pos)
133            })
134            .into_iter()
135            .next()
136    }
137
138    /// Gets or creates a leash knot at `pos`.
139    #[must_use]
140    pub fn get_or_create_knot(world: &Arc<World>, pos: BlockPos) -> Option<SharedEntity> {
141        if let Some(knot) = Self::get_knot(world.as_ref(), pos) {
142            return Some(knot);
143        }
144
145        let knot: SharedEntity = Arc::new(Self::new_attached(
146            &vanilla_entities::LEASH_KNOT,
147            next_entity_id(),
148            pos,
149            Arc::downgrade(world),
150        ));
151        if let Err(error) = world.try_add_entity(Arc::clone(&knot)) {
152            log::warn!("Failed to spawn leash knot entity: {error}");
153            return None;
154        }
155
156        Some(knot)
157    }
158
159    fn should_check_survival(&self) -> bool {
160        let mut check_interval = self.check_interval.lock();
161        if *check_interval == 100 {
162            *check_interval = 0;
163            true
164        } else {
165            *check_interval += 1;
166            false
167        }
168    }
169
170    fn drop_item(&self) {
171        self.play_sound(&sound_events::ITEM_LEAD_UNTIED, 1.0, 1.0);
172
173        // Vanilla does not drop a lead here. However, due to how Rust handles `Weak`
174        // pointers to leash holders when a holder despawns, in the code where a lead
175        // is supposed in drop in Vanilla, the holder is `None`. So, a lead does not drop
176        // in `leash_tick`. We can replicate this behavior by dropping it for each entity instead.
177        for entity in self.leashables_leashed_to() {
178            entity.spawn_at_location(ItemStack::new(&vanilla_items::LEAD), 0.0);
179        }
180    }
181
182    fn knot_center(block_pos: BlockPos) -> DVec3 {
183        DVec3::new(
184            f64::from(block_pos.x()) + 0.5,
185            f64::from(block_pos.y()) + 0.375,
186            f64::from(block_pos.z()) + 0.5,
187        )
188    }
189
190    fn knot_bounding_box(entity_type: EntityTypeRef, block_pos: BlockPos) -> WorldAabb {
191        let center = Self::knot_center(block_pos);
192        let half_width = f64::from(entity_type.dimensions.width) / 2.0;
193        let height = f64::from(entity_type.dimensions.height);
194        WorldAabb::new(
195            center.x - half_width,
196            center.y,
197            center.z - half_width,
198            center.x + half_width,
199            center.y + height,
200            center.z + half_width,
201        )
202    }
203}
204
205impl Entity for LeashFenceKnotEntity {
206    fn base(&self) -> &EntityBase {
207        &self.base
208    }
209
210    fn entity_type(&self) -> EntityTypeRef {
211        self.entity_type
212    }
213
214    fn spawn_position(&self) -> DVec3 {
215        let block_pos = self.block_pos();
216        DVec3::new(
217            f64::from(block_pos.x()),
218            f64::from(block_pos.y()),
219            f64::from(block_pos.z()),
220        )
221    }
222
223    fn notify_leashee_removed(&self, _leashable: &dyn Entity) {
224        if self.level().is_some() && self.leashables_leashed_to().is_empty() {
225            self.set_removed(RemovalReason::Discarded);
226        }
227    }
228
229    fn tick(&self) {
230        if self.level().is_none() {
231            return;
232        }
233        self.check_below_world();
234        if self.should_check_survival() && !self.is_removed() && !self.survives() {
235            self.set_removed(RemovalReason::Discarded);
236            self.drop_item();
237        }
238    }
239
240    fn interact(
241        &self,
242        player: &Player,
243        hand: InteractionHand,
244        location: DVec3,
245    ) -> InteractionResult {
246        let Some(world) = self.level() else {
247            return InteractionResult::Pass;
248        };
249
250        let holding_shears = {
251            let inventory = player.inventory.lock();
252            inventory.get_item_in_hand(hand).is(&vanilla_items::SHEARS)
253        };
254        if holding_shears {
255            let result = self.interact_entity(player, hand, location);
256            if result == InteractionResult::Success {
257                return result;
258            }
259        }
260
261        let mut attached_mob = false;
262        let Some(knot) = world.get_entity_by_id(self.id()) else {
263            return InteractionResult::Pass;
264        };
265        for entity in player.leashables_leashed_to() {
266            if let Some(leashable) = entity.as_leashable()
267                && leashable.can_have_a_leash_attached_to(self)
268            {
269                leashable.set_leashed_to(&knot);
270                attached_mob = true;
271            }
272        }
273
274        let mut any_dropped = false;
275        let Some(player_entity) = world.get_entity_by_id(player.id()) else {
276            return InteractionResult::Pass;
277        };
278        if !attached_mob && !player.is_secondary_use_active() {
279            for entity in knot.leashables_leashed_to() {
280                if let Some(leashable) = entity.as_leashable()
281                    && leashable.can_have_a_leash_attached_to(player)
282                {
283                    leashable.set_leashed_to(&player_entity);
284                    any_dropped = true;
285                }
286            }
287        }
288
289        if !attached_mob && !any_dropped {
290            return self.interact_entity(player, hand, location);
291        }
292
293        self.game_event_with_source_entity(&BLOCK_ATTACH, Some(player));
294        self.play_sound(&ITEM_LEAD_TIED, 1.0, 1.0);
295
296        InteractionResult::Success
297    }
298
299    fn is_pickable(&self) -> bool {
300        true
301    }
302
303    fn hurt(&self, world: &World, source: &DamageSource, _amount: f32) -> bool {
304        if self.is_invulnerable_to_base(source) {
305            return false;
306        }
307
308        let causing_entity = source
309            .causing_entity_id
310            .and_then(|id| world.get_entity_by_id(id));
311
312        if !world.get_game_rule(&MOB_GRIEFING)
313            && let Some(causing_entity) = causing_entity
314            && causing_entity.is_mob()
315        {
316            return false;
317        }
318
319        if !self.is_removed() {
320            self.kill(world);
321            self.mark_hurt();
322            self.drop_item();
323        }
324
325        true
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332    use simdnbt::owned::NbtCompound;
333
334    #[test]
335    fn leash_knot_uses_vanilla_position_and_bounding_box() {
336        let knot = LeashFenceKnotEntity::new_attached(
337            &vanilla_entities::LEASH_KNOT,
338            1,
339            BlockPos::new(4, 65, -9),
340            Weak::new(),
341        );
342
343        assert_eq!(knot.position(), DVec3::new(4.5, 65.375, -8.5));
344        assert_eq!(
345            knot.bounding_box(),
346            LeashFenceKnotEntity::knot_bounding_box(
347                &vanilla_entities::LEASH_KNOT,
348                BlockPos::new(4, 65, -9)
349            )
350        );
351    }
352
353    #[test]
354    fn leash_knot_spawn_packet_uses_attached_block_pos() {
355        let knot = LeashFenceKnotEntity::new_attached(
356            &vanilla_entities::LEASH_KNOT,
357            1,
358            BlockPos::new(4, 65, -9),
359            Weak::new(),
360        );
361
362        assert_eq!(knot.spawn_position(), DVec3::new(4.0, 65.0, -9.0));
363    }
364
365    #[test]
366    fn leash_knot_saves_no_type_specific_block_pos() {
367        let knot = LeashFenceKnotEntity::new_attached(
368            &vanilla_entities::LEASH_KNOT,
369            1,
370            BlockPos::new(4, 65, -9),
371            Weak::new(),
372        );
373
374        let mut nbt = NbtCompound::new();
375        knot.save_additional(&mut nbt);
376
377        assert!(nbt.is_empty());
378    }
379
380    #[test]
381    fn leash_knot_survival_check_matches_vanilla_interval() {
382        let knot = LeashFenceKnotEntity::new_attached(
383            &vanilla_entities::LEASH_KNOT,
384            1,
385            BlockPos::new(4, 65, -9),
386            Weak::new(),
387        );
388
389        for _ in 0..100 {
390            assert!(!knot.should_check_survival());
391        }
392        assert!(knot.should_check_survival());
393        assert!(!knot.should_check_survival());
394    }
395}