Skip to main content

steel_core/entity/
leash.rs

1use std::sync::Arc;
2
3use crate::entity::entities::LeashFenceKnotEntity;
4use crate::entity::{Entity, Mob, SharedEntity, WeakEntity};
5use glam::DVec3;
6use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
7use simdnbt::owned::{NbtCompound, NbtTag};
8use steel_math::DEG_TO_RAD;
9use steel_protocol::packets::game::SoundSource;
10use steel_registry::blocks::block_state_ext::BlockStateExt;
11use steel_registry::item_stack::ItemStack;
12use steel_registry::vanilla_game_rules::ENTITY_DROPS;
13use steel_registry::{sound_events, vanilla_items};
14use steel_utils::locks::SyncMutex;
15use steel_utils::{BlockPos, Downcast, UuidExt as _};
16use uuid::Uuid;
17
18pub const LEASH_SNAP_DISTANCE: f64 = 12.0;
19pub const LEASH_ELASTIC_DISTANCE: f64 = 6.0;
20pub const LEASH_AXIS_SPECIFIC_ELASTICITY: DVec3 = DVec3::new(0.8, 0.2, 0.8);
21pub const LEASH_SPRING_DAMPENING: f64 = 0.7;
22pub const LEASH_TORSIONAL_ELASTICITY: f64 = 10.0;
23pub const LEASH_STIFFNESS: f64 = 0.11;
24pub const ENTITY_LEASH_ATTACHMENT_POINT: DVec3 = DVec3::new(0.0, 0.5, 0.5);
25pub const LEASHER_ATTACHMENT_POINT: DVec3 = DVec3::new(0.0, 0.5, 0.0);
26pub const DELAYED_LEASH_DROP_TICKS: i32 = 100;
27pub const BASE_HORIZONTAL_FRICTION: f64 = 0.91;
28
29/// Vanilla behavior shared by entities that extend `Leashable`.
30///
31/// Leashable entities can be leashed to an entity holding a lead, or a fence holding a lead.
32pub trait Leashable: Entity {
33    /// Returns the shared leash data (if any).
34    fn leash_data(&self) -> &SyncMutex<Option<LeashData>>;
35
36    /// Returns whether this entity is leashed.
37    fn is_leashed(&self) -> bool {
38        self.leash_holder().is_some()
39    }
40
41    /// Returns whether this entity can be leashed with respect to its leash state.
42    ///
43    /// In other words, this returns `false` if the entity is already leashed and `true` if not.
44    ///
45    /// See also: [`Leashable::can_be_leashed`]
46    fn may_be_leashed(&self) -> bool {
47        self.leash_data().lock().is_some()
48    }
49
50    /// Returns the entity holding this entity with a leash, if any.
51    fn leash_holder(&self) -> Option<SharedEntity> {
52        self.leash_data()
53            .lock()
54            .as_ref()
55            .and_then(LeashData::holder)
56    }
57
58    fn leash_attachment(&self) -> Option<LeashAttachment> {
59        self.leash_data()
60            .lock()
61            .as_ref()
62            .and_then(LeashData::attachment)
63    }
64
65    fn set_delayed_leash_attachment(&self, attachment: LeashAttachment) {
66        *self.leash_data().lock() = Some(LeashData::from_delayed_attachment(attachment));
67        self.remove_leash();
68    }
69
70    /// Returns whether this entity can be leashed with respect to the entity's type or classification.
71    ///
72    /// For example, mobs like dolphins and hoglins return `true` for this, while `villagers` return `false`.
73    ///
74    /// See also: [`Leashable::may_be_leashed`]
75    fn can_be_leashed(&self) -> bool {
76        true
77    }
78
79    /// Returns the distance between the bounding box's center of the entity and that of `holder`.
80    ///
81    /// Despite the same, this function does not check for any leash state.
82    fn leash_distance_to(&self, holder: &dyn Entity) -> f64 {
83        leash_bounding_box_center(self.as_entity_event_source())
84            .distance(leash_bounding_box_center(holder))
85    }
86
87    /// Returns the minimum leash distance for which a leash will snap.
88    fn leash_snap_distance(&self) -> f64 {
89        LEASH_SNAP_DISTANCE
90    }
91
92    /// Returns the minimum leash distance for which a leash behaves elastically
93    /// (it pulls the leashed entity towards the holder).
94    fn leash_elastic_distance(&self) -> f64 {
95        LEASH_ELASTIC_DISTANCE
96    }
97
98    /// Called when this entity is leashed to `holder`.
99    fn when_leashed_to(&self, holder: &dyn Entity) {
100        holder.notify_leash_holder(self.as_entity_event_source());
101    }
102
103    /// Called every tick this entity's leash is stretched too far (this entity is too far from its holder).
104    fn leash_too_far_behaviour(&self) {
105        self.drop_leash();
106    }
107
108    /// Called every tick this entity's leash starts acting elastic (it pulls the leashed entity towards the holder).
109    fn on_elastic_leash_pull(&self) {
110        self.check_fall_distance_accumulation();
111    }
112
113    /// Called every tick this leash is not elastic (pulling) or snappable.
114    fn close_range_leash_behaviour(&self, _holder: &dyn Entity) {}
115
116    /// Performs the calculations to pull this entity towards its leash holder and applies
117    /// velocity to it.
118    fn check_elastic_interactions(&self, holder: &dyn Entity) -> bool {
119        let Some(wrench) = compute_elastic_interaction(
120            self.as_entity_event_source(),
121            holder,
122            self.leash_elastic_distance(),
123        ) else {
124            return false;
125        };
126
127        {
128            let mut leash_data = self.leash_data().lock();
129            let Some(leash_data) = leash_data.as_mut() else {
130                return false;
131            };
132            leash_data.angular_momentum += LEASH_TORSIONAL_ELASTICITY * wrench.torque;
133        }
134
135        let relative_velocity_to_leasher =
136            leash_holder_movement(holder) - leash_holder_movement(self.as_entity_event_source());
137        self.push_impulse(
138            axis_specific_leash_elasticity(wrench.force)
139                + relative_velocity_to_leasher * LEASH_STIFFNESS,
140        );
141        true
142    }
143
144    /// Applies some angular momentum by the leash for rotation purposes.
145    fn apply_leash_angular_momentum(&self) -> bool {
146        let angular_friction = self.leash_angular_friction();
147        let angular_momentum = {
148            let mut leash_data = self.leash_data().lock();
149            let Some(leash_data) = leash_data.as_mut() else {
150                return false;
151            };
152            let angular_momentum = leash_data.angular_momentum;
153            leash_data.angular_momentum *= angular_friction;
154            angular_momentum
155        };
156        self.rotate_by_leash_angular_momentum(angular_momentum);
157        true
158    }
159
160    /// Rotates this entity with the provided angular momentum value.
161    fn rotate_by_leash_angular_momentum(&self, angular_momentum: f64) {
162        let (yaw, pitch) = self.rotation();
163        self.set_rotation((yaw - angular_momentum as f32, pitch));
164    }
165
166    /// Returns the angular momentum experienced by this entity (if it is leashed).
167    fn leash_angular_momentum(&self) -> Option<f64> {
168        self.leash_data()
169            .lock()
170            .as_ref()
171            .map(|leash_data| leash_data.angular_momentum)
172    }
173
174    /// Returns the friction multiplier for calculating the angular momentum of a leash.
175    ///
176    /// This is multiplied with the base angular momentum to get the final angular momentum.
177    fn leash_angular_friction(&self) -> f64 {
178        if self.on_ground() {
179            let Some(world) = self.level() else {
180                return BASE_HORIZONTAL_FRICTION;
181            };
182            let Some(pos) = self.block_pos_below_that_affects_movement() else {
183                return BASE_HORIZONTAL_FRICTION;
184            };
185            return f64::from(
186                world.get_block_state(pos).get_block().config.friction
187                    * BASE_HORIZONTAL_FRICTION as f32,
188            );
189        }
190
191        if self.is_in_water() || self.is_in_lava() {
192            return 0.8;
193        }
194
195        BASE_HORIZONTAL_FRICTION
196    }
197
198    /// Returns whether this entity can have a leash attached to another. Mirrors Vanilla's `Leashable.canHaveALeashAttachedTo`.
199    fn can_have_a_leash_attached_to(&self, holder: &dyn Entity) -> bool {
200        self.id() != holder.id()
201            && self.leash_distance_to(holder) <= self.leash_snap_distance()
202            && self.can_be_leashed()
203    }
204
205    /// Sets this entity to be leashed to a holder, removing the old holder's connection, if any.
206    fn set_leashed_to(&self, holder: &SharedEntity) -> bool {
207        if self.id() == holder.id() {
208            return false;
209        }
210
211        let old_holder = self.leash_holder();
212        {
213            let mut leash_data = self.leash_data().lock();
214            if let Some(leash_data) = leash_data.as_mut() {
215                leash_data.set_holder(holder);
216            } else {
217                *leash_data = Some(LeashData::from_entity(holder));
218            }
219        }
220
221        if self.is_passenger() {
222            self.stop_riding();
223        }
224        if let Some(old_holder) = old_holder
225            && old_holder.id() != holder.id()
226        {
227            old_holder.notify_leashee_removed(self.as_entity_event_source());
228        }
229        true
230    }
231
232    /// Updates the delayed leash info to use the entity's current context to resolve
233    /// the entity's actual leash connection (whether it be an external entity or a fence knot).
234    fn restore_leash_from_save(&self) {
235        if let Some(attachment) = self.leash_attachment()
236            && let Some(world) = self.level()
237        {
238            match attachment {
239                LeashAttachment::Entity(uuid) => {
240                    if let Some(holder) = world.get_entity_by_uuid(&uuid) {
241                        let _ = self.set_leashed_to(&holder);
242                        return;
243                    }
244                }
245                LeashAttachment::FenceKnot(pos) => {
246                    if let Some(holder) = LeashFenceKnotEntity::get_or_create_knot(&world, pos) {
247                        let _ = self.set_leashed_to(&holder);
248                        return;
249                    }
250                }
251            }
252
253            if self.tick_count() > DELAYED_LEASH_DROP_TICKS {
254                let _ = self.spawn_at_location(ItemStack::new(&vanilla_items::LEAD), 0.0);
255                self.remove_leash_state();
256            }
257        }
258    }
259
260    /// Ticks the leash *holding* this entity. Mirrors Vanilla's `Leashable.tickLeash`.
261    fn tick_leash(&self) {
262        self.restore_leash_from_save();
263
264        if let Some(holder) = self.leash_holder() {
265            if !self.can_interact_with_level() || !holder.can_interact_with_level() {
266                if let Some(world) = self.level()
267                    && world.get_game_rule(&ENTITY_DROPS)
268                {
269                    self.drop_leash();
270                } else {
271                    self.remove_leash();
272                }
273                return;
274            }
275            if let Some(holder) = self.leash_holder()
276                && holder.level().map(|level| level.key.clone())
277                    == self.level().map(|level| level.key.clone())
278            {
279                let distance_to = self.leash_distance_to(holder.as_ref());
280                self.when_leashed_to(holder.as_ref());
281                let angular_momentum_before_distance_action = self.leash_angular_momentum();
282                if distance_to > self.leash_snap_distance() {
283                    if let Some(world) = self.level() {
284                        world.play_sound_at(
285                            &sound_events::ITEM_LEAD_BREAK,
286                            SoundSource::Neutral,
287                            holder.position(),
288                            1.0,
289                            1.0,
290                            None,
291                        );
292                    }
293                    self.leash_too_far_behaviour();
294                } else if distance_to
295                    > self.leash_elastic_distance()
296                        - f64::from(holder.base().dimensions().width)
297                        - f64::from(self.base().dimensions().width)
298                    && self.check_elastic_interactions(holder.as_ref())
299                {
300                    self.on_elastic_leash_pull();
301                } else {
302                    self.close_range_leash_behaviour(holder.as_ref());
303                }
304                if !self.apply_leash_angular_momentum()
305                    && let Some(angular_momentum) = angular_momentum_before_distance_action
306                {
307                    self.rotate_by_leash_angular_momentum(angular_momentum);
308                }
309            }
310        }
311    }
312
313    /// Breaks the leash and drops a lead item. Mirrors Vanilla's `Leashable.dropLeash`.
314    fn drop_leash(&self) {
315        if self.leash_holder().is_none() {
316            return;
317        }
318
319        let holder = self.remove_leash_state();
320        let _ = self.spawn_at_location(ItemStack::new(&vanilla_items::LEAD), 0.0);
321        if let Some(holder) = holder {
322            holder.notify_leashee_removed(self.as_entity_event_source());
323        }
324    }
325
326    /// Removes the leash without dropping a lead item. Mirrors Vanilla's `Leashable.removeLeash`.
327    fn remove_leash(&self) {
328        if self.leash_holder().is_some()
329            && let Some(holder) = self.remove_leash_state()
330        {
331            holder.notify_leashee_removed(self.as_entity_event_source());
332        }
333    }
334
335    /// Removes the leash state of this entity, returning its holder before the leash's removal, if any.
336    fn remove_leash_state(&self) -> Option<SharedEntity> {
337        self.leash_data()
338            .lock()
339            .take()
340            .and_then(|leash_data| leash_data.holder())
341    }
342}
343
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub enum LeashAttachment {
346    Entity(Uuid),
347    FenceKnot(BlockPos),
348}
349
350#[derive(Debug, Clone)]
351pub struct LeashData {
352    pub holder: LeashHolder,
353    pub angular_momentum: f64,
354}
355
356/// Represents the holder of a leash (entity holding the leash).
357#[derive(Debug, Clone)]
358pub enum LeashHolder {
359    /// A direct entity reference to the leash holder.
360    Entity(WeakEntity),
361    /// An indirect attachment (reference) to the leash holder, which can be resolved later.
362    Delayed(LeashAttachment),
363}
364
365#[derive(Debug, Clone, Copy, PartialEq)]
366pub(super) struct LeashWrench {
367    pub(super) force: DVec3,
368    pub(super) torque: f64,
369}
370
371impl LeashWrench {
372    pub const fn new(force: DVec3, torque: f64) -> Self {
373        Self { force, torque }
374    }
375}
376
377impl LeashData {
378    pub(crate) fn from_entity(holder: &SharedEntity) -> Self {
379        Self {
380            holder: LeashHolder::Entity(Arc::downgrade(holder)),
381            angular_momentum: 0.0,
382        }
383    }
384
385    pub(crate) const fn from_delayed_attachment(attachment: LeashAttachment) -> Self {
386        Self {
387            holder: LeashHolder::Delayed(attachment),
388            angular_momentum: 0.0,
389        }
390    }
391
392    pub(super) fn holder(&self) -> Option<SharedEntity> {
393        let LeashHolder::Entity(entity) = &self.holder else {
394            return None;
395        };
396        entity.upgrade()
397    }
398
399    pub(super) const fn attachment(&self) -> Option<LeashAttachment> {
400        let LeashHolder::Delayed(attachment) = self.holder else {
401            return None;
402        };
403        Some(attachment)
404    }
405
406    pub(super) fn saved_attachment(&self) -> Option<LeashAttachment> {
407        match &self.holder {
408            LeashHolder::Entity(holder) => {
409                let upgraded = holder.upgrade()?;
410                if let Some(knot) = upgraded.downcast_ref::<LeashFenceKnotEntity>() {
411                    // This is a leash knot. Store a position.
412                    Some(LeashAttachment::FenceKnot(knot.block_pos()))
413                } else {
414                    // This is a normal entity. Store its UUID.
415                    Some(LeashAttachment::Entity(upgraded.uuid()))
416                }
417            }
418            LeashHolder::Delayed(attachment) => Some(*attachment),
419        }
420    }
421
422    pub(super) fn set_holder(&mut self, holder: &SharedEntity) {
423        self.holder = LeashHolder::Entity(Arc::downgrade(holder));
424        self.angular_momentum = 0.0;
425    }
426
427    pub(super) fn save(&self, nbt: &mut NbtCompound) {
428        if let Some(attachment) = self.saved_attachment() {
429            match attachment {
430                LeashAttachment::Entity(uuid) => {
431                    let mut leash = NbtCompound::new();
432                    leash.insert("UUID", NbtTag::IntArray(uuid.to_int_array().to_vec()));
433                    nbt.insert("leash", NbtTag::Compound(leash));
434                }
435                LeashAttachment::FenceKnot(pos) => {
436                    nbt.insert("leash", NbtTag::IntArray(vec![pos.x(), pos.y(), pos.z()]));
437                }
438            }
439        }
440    }
441
442    pub(super) fn load(nbt: BorrowedNbtCompoundView<'_, '_>) -> Option<Self> {
443        if let Some(leash) = nbt.compound("leash")
444            && let Some(uuid_array) = leash.int_array("UUID")
445            && let Some(uuid) = Uuid::from_int_array(&uuid_array)
446        {
447            return Some(Self::from_delayed_attachment(LeashAttachment::Entity(uuid)));
448        }
449
450        nbt.int_array("leash")
451            .filter(|position| position.len() == 3)
452            .map(|position| {
453                Self::from_delayed_attachment(LeashAttachment::FenceKnot(BlockPos::new(
454                    position[0],
455                    position[1],
456                    position[2],
457                )))
458            })
459    }
460}
461
462pub(super) fn leash_dimensions(entity: &dyn Entity) -> DVec3 {
463    let dimensions = entity.base().dimensions();
464    DVec3::new(
465        f64::from(dimensions.width),
466        f64::from(dimensions.height),
467        f64::from(dimensions.width),
468    )
469}
470
471pub(super) fn leash_bounding_box_center(entity: &dyn Entity) -> DVec3 {
472    let bounding_box = entity.bounding_box();
473    DVec3::new(
474        f64::midpoint(bounding_box.min_x(), bounding_box.max_x()),
475        f64::midpoint(bounding_box.min_y(), bounding_box.max_y()),
476        f64::midpoint(bounding_box.min_z(), bounding_box.max_z()),
477    )
478}
479
480pub(super) fn leash_holder_movement(entity: &dyn Entity) -> DVec3 {
481    if entity.as_mob().is_some_and(Mob::is_no_ai) {
482        return DVec3::ZERO;
483    }
484
485    entity.known_movement()
486}
487
488pub(super) fn rotate_y(vector: DVec3, radians: f32) -> DVec3 {
489    let cos = f64::from(radians.cos());
490    let sin = f64::from(radians.sin());
491    DVec3::new(
492        vector.x * cos + vector.z * sin,
493        vector.y,
494        vector.z * cos - vector.x * sin,
495    )
496}
497
498pub(super) fn axis_specific_leash_elasticity(force: DVec3) -> DVec3 {
499    force * LEASH_AXIS_SPECIFIC_ELASTICITY
500}
501
502pub(super) fn compute_elastic_interaction(
503    entity: &dyn Entity,
504    holder: &dyn Entity,
505    slack_distance: f64,
506) -> Option<LeashWrench> {
507    let entity_y_rot = entity.rotation().0 * DEG_TO_RAD;
508    let entity_attach_vector = rotate_y(
509        ENTITY_LEASH_ATTACHMENT_POINT * leash_dimensions(entity),
510        -entity_y_rot,
511    );
512    let entity_attach_pos = entity.position() + entity_attach_vector;
513
514    let holder_y_rot = holder.rotation().0 * DEG_TO_RAD;
515    let holder_attach_vector = rotate_y(
516        LEASHER_ATTACHMENT_POINT * leash_dimensions(holder),
517        -holder_y_rot,
518    );
519    let holder_attach_pos = holder.position() + holder_attach_vector;
520
521    compute_dampened_spring_interaction(
522        holder_attach_pos,
523        entity_attach_pos,
524        slack_distance,
525        leash_holder_movement(entity),
526        entity_attach_vector,
527    )
528}
529
530pub(super) fn compute_dampened_spring_interaction(
531    pivot_point: DVec3,
532    object_position: DVec3,
533    spring_slack: f64,
534    object_motion: DVec3,
535    lever_arm: DVec3,
536) -> Option<LeashWrench> {
537    let distance = object_position.distance(pivot_point);
538    if distance < spring_slack {
539        return None;
540    }
541
542    let mut displacement = (pivot_point - object_position).normalize() * (distance - spring_slack);
543    let torque = torque_from_force(lever_arm, displacement);
544    if object_motion.dot(displacement) >= 0.0 {
545        displacement *= 1.0 - LEASH_SPRING_DAMPENING;
546    }
547
548    Some(LeashWrench::new(displacement, torque))
549}
550
551pub(super) fn torque_from_force(lever_arm: DVec3, force: DVec3) -> f64 {
552    lever_arm.z * force.x - lever_arm.x * force.z
553}