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