Skip to main content

steel_core/entity/entities/
leash_fence_knot.rs

1//! Leash fence knot entity foundation.
2
3use std::sync::{Arc, Weak};
4
5use glam::DVec3;
6use steel_macros::entity_behavior;
7use steel_registry::blocks::block_state_ext::BlockStateExt as _;
8use steel_registry::entity_type::EntityTypeRef;
9use steel_registry::sound_events;
10use steel_registry::vanilla_block_tags::BlockTag;
11use steel_registry::vanilla_entities;
12use steel_utils::locks::SyncMutex;
13use steel_utils::{BlockPos, Downcast as _, DowncastType, DowncastTypeKey, WorldAabb};
14
15use crate::entity::{
16    Entity, EntityBase, EntityBaseLoad, EntityBaseState, RemovalReason, SharedEntity,
17    next_entity_id,
18};
19use crate::world::World;
20
21/// Vanilla leash knot attached to a fence block.
22#[entity_behavior(class = "LeashFenceKnotEntity")]
23pub struct LeashFenceKnotEntity {
24    base: EntityBase,
25    entity_type: EntityTypeRef,
26    block_pos: SyncMutex<BlockPos>,
27    check_interval: SyncMutex<i32>,
28}
29
30// SAFETY: This key is owned by Steel and uniquely identifies `LeashFenceKnotEntity`.
31unsafe impl DowncastType for LeashFenceKnotEntity {
32    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/leash_fence_knot");
33}
34
35impl LeashFenceKnotEntity {
36    /// Creates a fresh leash knot from the generic entity factory path.
37    #[must_use]
38    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
39        Self::new_attached(
40            entity_type,
41            id,
42            BlockPos::new(
43                position.x.floor() as i32,
44                position.y.floor() as i32,
45                position.z.floor() as i32,
46            ),
47            world,
48        )
49    }
50
51    /// Creates a fresh leash knot attached to `block_pos`.
52    #[must_use]
53    pub fn new_attached(
54        entity_type: EntityTypeRef,
55        id: i32,
56        block_pos: BlockPos,
57        world: Weak<World>,
58    ) -> Self {
59        Self {
60            base: EntityBase::new_with_state(
61                id,
62                EntityBaseState::new_with_bounding_box(
63                    Self::knot_center(block_pos),
64                    entity_type.dimensions,
65                    Self::knot_bounding_box(entity_type, block_pos),
66                ),
67                world,
68            ),
69            entity_type,
70            block_pos: SyncMutex::new(block_pos),
71            check_interval: SyncMutex::new(0),
72        }
73    }
74
75    /// Creates a leash knot from persistent entity data.
76    #[must_use]
77    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
78        let position = load.position;
79        let block_pos = BlockPos::new(
80            position.x.floor() as i32,
81            position.y.floor() as i32,
82            position.z.floor() as i32,
83        );
84        Self {
85            base: EntityBase::from_load(load, entity_type.dimensions),
86            entity_type,
87            block_pos: SyncMutex::new(block_pos),
88            check_interval: SyncMutex::new(0),
89        }
90    }
91
92    /// Returns the fence block this knot is attached to.
93    #[must_use]
94    pub fn block_pos(&self) -> BlockPos {
95        *self.block_pos.lock()
96    }
97
98    /// Returns true when the backing fence block still supports this knot.
99    #[must_use]
100    pub fn survives(&self) -> bool {
101        let Some(world) = self.level() else {
102            return false;
103        };
104        world
105            .get_block_state(self.block_pos())
106            .get_block()
107            .has_tag(&BlockTag::FENCES)
108    }
109
110    /// Finds an existing leash knot at `pos`.
111    #[must_use]
112    pub fn get_knot(world: &World, pos: BlockPos) -> Option<SharedEntity> {
113        let search_box = WorldAabb::new(
114            f64::from(pos.x()) - 1.0,
115            f64::from(pos.y()) - 1.0,
116            f64::from(pos.z()) - 1.0,
117            f64::from(pos.x()) + 1.0,
118            f64::from(pos.y()) + 1.0,
119            f64::from(pos.z()) + 1.0,
120        );
121        world
122            .get_entities_in_aabb_matching(&search_box, |entity| {
123                entity
124                    .downcast_ref::<Self>()
125                    .is_some_and(|knot| knot.block_pos() == pos)
126            })
127            .into_iter()
128            .next()
129    }
130
131    /// Gets or creates a leash knot at `pos`.
132    #[must_use]
133    pub fn get_or_create_knot(world: &Arc<World>, pos: BlockPos) -> Option<SharedEntity> {
134        if let Some(knot) = Self::get_knot(world.as_ref(), pos) {
135            return Some(knot);
136        }
137
138        let knot: SharedEntity = Arc::new(Self::new_attached(
139            &vanilla_entities::LEASH_KNOT,
140            next_entity_id(),
141            pos,
142            Arc::downgrade(world),
143        ));
144        if let Err(error) = world.try_add_entity(Arc::clone(&knot)) {
145            log::warn!("Failed to spawn leash knot entity: {error}");
146            return None;
147        }
148
149        Some(knot)
150    }
151
152    fn should_check_survival(&self) -> bool {
153        let mut check_interval = self.check_interval.lock();
154        if *check_interval == 100 {
155            *check_interval = 0;
156            true
157        } else {
158            *check_interval += 1;
159            false
160        }
161    }
162
163    fn play_drop_sound(&self) {
164        self.play_sound(&sound_events::ITEM_LEAD_UNTIED, 1.0, 1.0);
165    }
166
167    fn knot_center(block_pos: BlockPos) -> DVec3 {
168        DVec3::new(
169            f64::from(block_pos.x()) + 0.5,
170            f64::from(block_pos.y()) + 0.375,
171            f64::from(block_pos.z()) + 0.5,
172        )
173    }
174
175    fn knot_bounding_box(entity_type: EntityTypeRef, block_pos: BlockPos) -> WorldAabb {
176        let center = Self::knot_center(block_pos);
177        let half_width = f64::from(entity_type.dimensions.width) / 2.0;
178        let height = f64::from(entity_type.dimensions.height);
179        WorldAabb::new(
180            center.x - half_width,
181            center.y,
182            center.z - half_width,
183            center.x + half_width,
184            center.y + height,
185            center.z + half_width,
186        )
187    }
188}
189
190impl Entity for LeashFenceKnotEntity {
191    fn base(&self) -> &EntityBase {
192        &self.base
193    }
194
195    fn entity_type(&self) -> EntityTypeRef {
196        self.entity_type
197    }
198
199    fn spawn_position(&self) -> DVec3 {
200        let block_pos = self.block_pos();
201        DVec3::new(
202            f64::from(block_pos.x()),
203            f64::from(block_pos.y()),
204            f64::from(block_pos.z()),
205        )
206    }
207
208    fn notify_leashee_removed(&self, _leashable: &dyn Entity) {
209        if self.level().is_some() && self.leashables_leashed_to().is_empty() {
210            self.set_removed(RemovalReason::Discarded);
211        }
212    }
213
214    fn tick(&self) {
215        if self.level().is_none() {
216            return;
217        }
218        self.check_below_world();
219        if self.should_check_survival() && !self.is_removed() && !self.survives() {
220            self.set_removed(RemovalReason::Discarded);
221            self.play_drop_sound();
222        }
223    }
224
225    fn is_pickable(&self) -> bool {
226        true
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233    use simdnbt::owned::NbtCompound;
234
235    #[test]
236    fn leash_knot_uses_vanilla_position_and_bounding_box() {
237        let knot = LeashFenceKnotEntity::new_attached(
238            &vanilla_entities::LEASH_KNOT,
239            1,
240            BlockPos::new(4, 65, -9),
241            Weak::new(),
242        );
243
244        assert_eq!(knot.position(), DVec3::new(4.5, 65.375, -8.5));
245        assert_eq!(
246            knot.bounding_box(),
247            LeashFenceKnotEntity::knot_bounding_box(
248                &vanilla_entities::LEASH_KNOT,
249                BlockPos::new(4, 65, -9)
250            )
251        );
252    }
253
254    #[test]
255    fn leash_knot_spawn_packet_uses_attached_block_pos() {
256        let knot = LeashFenceKnotEntity::new_attached(
257            &vanilla_entities::LEASH_KNOT,
258            1,
259            BlockPos::new(4, 65, -9),
260            Weak::new(),
261        );
262
263        assert_eq!(knot.spawn_position(), DVec3::new(4.0, 65.0, -9.0));
264    }
265
266    #[test]
267    fn leash_knot_saves_no_type_specific_block_pos() {
268        let knot = LeashFenceKnotEntity::new_attached(
269            &vanilla_entities::LEASH_KNOT,
270            1,
271            BlockPos::new(4, 65, -9),
272            Weak::new(),
273        );
274
275        let mut nbt = NbtCompound::new();
276        knot.save_additional(&mut nbt);
277
278        assert!(nbt.is_empty());
279    }
280
281    #[test]
282    fn leash_knot_survival_check_matches_vanilla_interval() {
283        let knot = LeashFenceKnotEntity::new_attached(
284            &vanilla_entities::LEASH_KNOT,
285            1,
286            BlockPos::new(4, 65, -9),
287            Weak::new(),
288        );
289
290        for _ in 0..100 {
291            assert!(!knot.should_check_survival());
292        }
293        assert!(knot.should_check_survival());
294        assert!(!knot.should_check_survival());
295    }
296}