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