diff --git a/Content.Shared/Floofstation/Leash/Components/LeashComponent.cs b/Content.Shared/Floofstation/Leash/Components/LeashComponent.cs
index 7d17ecb8e8..181d61b071 100644
--- a/Content.Shared/Floofstation/Leash/Components/LeashComponent.cs
+++ b/Content.Shared/Floofstation/Leash/Components/LeashComponent.cs
@@ -55,8 +55,12 @@ public sealed partial class LeashComponent : Component
[DataDefinition, Serializable, NetSerializable]
public sealed partial class LeashData
{
+ ///
+ /// Id of the joint created by this leash. May be null if this leash does not currently create a joint
+ /// (e.g. because it's attached to the same entity who holds it)
+ ///
[DataField]
- public string JointId = string.Empty;
+ public string? JointId = null;
[DataField]
public NetEntity Pulled = NetEntity.Invalid;
@@ -67,7 +71,7 @@ public sealed partial class LeashData
[DataField]
public NetEntity? LeashVisuals = null;
- public LeashData(string jointId, NetEntity pulled)
+ public LeashData(string? jointId, NetEntity pulled)
{
JointId = jointId;
Pulled = pulled;
diff --git a/Content.Shared/Floofstation/Leash/LeashSystem.cs b/Content.Shared/Floofstation/Leash/LeashSystem.cs
index ca52d8928c..9b4bd293f9 100644
--- a/Content.Shared/Floofstation/Leash/LeashSystem.cs
+++ b/Content.Shared/Floofstation/Leash/LeashSystem.cs
@@ -45,6 +45,9 @@ public override void Initialize()
SubscribeLocalEvent(OnJointRemoved, after: [typeof(SharedJointSystem)]);
SubscribeLocalEvent>(OnGetLeashedVerbs);
+ SubscribeLocalEvent(OnLeashInserted);
+ SubscribeLocalEvent(OnLeashRemoved);
+
SubscribeLocalEvent(OnAttachDoAfter);
SubscribeLocalEvent(OnDetachDoAfter);
@@ -74,6 +77,7 @@ public override void Update(float frameTime)
// Client side only: set max distance to infinity to prevent the client from ever predicting leashes.
if (_net.IsClient
+ && data.JointId is not null
&& TryComp(target, out var jointComp)
&& jointComp.GetJoints.TryGetValue(data.JointId, out var joint)
&& joint is DistanceJoint distanceJoint
@@ -167,17 +171,37 @@ private void OnJointRemoved(Entity ent, ref JointRemovedEvent
|| ent.Comp.Puller is not { } puller
|| !TryComp(ent.Comp.Anchor, out var anchor)
|| !TryComp(puller, out var leash)
- || leash.Leashed.All(it => it.JointId != id)
- || !Transform(ent).Coordinates.TryDistance(EntityManager, Transform(puller).Coordinates, out var dst)
- || dst > leash.MaxDistance
- )
+ || leash.Leashed.All(it => it.JointId != id))
return;
- // If the entity still has a leashed comp, and is on the same map, and is within the max distance of the leash
- // Then the leash was likely broken due to some weird unforeseen fucking robust toolbox magic. We can try to recreate it.
- // This is hella unsafe to do. It will crash in debug builds under certain conditions. Luckily, release builds are safe.
RemoveLeash(ent!, (puller, leash), false);
- DoLeash((ent.Comp.Anchor.Value, anchor), (puller, leash), ent);
+
+ // If the entity still has a leashed comp, and is on the same map, and is within the max distance of the leash
+ // Then the leash was likely broken due to some weird unforeseen fucking robust toolbox magic.
+ // We can try to recreate it, but on the next tick.
+ Timer.Spawn(0, () =>
+ {
+ if (TerminatingOrDeleted(ent.Comp.Anchor.Value)
+ || TerminatingOrDeleted(puller)
+ || !Transform(ent).Coordinates.TryDistance(EntityManager, Transform(puller).Coordinates, out var dst)
+ || dst > leash.MaxDistance
+ )
+ return;
+
+ DoLeash((ent.Comp.Anchor.Value, anchor), (puller, leash), ent);
+ });
+ }
+
+ private void OnLeashInserted(Entity ent, ref EntGotInsertedIntoContainerMessage args)
+ {
+ if (!_net.IsClient)
+ RefreshJoints(ent);
+ }
+
+ private void OnLeashRemoved(Entity ent, ref EntGotRemovedFromContainerMessage args)
+ {
+ if (!_net.IsClient)
+ RefreshJoints(ent);
}
private void OnAttachDoAfter(Entity ent, ref LeashAttachDoAfterEvent args)
@@ -257,9 +281,27 @@ private bool TryGetLeashTarget(Entity ent, out EntityUid
return true;
}
+ ///
+ /// Returns true if a leash joint can be created between the two specified entities.
+ /// This will return false if one of the entities is a parent of another.
+ ///
+ public bool CanCreateJoint(EntityUid a, EntityUid b)
+ {
+ BaseContainer? aOuter = null, bOuter = null;
+
+ // If neither of the entities are in contianers, it's safe to create a joint
+ if (!_container.TryGetOuterContainer(a, Transform(a), out aOuter)
+ && !_container.TryGetOuterContainer(b, Transform(b), out bOuter))
+ return true;
+
+ // Otherwise, we need to make sure that neither of the entities contain the other, and that they are not in the same container.
+ return a != bOuter?.Owner && b != aOuter?.Owner && aOuter?.Owner != bOuter?.Owner;
+ }
+
private DistanceJoint CreateLeashJoint(string jointId, Entity leash, EntityUid leashTarget)
{
var joint = _joints.CreateDistanceJoint(leash, leashTarget, id: jointId);
+
joint.CollideConnected = false;
joint.Length = leash.Comp.Length;
joint.MinLength = 0f;
@@ -281,10 +323,10 @@ public bool CanLeash(Entity anchor, Entity
&& TryGetLeashTarget(anchor!, out var leashTarget)
&& CompOrNull(leashTarget)?.JointId == null
&& Transform(anchor).Coordinates.TryDistance(EntityManager, Transform(leash).Coordinates, out var dst)
- && dst <= leash.Comp.Length
- && !_xform.IsParentOf(Transform(leashTarget), leash); // google recursion - this makes the game explode for some reason
+ && dst <= leash.Comp.Length;
}
+
public bool TryLeash(Entity anchor, Entity leash, EntityUid user, bool popup = true)
{
if (!CanLeash(anchor, leash) || !TryGetLeashTarget(anchor!, out var leashTarget))
@@ -362,13 +404,21 @@ public void DoLeash(Entity anchor, Entity
var leashedComp = EnsureComp(leashTarget);
var netLeashTarget = GetNetEntity(leashTarget);
- leashedComp.JointId = $"leash-joint-{netLeashTarget}";
+ var data = new LeashComponent.LeashData(null, netLeashTarget);
+
leashedComp.Puller = leash;
leashedComp.Anchor = anchor;
- // I'd like to use a chain joint or smth, but it's too hard and oftentimes buggy - lamia is a good bad example of that.
- var joint = CreateLeashJoint(leashedComp.JointId, leash, leashTarget);
- var data = new LeashComponent.LeashData(leashedComp.JointId, netLeashTarget);
+ if (CanCreateJoint(leashTarget, leash))
+ {
+ var jointId = $"leash-joint-{netLeashTarget}";
+ var joint = CreateLeashJoint(jointId, leash, leashTarget);
+ data.JointId = leashedComp.JointId = jointId;
+ }
+ else
+ {
+ leashedComp.JointId = null;
+ }
if (leash.Comp.LeashSprite is { } sprite)
{
@@ -411,5 +461,38 @@ public void RemoveLeash(Entity leashed, Entity
+ /// Refreshes all joints for the specified leash.
+ /// This will remove all obsolete joints, such as those for which CanCreateJoint returns false,
+ /// and re-add all joints that were previously removed for the same reason, but became valid later.
+ ///
+ public void RefreshJoints(Entity leash)
+ {
+ foreach (var data in leash.Comp.Leashed)
+ {
+ if (!TryGetEntity(data.Pulled, out var pulled) || !TryComp(pulled, out var leashed))
+ continue;
+
+ var shouldExist = CanCreateJoint(pulled.Value, leash);
+ var exists = data.JointId != null;
+
+ if (exists && !shouldExist && TryComp(pulled, out var jointComp) && jointComp.GetJoints.TryGetValue(data.JointId!, out var joint))
+ {
+ data.JointId = leashed.JointId = null;
+ _joints.RemoveJoint(joint);
+
+ Log.Debug($"Removed obsolete leash joint between {leash.Owner} and {pulled.Value}");
+ }
+ else if (!exists && shouldExist)
+ {
+ var jointId = $"leash-joint-{data.Pulled}";
+ joint = CreateLeashJoint(jointId, leash, pulled.Value);
+ data.JointId = leashed.JointId = jointId;
+
+ Log.Debug($"Added new leash joint between {leash.Owner} and {pulled.Value}");
+ }
+ }
+ }
+
#endregion
}