]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Partial buckling refactor (#29031)
authorLeon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Wed, 19 Jun 2024 15:14:18 +0000 (03:14 +1200)
committerGitHub <noreply@github.com>
Wed, 19 Jun 2024 15:14:18 +0000 (01:14 +1000)
* partial buckling refactor

* git mv test

* change test namespace

* git mv test

* Update test namespace

* Add pulling test

* Network BuckleTime

* Add two more tests

* smelly

38 files changed:
Content.Client/Buckle/BuckleSystem.cs
Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs
Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs
Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/Movement/MovementTest.cs [moved from Content.IntegrationTests/Tests/Interaction/MovementTest.cs with 95% similarity]
Content.IntegrationTests/Tests/Movement/PullingTest.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/Movement/SlippingTest.cs [moved from Content.IntegrationTests/Tests/Slipping/SlippingTest.cs with 91% similarity]
Content.Server/Bed/BedSystem.cs
Content.Server/Bed/Components/HealOnBuckleComponent.cs
Content.Server/Bed/Components/HealOnBuckleHealing.cs
Content.Server/Bed/Components/StasisBedComponent.cs
Content.Server/Body/Systems/BloodstreamSystem.cs
Content.Server/Body/Systems/MetabolizerSystem.cs
Content.Server/Body/Systems/RespiratorSystem.cs
Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs
Content.Shared/Bed/Sleep/SleepingSystem.cs
Content.Shared/Buckle/Components/BuckleComponent.cs
Content.Shared/Buckle/Components/StrapComponent.cs
Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs
Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs [new file with mode: 0644]
Content.Shared/Buckle/SharedBuckleSystem.Strap.cs
Content.Shared/Buckle/SharedBuckleSystem.cs
Content.Shared/Climbing/Systems/ClimbSystem.cs
Content.Shared/Cuffs/SharedCuffableSystem.cs
Content.Shared/Foldable/FoldableSystem.cs
Content.Shared/Interaction/RotateToFaceSystem.cs
Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs
Content.Shared/Movement/Pulling/Events/PullStartedMessage.cs
Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs
Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
Content.Shared/Movement/Systems/SharedWaddleAnimationSystem.cs
Content.Shared/Traits/Assorted/LegsParalyzedSystem.cs
Resources/Prototypes/Entities/Structures/Furniture/chairs.yml
Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml

index fea18e5cf3c11a309877396b08e5956e0b984225..6770899e0aa293fac14fd0f2cb9cc271337d58c1 100644 (file)
@@ -3,6 +3,7 @@ using Content.Shared.Buckle;
 using Content.Shared.Buckle.Components;
 using Content.Shared.Rotation;
 using Robust.Client.GameObjects;
+using Robust.Shared.GameStates;
 
 namespace Content.Client.Buckle;
 
@@ -14,40 +15,63 @@ internal sealed class BuckleSystem : SharedBuckleSystem
     {
         base.Initialize();
 
-        SubscribeLocalEvent<BuckleComponent, AfterAutoHandleStateEvent>(OnBuckleAfterAutoHandleState);
+        SubscribeLocalEvent<BuckleComponent, ComponentHandleState>(OnHandleState);
         SubscribeLocalEvent<BuckleComponent, AppearanceChangeEvent>(OnAppearanceChange);
+        SubscribeLocalEvent<StrapComponent, MoveEvent>(OnStrapMoveEvent);
     }
 
-    private void OnBuckleAfterAutoHandleState(EntityUid uid, BuckleComponent component, ref AfterAutoHandleStateEvent args)
+    private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args)
     {
-        ActionBlocker.UpdateCanMove(uid);
+        // I'm moving this to the client-side system, but for the sake of posterity let's keep this comment:
+        // > This is mega cursed. Please somebody save me from Mr Buckle's wild ride
 
-        if (!TryComp<SpriteComponent>(uid, out var ownerSprite))
+        // The nice thing is its still true, this is quite cursed, though maybe not omega cursed anymore.
+        // This code is garbage, it doesn't work with rotated viewports. I need to finally get around to reworking
+        // sprite rendering for entity layers & direction dependent sorting.
+
+        if (args.NewRotation == args.OldRotation)
             return;
 
-        // Adjust draw depth when the chair faces north so that the seat back is drawn over the player.
-        // Reset the draw depth when rotated in any other direction.
-        // TODO when ECSing, make this a visualizer
-        // This code was written before rotatable viewports were introduced, so hard-coding Direction.North
-        // and comparing it against LocalRotation now breaks this in other rotations. This is a FIXME, but
-        // better to get it working for most people before we look at a more permanent solution.
-        if (component is { Buckled: true, LastEntityBuckledTo: { } } &&
-            Transform(component.LastEntityBuckledTo.Value).LocalRotation.GetCardinalDir() == Direction.North &&
-            TryComp<SpriteComponent>(component.LastEntityBuckledTo, out var buckledSprite))
-        {
-            component.OriginalDrawDepth ??= ownerSprite.DrawDepth;
-            ownerSprite.DrawDepth = buckledSprite.DrawDepth - 1;
+        if (!TryComp<SpriteComponent>(uid, out var strapSprite))
             return;
-        }
 
-        // If here, we're not turning north and should restore the saved draw depth.
-        if (component.OriginalDrawDepth.HasValue)
+        var isNorth = Transform(uid).LocalRotation.GetCardinalDir() == Direction.North;
+        foreach (var buckledEntity in component.BuckledEntities)
         {
-            ownerSprite.DrawDepth = component.OriginalDrawDepth.Value;
-            component.OriginalDrawDepth = null;
+            if (!TryComp<BuckleComponent>(buckledEntity, out var buckle))
+                continue;
+
+            if (!TryComp<SpriteComponent>(buckledEntity, out var buckledSprite))
+                continue;
+
+            if (isNorth)
+            {
+                buckle.OriginalDrawDepth ??= buckledSprite.DrawDepth;
+                buckledSprite.DrawDepth = strapSprite.DrawDepth - 1;
+            }
+            else if (buckle.OriginalDrawDepth.HasValue)
+            {
+                buckledSprite.DrawDepth = buckle.OriginalDrawDepth.Value;
+                buckle.OriginalDrawDepth = null;
+            }
         }
     }
 
+    private void OnHandleState(Entity<BuckleComponent> ent, ref ComponentHandleState args)
+    {
+        if (args.Current is not BuckleState state)
+            return;
+
+        ent.Comp.DontCollide = state.DontCollide;
+        ent.Comp.BuckleTime = state.BuckleTime;
+        var strapUid = EnsureEntity<BuckleComponent>(state.BuckledTo, ent);
+
+        SetBuckledTo(ent, strapUid == null ? null : new (strapUid.Value, null));
+
+        var (uid, component) = ent;
+
+    }
+
     private void OnAppearanceChange(EntityUid uid, BuckleComponent component, ref AppearanceChangeEvent args)
     {
         if (!TryComp<RotationVisualsComponent>(uid, out var rotVisuals))
diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs b/Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs
new file mode 100644 (file)
index 0000000..8df151d
--- /dev/null
@@ -0,0 +1,56 @@
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Buckle;
+using Content.Shared.Buckle.Components;
+using Content.Shared.Input;
+using Content.Shared.Movement.Pulling.Components;
+
+namespace Content.IntegrationTests.Tests.Buckle;
+
+public sealed class BuckleDragTest : InteractionTest
+{
+    // Check that dragging a buckled player unbuckles them.
+    [Test]
+    public async Task BucklePullTest()
+    {
+        var urist = await SpawnTarget("MobHuman");
+        var sUrist = ToServer(urist);
+        await SpawnTarget("Chair");
+
+        var buckle = Comp<BuckleComponent>(urist);
+        var strap = Comp<StrapComponent>(Target);
+        var puller = Comp<PullerComponent>(Player);
+        var pullable = Comp<PullableComponent>(urist);
+
+#pragma warning disable RA0002
+        buckle.Delay = TimeSpan.Zero;
+#pragma warning restore RA0002
+
+        // Initially not buckled to the chair and not pulling anything
+        Assert.That(buckle.Buckled, Is.False);
+        Assert.That(buckle.BuckledTo, Is.Null);
+        Assert.That(strap.BuckledEntities, Is.Empty);
+        Assert.That(puller.Pulling, Is.Null);
+        Assert.That(pullable.Puller, Is.Null);
+        Assert.That(pullable.BeingPulled, Is.False);
+
+        // Strap the human to the chair
+        Assert.That(Server.System<SharedBuckleSystem>().TryBuckle(sUrist, SPlayer, STarget.Value));
+        await RunTicks(5);
+        Assert.That(buckle.Buckled, Is.True);
+        Assert.That(buckle.BuckledTo, Is.EqualTo(STarget));
+        Assert.That(strap.BuckledEntities, Is.EquivalentTo(new[]{sUrist}));
+        Assert.That(puller.Pulling, Is.Null);
+        Assert.That(pullable.Puller, Is.Null);
+        Assert.That(pullable.BeingPulled, Is.False);
+
+        // Start pulling, and thus unbuckle them
+        await PressKey(ContentKeyFunctions.TryPullObject, cursorEntity:urist);
+        await RunTicks(5);
+        Assert.That(buckle.Buckled, Is.False);
+        Assert.That(buckle.BuckledTo, Is.Null);
+        Assert.That(strap.BuckledEntities, Is.Empty);
+        Assert.That(puller.Pulling, Is.EqualTo(sUrist));
+        Assert.That(pullable.Puller, Is.EqualTo(SPlayer));
+        Assert.That(pullable.BeingPulled, Is.True);
+    }
+}
index 57ac63b12476b3f8b60f8a58ff083dbb8075363f..156f42aac333c4a04787e152baf8918a08a11778 100644 (file)
@@ -91,7 +91,6 @@ namespace Content.IntegrationTests.Tests.Buckle
                 {
                     Assert.That(strap, Is.Not.Null);
                     Assert.That(strap.BuckledEntities, Is.Empty);
-                    Assert.That(strap.OccupiedSize, Is.Zero);
                 });
 
                 // Side effects of buckling
@@ -111,8 +110,6 @@ namespace Content.IntegrationTests.Tests.Buckle
 
                     // Side effects of buckling for the strap
                     Assert.That(strap.BuckledEntities, Does.Contain(human));
-                    Assert.That(strap.OccupiedSize, Is.EqualTo(buckle.Size));
-                    Assert.That(strap.OccupiedSize, Is.Positive);
                 });
 
 #pragma warning disable NUnit2045 // Interdependent asserts.
@@ -122,7 +119,7 @@ namespace Content.IntegrationTests.Tests.Buckle
                 // Trying to unbuckle too quickly fails
                 Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False);
                 Assert.That(buckle.Buckled);
-                Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
+                Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False);
                 Assert.That(buckle.Buckled);
 #pragma warning restore NUnit2045
             });
@@ -149,7 +146,6 @@ namespace Content.IntegrationTests.Tests.Buckle
 
                     // Unbuckle, strap
                     Assert.That(strap.BuckledEntities, Is.Empty);
-                    Assert.That(strap.OccupiedSize, Is.Zero);
                 });
 
 #pragma warning disable NUnit2045 // Interdependent asserts.
@@ -160,9 +156,9 @@ namespace Content.IntegrationTests.Tests.Buckle
                 // On cooldown
                 Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False);
                 Assert.That(buckle.Buckled);
-                Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
+                Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False);
                 Assert.That(buckle.Buckled);
-                Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
+                Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False);
                 Assert.That(buckle.Buckled);
 #pragma warning restore NUnit2045
             });
@@ -189,7 +185,6 @@ namespace Content.IntegrationTests.Tests.Buckle
 #pragma warning disable NUnit2045 // Interdependent asserts.
                 Assert.That(buckleSystem.TryBuckle(human, human, chair, buckleComp: buckle), Is.False);
                 Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False);
-                Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
 #pragma warning restore NUnit2045
 
                 // Move near the chair
@@ -202,12 +197,10 @@ namespace Content.IntegrationTests.Tests.Buckle
                 Assert.That(buckle.Buckled);
                 Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False);
                 Assert.That(buckle.Buckled);
-                Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
-                Assert.That(buckle.Buckled);
 #pragma warning restore NUnit2045
 
                 // Force unbuckle
-                Assert.That(buckleSystem.TryUnbuckle(human, human, true, buckleComp: buckle));
+                buckleSystem.Unbuckle(human, human);
                 Assert.Multiple(() =>
                 {
                     Assert.That(buckle.Buckled, Is.False);
@@ -311,7 +304,7 @@ namespace Content.IntegrationTests.Tests.Buckle
                 // Break our guy's kneecaps
                 foreach (var leg in legs)
                 {
-                    xformSystem.DetachParentToNull(leg.Id, entityManager.GetComponent<TransformComponent>(leg.Id));
+                    entityManager.DeleteEntity(leg.Id);
                 }
             });
 
@@ -328,7 +321,8 @@ namespace Content.IntegrationTests.Tests.Buckle
                     Assert.That(hand.HeldEntity, Is.Null);
                 }
 
-                buckleSystem.TryUnbuckle(human, human, true, buckleComp: buckle);
+                buckleSystem.Unbuckle(human, human);
+                Assert.That(buckle.Buckled, Is.False);
             });
 
             await pair.CleanReturnAsync();
index d8d3086520eba81f09247f47eb887a926ded0926..2db0a9acd3d7b5ecba384c166773898f49863747 100644 (file)
@@ -1,5 +1,6 @@
 #nullable enable
 using Content.IntegrationTests.Tests.Interaction;
+using Content.IntegrationTests.Tests.Movement;
 using Robust.Shared.Maths;
 using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent;
 using ClimbSystem = Content.Shared.Climbing.Systems.ClimbSystem;
index 76911eba5f709dbcfe6c6780c5fd349bf732f904..74d0e9242176e389fbdb4d7c1c84b131b1cbcc87 100644 (file)
@@ -59,11 +59,6 @@ public sealed class CraftingTests : InteractionTest
         await AssertEntityLookup((Rod, 2), (Cable, 7), (ShardGlass, 2), (Spear, 1));
     }
 
-    // The following is wrapped in an if DEBUG. This is because of cursed state handling bugs. Tests don't (de)serialize
-    // net messages and just copy objects by reference. This means that the server will directly modify cached server
-    // states on the client's end. Crude fix at the moment is to used modified state handling while in debug mode
-    // Otherwise, this test cannot work.
-#if DEBUG
     /// <summary>
     /// Cancel crafting a complex recipe.
     /// </summary>
@@ -93,28 +88,22 @@ public sealed class CraftingTests : InteractionTest
         await RunTicks(1);
 
         // DoAfter is in progress. Entity not spawned, stacks have been split and someingredients are in a container.
-        Assert.Multiple(async () =>
-        {
-            Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
-            Assert.That(sys.IsEntityInContainer(shard), Is.True);
-            Assert.That(sys.IsEntityInContainer(rods), Is.False);
-            Assert.That(sys.IsEntityInContainer(wires), Is.False);
-            Assert.That(rodStack, Has.Count.EqualTo(8));
-            Assert.That(wireStack, Has.Count.EqualTo(7));
+        Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
+        Assert.That(sys.IsEntityInContainer(shard), Is.True);
+        Assert.That(sys.IsEntityInContainer(rods), Is.False);
+        Assert.That(sys.IsEntityInContainer(wires), Is.False);
+        Assert.That(rodStack, Has.Count.EqualTo(8));
+        Assert.That(wireStack, Has.Count.EqualTo(7));
 
-            await FindEntity(Spear, shouldSucceed: false);
-        });
+        await FindEntity(Spear, shouldSucceed: false);
 
         // Cancel the DoAfter. Should drop ingredients to the floor.
         await CancelDoAfters();
-        Assert.Multiple(async () =>
-        {
-            Assert.That(sys.IsEntityInContainer(rods), Is.False);
-            Assert.That(sys.IsEntityInContainer(wires), Is.False);
-            Assert.That(sys.IsEntityInContainer(shard), Is.False);
-            await FindEntity(Spear, shouldSucceed: false);
-            await AssertEntityLookup((Rod, 10), (Cable, 10), (ShardGlass, 1));
-        });
+        Assert.That(sys.IsEntityInContainer(rods), Is.False);
+        Assert.That(sys.IsEntityInContainer(wires), Is.False);
+        Assert.That(sys.IsEntityInContainer(shard), Is.False);
+        await FindEntity(Spear, shouldSucceed: false);
+        await AssertEntityLookup((Rod, 10), (Cable, 10), (ShardGlass, 1));
 
         // Re-attempt the do-after
 #pragma warning disable CS4014 // Legacy construction code uses DoAfterAwait. See above.
@@ -123,24 +112,17 @@ public sealed class CraftingTests : InteractionTest
         await RunTicks(1);
 
         // DoAfter is in progress. Entity not spawned, ingredients are in a container.
-        Assert.Multiple(async () =>
-        {
-            Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
-            Assert.That(sys.IsEntityInContainer(shard), Is.True);
-            await FindEntity(Spear, shouldSucceed: false);
-        });
+        Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
+        Assert.That(sys.IsEntityInContainer(shard), Is.True);
+        await FindEntity(Spear, shouldSucceed: false);
 
         // Finish the DoAfter
         await AwaitDoAfters();
 
         // Spear has been crafted. Rods and wires are no longer contained. Glass has been consumed.
-        Assert.Multiple(async () =>
-        {
-            await FindEntity(Spear);
-            Assert.That(sys.IsEntityInContainer(rods), Is.False);
-            Assert.That(sys.IsEntityInContainer(wires), Is.False);
-            Assert.That(SEntMan.Deleted(shard));
-        });
+        await FindEntity(Spear);
+        Assert.That(sys.IsEntityInContainer(rods), Is.False);
+        Assert.That(sys.IsEntityInContainer(wires), Is.False);
+        Assert.That(SEntMan.Deleted(shard));
     }
-#endif
 }
index f4826cb2495c4cf8657ef27198c56cb350472a8f..a61a05930177421baf205dbe79fb4398aa688012 100644 (file)
@@ -84,8 +84,9 @@ public abstract partial class InteractionTest
     /// <summary>
     /// Spawn an entity entity and set it as the target.
     /// </summary>
-    [MemberNotNull(nameof(Target))]
-    protected async Task SpawnTarget(string prototype)
+    [MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))]
+#pragma warning disable CS8774 // Member must have a non-null value when exiting.
+    protected async Task<NetEntity> SpawnTarget(string prototype)
     {
         Target = NetEntity.Invalid;
         await Server.WaitPost(() =>
@@ -95,7 +96,9 @@ public abstract partial class InteractionTest
 
         await RunTicks(5);
         AssertPrototype(prototype);
+        return Target!.Value;
     }
+#pragma warning restore CS8774 // Member must have a non-null value when exiting.
 
     /// <summary>
     /// Spawn an entity in preparation for deconstruction
@@ -1170,14 +1173,17 @@ public abstract partial class InteractionTest
 
     #region Inputs
 
+
+
     /// <summary>
     ///     Make the client press and then release a key. This assumes the key is currently released.
+    ///     This will default to using the <see cref="Target"/> entity and <see cref="TargetCoords"/> coordinates.
     /// </summary>
     protected async Task PressKey(
         BoundKeyFunction key,
         int ticks = 1,
         NetCoordinates? coordinates = null,
-        NetEntity cursorEntity = default)
+        NetEntity? cursorEntity = null)
     {
         await SetKey(key, BoundKeyState.Down, coordinates, cursorEntity);
         await RunTicks(ticks);
@@ -1186,15 +1192,17 @@ public abstract partial class InteractionTest
     }
 
     /// <summary>
-    ///     Make the client press or release a key
+    ///     Make the client press or release a key.
+    ///     This will default to using the <see cref="Target"/> entity and <see cref="TargetCoords"/> coordinates.
     /// </summary>
     protected async Task SetKey(
         BoundKeyFunction key,
         BoundKeyState state,
         NetCoordinates? coordinates = null,
-        NetEntity cursorEntity = default)
+        NetEntity? cursorEntity = null)
     {
         var coords = coordinates ?? TargetCoords;
+        var target = cursorEntity ?? Target ?? default;
         ScreenCoordinates screen = default;
 
         var funcId = InputManager.NetworkBindMap.KeyFunctionID(key);
@@ -1203,7 +1211,7 @@ public abstract partial class InteractionTest
             State = state,
             Coordinates = CEntMan.GetCoordinates(coords),
             ScreenCoordinates = screen,
-            Uid = CEntMan.GetEntity(cursorEntity),
+            Uid = CEntMan.GetEntity(target),
         };
 
         await Client.WaitPost(() => InputSystem.HandleInputCommand(ClientSession, key, message));
index 089addfaefd063106b097f0f73a4db35308056bd..37102481ed0e223dda4e65ca93ed3f85b490c33d 100644 (file)
@@ -84,6 +84,7 @@ public abstract partial class InteractionTest
     protected NetEntity? Target;
 
     protected EntityUid? STarget => ToServer(Target);
+
     protected EntityUid? CTarget => ToClient(Target);
 
     /// <summary>
@@ -128,7 +129,6 @@ public abstract partial class InteractionTest
 
     public float TickPeriod => (float) STiming.TickPeriod.TotalSeconds;
 
-
     // Simple mob that has one hand and can perform misc interactions.
     [TestPrototypes]
     private const string TestPrototypes = @"
@@ -142,6 +142,8 @@ public abstract partial class InteractionTest
   - type: ComplexInteraction
   - type: MindContainer
   - type: Stripping
+  - type: Puller
+  - type: Physics
   - type: Tag
     tags:
     - CanPilot
@@ -207,8 +209,8 @@ public abstract partial class InteractionTest
             SEntMan.System<SharedMindSystem>().WipeMind(ServerSession.ContentData()?.Mind);
 
             old = cPlayerMan.LocalEntity;
-            Player = SEntMan.GetNetEntity(SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords)));
-            SPlayer = SEntMan.GetEntity(Player);
+            SPlayer = SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords));
+            Player = SEntMan.GetNetEntity(SPlayer);
             Server.PlayerMan.SetAttachedEntity(ServerSession, SPlayer);
             Hands = SEntMan.GetComponent<HandsComponent>(SPlayer);
             DoAfters = SEntMan.GetComponent<DoAfterComponent>(SPlayer);
diff --git a/Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs b/Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs
new file mode 100644 (file)
index 0000000..8d91855
--- /dev/null
@@ -0,0 +1,63 @@
+using Content.Shared.Alert;
+using Content.Shared.Buckle.Components;
+using Robust.Shared.Maths;
+
+namespace Content.IntegrationTests.Tests.Movement;
+
+public sealed class BuckleMovementTest : MovementTest
+{
+    // Check that interacting with a chair straps you to it and prevents movement.
+    [Test]
+    public async Task ChairTest()
+    {
+        await SpawnTarget("Chair");
+
+        var cAlert = Client.System<AlertsSystem>();
+        var sAlert = Server.System<AlertsSystem>();
+        var buckle = Comp<BuckleComponent>(Player);
+        var strap = Comp<StrapComponent>(Target);
+
+#pragma warning disable RA0002
+        buckle.Delay = TimeSpan.Zero;
+#pragma warning restore RA0002
+
+        // Initially not buckled to the chair, and standing off to the side
+        Assert.That(Delta(), Is.InRange(0.9f, 1.1f));
+        Assert.That(buckle.Buckled, Is.False);
+        Assert.That(buckle.BuckledTo, Is.Null);
+        Assert.That(strap.BuckledEntities, Is.Empty);
+        Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.False);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.False);
+
+        // Interact results in being buckled to the chair
+        await Interact();
+        Assert.That(Delta(), Is.InRange(-0.01f, 0.01f));
+        Assert.That(buckle.Buckled, Is.True);
+        Assert.That(buckle.BuckledTo, Is.EqualTo(STarget));
+        Assert.That(strap.BuckledEntities, Is.EquivalentTo(new[]{SPlayer}));
+        Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.True);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.True);
+
+        // Attempting to walk away does nothing
+        await Move(DirectionFlag.East, 1);
+        Assert.That(Delta(), Is.InRange(-0.01f, 0.01f));
+        Assert.That(buckle.Buckled, Is.True);
+        Assert.That(buckle.BuckledTo, Is.EqualTo(STarget));
+        Assert.That(strap.BuckledEntities, Is.EquivalentTo(new[]{SPlayer}));
+        Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.True);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.True);
+
+        // Interacting again will unbuckle the player
+        await Interact();
+        Assert.That(Delta(), Is.InRange(-0.5f, 0.5f));
+        Assert.That(buckle.Buckled, Is.False);
+        Assert.That(buckle.BuckledTo, Is.Null);
+        Assert.That(strap.BuckledEntities, Is.Empty);
+        Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.False);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.False);
+
+        // And now they can move away
+        await Move(DirectionFlag.SouthEast, 1);
+        Assert.That(Delta(), Is.LessThan(-1));
+    }
+}
similarity index 95%
rename from Content.IntegrationTests/Tests/Interaction/MovementTest.cs
rename to Content.IntegrationTests/Tests/Movement/MovementTest.cs
index dc5aec92cfc71107470d545a50b83ffe0291b49d..ad7b1d0459f434c29f2502de34762af3836bfd2a 100644 (file)
@@ -1,8 +1,9 @@
 #nullable enable
 using System.Numerics;
+using Content.IntegrationTests.Tests.Interaction;
 using Robust.Shared.GameObjects;
 
-namespace Content.IntegrationTests.Tests.Interaction;
+namespace Content.IntegrationTests.Tests.Movement;
 
 /// <summary>
 /// This is a variation of <see cref="InteractionTest"/> that sets up the player with a normal human entity and a simple
diff --git a/Content.IntegrationTests/Tests/Movement/PullingTest.cs b/Content.IntegrationTests/Tests/Movement/PullingTest.cs
new file mode 100644 (file)
index 0000000..d96c4ea
--- /dev/null
@@ -0,0 +1,73 @@
+#nullable enable
+using Content.Shared.Alert;
+using Content.Shared.Input;
+using Content.Shared.Movement.Pulling.Components;
+using Robust.Shared.Maths;
+
+namespace Content.IntegrationTests.Tests.Movement;
+
+public sealed class PullingTest : MovementTest
+{
+    protected override int Tiles => 4;
+
+    [Test]
+    public async Task PullTest()
+    {
+        var cAlert = Client.System<AlertsSystem>();
+        var sAlert = Server.System<AlertsSystem>();
+        await SpawnTarget("MobHuman");
+
+        var puller = Comp<PullerComponent>(Player);
+        var pullable = Comp<PullableComponent>(Target);
+
+        // Player is initially to the left of the target and not pulling anything
+        Assert.That(Delta(), Is.InRange(0.9f, 1.1f));
+        Assert.That(puller.Pulling, Is.Null);
+        Assert.That(pullable.Puller, Is.Null);
+        Assert.That(pullable.BeingPulled, Is.False);
+        Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.False);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.False);
+
+        // Start pulling
+        await PressKey(ContentKeyFunctions.TryPullObject);
+        await RunTicks(5);
+        Assert.That(puller.Pulling, Is.EqualTo(STarget));
+        Assert.That(pullable.Puller, Is.EqualTo(SPlayer));
+        Assert.That(pullable.BeingPulled, Is.True);
+        Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.True);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.True);
+
+        // Move to the left and check that the target moves with the player and is still being pulled.
+        await Move(DirectionFlag.West, 1);
+        Assert.That(Delta(), Is.InRange(0.9f, 1.3f));
+        Assert.That(puller.Pulling, Is.EqualTo(STarget));
+        Assert.That(pullable.Puller, Is.EqualTo(SPlayer));
+        Assert.That(pullable.BeingPulled, Is.True);
+        Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.True);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.True);
+
+        // Move in the other direction
+        await Move(DirectionFlag.East, 2);
+        Assert.That(Delta(), Is.InRange(-1.3f, -0.9f));
+        Assert.That(puller.Pulling, Is.EqualTo(STarget));
+        Assert.That(pullable.Puller, Is.EqualTo(SPlayer));
+        Assert.That(pullable.BeingPulled, Is.True);
+        Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.True);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.True);
+
+        // Stop pulling
+        await PressKey(ContentKeyFunctions.ReleasePulledObject);
+        await RunTicks(5);
+        Assert.That(Delta(), Is.InRange(-1.3f, -0.9f));
+        Assert.That(puller.Pulling, Is.Null);
+        Assert.That(pullable.Puller, Is.Null);
+        Assert.That(pullable.BeingPulled, Is.False);
+        Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.False);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.False);
+
+        // Move back to the left and ensure the target is no longer following us.
+        await Move(DirectionFlag.West, 2);
+        Assert.That(Delta(), Is.GreaterThan(2f));
+    }
+}
+
similarity index 91%
rename from Content.IntegrationTests/Tests/Slipping/SlippingTest.cs
rename to Content.IntegrationTests/Tests/Movement/SlippingTest.cs
index 7f77146f4552a03a9a3118bc27681569da34dc8f..7ee895d7c278082f81dfd67ec5be36cb070f8a92 100644 (file)
@@ -8,7 +8,7 @@ using Robust.Shared.GameObjects;
 using Robust.Shared.Input;
 using Robust.Shared.Maths;
 
-namespace Content.IntegrationTests.Tests.Slipping;
+namespace Content.IntegrationTests.Tests.Movement;
 
 public sealed class SlippingTest : MovementTest
 {
@@ -36,18 +36,14 @@ public sealed class SlippingTest : MovementTest
         Assert.That(modifier, Is.EqualTo(1), "Player is not moving at full speed.");
 
         // Player is to the left of the banana peel and has not slipped.
-#pragma warning disable NUnit2045
         Assert.That(Delta(), Is.GreaterThan(0.5f));
         Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player)));
-#pragma warning restore NUnit2045
 
         // Walking over the banana slowly does not trigger a slip.
         await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Down);
         await Move(DirectionFlag.East, 1f);
-#pragma warning disable NUnit2045
         Assert.That(Delta(), Is.LessThan(0.5f));
         Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player)));
-#pragma warning restore NUnit2045
         AssertComp<KnockedDownComponent>(false, Player);
 
         // Moving at normal speeds does trigger a slip.
index ee43cff26de62173baf2fef006276c47a138adb3..a6b61da591f34907928131da9e23ee8fc0b4ff1d 100644 (file)
@@ -11,6 +11,7 @@ using Content.Shared.Damage;
 using Content.Shared.Emag.Systems;
 using Content.Shared.Mobs.Systems;
 using Robust.Shared.Timing;
+using Robust.Shared.Utility;
 
 namespace Content.Server.Bed
 {
@@ -26,25 +27,29 @@ namespace Content.Server.Bed
         public override void Initialize()
         {
             base.Initialize();
-            SubscribeLocalEvent<HealOnBuckleComponent, BuckleChangeEvent>(ManageUpdateList);
-            SubscribeLocalEvent<StasisBedComponent, BuckleChangeEvent>(OnBuckleChange);
+            SubscribeLocalEvent<HealOnBuckleComponent, StrappedEvent>(OnStrapped);
+            SubscribeLocalEvent<HealOnBuckleComponent, UnstrappedEvent>(OnUnstrapped);
+            SubscribeLocalEvent<StasisBedComponent, StrappedEvent>(OnStasisStrapped);
+            SubscribeLocalEvent<StasisBedComponent, UnstrappedEvent>(OnStasisUnstrapped);
             SubscribeLocalEvent<StasisBedComponent, PowerChangedEvent>(OnPowerChanged);
             SubscribeLocalEvent<StasisBedComponent, GotEmaggedEvent>(OnEmagged);
         }
 
-        private void ManageUpdateList(EntityUid uid, HealOnBuckleComponent component, ref BuckleChangeEvent args)
+        private void OnStrapped(Entity<HealOnBuckleComponent> bed, ref StrappedEvent args)
         {
-            if (args.Buckling)
-            {
-                AddComp<HealOnBuckleHealingComponent>(uid);
-                component.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(component.HealTime);
-                _actionsSystem.AddAction(args.BuckledEntity, ref component.SleepAction, SleepingSystem.SleepActionId, uid);
-                return;
-            }
+            EnsureComp<HealOnBuckleHealingComponent>(bed);
+            bed.Comp.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(bed.Comp.HealTime);
+            _actionsSystem.AddAction(args.Buckle, ref bed.Comp.SleepAction, SleepingSystem.SleepActionId, bed);
 
-            _actionsSystem.RemoveAction(args.BuckledEntity, component.SleepAction);
-            _sleepingSystem.TryWaking(args.BuckledEntity);
-            RemComp<HealOnBuckleHealingComponent>(uid);
+            // Single action entity, cannot strap multiple entities to the same bed.
+            DebugTools.AssertEqual(args.Strap.Comp.BuckledEntities.Count, 1);
+        }
+
+        private void OnUnstrapped(Entity<HealOnBuckleComponent> bed, ref UnstrappedEvent args)
+        {
+            _actionsSystem.RemoveAction(args.Buckle, bed.Comp.SleepAction);
+            _sleepingSystem.TryWaking(args.Buckle.Owner);
+            RemComp<HealOnBuckleHealingComponent>(bed);
         }
 
         public override void Update(float frameTime)
@@ -82,18 +87,22 @@ namespace Content.Server.Bed
             _appearance.SetData(uid, StasisBedVisuals.IsOn, isOn);
         }
 
-        private void OnBuckleChange(EntityUid uid, StasisBedComponent component, ref BuckleChangeEvent args)
+        private void OnStasisStrapped(Entity<StasisBedComponent> bed, ref StrappedEvent args)
         {
-            // In testing this also received an unbuckle event when the bed is destroyed
-            // So don't worry about that
-            if (!HasComp<BodyComponent>(args.BuckledEntity))
+            if (!HasComp<BodyComponent>(args.Buckle) || !this.IsPowered(bed, EntityManager))
                 return;
 
-            if (!this.IsPowered(uid, EntityManager))
+            var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, true);
+            RaiseLocalEvent(args.Buckle, ref metabolicEvent);
+        }
+
+        private void OnStasisUnstrapped(Entity<StasisBedComponent> bed, ref UnstrappedEvent args)
+        {
+            if (!HasComp<BodyComponent>(args.Buckle) || !this.IsPowered(bed, EntityManager))
                 return;
 
-            var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.BuckledEntity, component.Multiplier, args.Buckling);
-            RaiseLocalEvent(args.BuckledEntity, ref metabolicEvent);
+            var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, false);
+            RaiseLocalEvent(args.Buckle, ref metabolicEvent);
         }
 
         private void OnPowerChanged(EntityUid uid, StasisBedComponent component, ref PowerChangedEvent args)
index f29fe30429f9151566bbcc7e5acf8cd1b0db1d68..3c6f3a4382bce7679ecaf019a17a1d073b92d55b 100644 (file)
@@ -5,19 +5,26 @@ namespace Content.Server.Bed.Components
     [RegisterComponent]
     public sealed partial class HealOnBuckleComponent : Component
     {
-        [DataField("damage", required: true)]
-        [ViewVariables(VVAccess.ReadWrite)]
+        /// <summary>
+        /// Damage to apply to entities that are strapped to this entity.
+        /// </summary>
+        [DataField(required: true)]
         public DamageSpecifier Damage = default!;
 
-        [DataField("healTime", required: false)]
-        [ViewVariables(VVAccess.ReadWrite)]
-        public float HealTime = 1f; // How often the bed applies the damage
+        /// <summary>
+        /// How frequently the damage should be applied, in seconds.
+        /// </summary>
+        [DataField(required: false)]
+        public float HealTime = 1f;
 
-        [DataField("sleepMultiplier")]
+        /// <summary>
+        /// Damage multiplier that gets applied if the entity is sleeping.
+        /// </summary>
+        [DataField]
         public float SleepMultiplier = 3f;
 
         public TimeSpan NextHealTime = TimeSpan.Zero; //Next heal
 
-        [DataField("sleepAction")] public EntityUid? SleepAction;
+        [DataField] public EntityUid? SleepAction;
     }
 }
index a944e67e12d701a8971956e1f62580164ceeb726..aaa82c737c52f4065b68de0fdbdadafab5d4fa1c 100644 (file)
@@ -1,5 +1,6 @@
 namespace Content.Server.Bed.Components
 {
+    // TODO rename this component
     [RegisterComponent]
     public sealed partial class HealOnBuckleHealingComponent : Component
     {}
index e2175d6e6467e374bf119b8015771307d8475ae1..160bd0ac67885dd0ab05a980031634739012a80c 100644 (file)
@@ -6,7 +6,8 @@ namespace Content.Server.Bed.Components
         /// <summary>
         /// What the metabolic update rate will be multiplied by (higher = slower metabolism)
         /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
+        [ViewVariables(VVAccess.ReadOnly)] // Writing is is not supported. ApplyMetabolicMultiplierEvent needs to be refactored first
+        [DataField]
         public float Multiplier = 10f;
     }
 }
index f961307fc6a079911d80be664e27c89875e336a2..eaa7b62f25ae9e40683d5090d69c898ef28ffa4a 100644 (file)
@@ -268,6 +268,9 @@ public sealed class BloodstreamSystem : EntitySystem
         Entity<BloodstreamComponent> ent,
         ref ApplyMetabolicMultiplierEvent args)
     {
+        // TODO REFACTOR THIS
+        // This will slowly drift over time due to floating point errors.
+        // Instead, raise an event with the base rates and allow modifiers to get applied to it.
         if (args.Apply)
         {
             ent.Comp.UpdateInterval *= args.Multiplier;
index 45cba5a195f234768e0cba36fef96a2aeb073e8a..8394d9999bcf162b5d9c2f7b9a23bfe8c1f77cad 100644 (file)
@@ -67,6 +67,9 @@ namespace Content.Server.Body.Systems
             Entity<MetabolizerComponent> ent,
             ref ApplyMetabolicMultiplierEvent args)
         {
+            // TODO REFACTOR THIS
+            // This will slowly drift over time due to floating point errors.
+            // Instead, raise an event with the base rates and allow modifiers to get applied to it.
             if (args.Apply)
             {
                 ent.Comp.UpdateInterval *= args.Multiplier;
@@ -229,6 +232,9 @@ namespace Content.Server.Body.Systems
         }
     }
 
+    // TODO REFACTOR THIS
+    // This will cause rates to slowly drift over time due to floating point errors.
+    // Instead, the system that raised this should trigger an update and subscribe to get-modifier events.
     [ByRefEvent]
     public readonly record struct ApplyMetabolicMultiplierEvent(
         EntityUid Uid,
index 900920a14fe3ee51faa94c1f5bf53cd5980bd5b6..5461f68db2f85f8e62debc683b3a301925fad9cb 100644 (file)
@@ -326,6 +326,9 @@ public sealed class RespiratorSystem : EntitySystem
         Entity<RespiratorComponent> ent,
         ref ApplyMetabolicMultiplierEvent args)
     {
+        // TODO REFACTOR THIS
+        // This will slowly drift over time due to floating point errors.
+        // Instead, raise an event with the base rates and allow modifiers to get applied to it.
         if (args.Apply)
         {
             ent.Comp.UpdateInterval *= args.Multiplier;
index 207665d786f015e7c41a94621f7739fe61a5b910..116e8fe7c7f992f020a90442a6d345f64d694d66 100644 (file)
@@ -1,11 +1,9 @@
 using Content.Server.Buckle.Systems;
-using Content.Shared.Buckle.Components;
 
 namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
 
 public sealed partial class UnbuckleOperator : HTNOperator
 {
-    [Dependency] private readonly IEntityManager _entManager = default!;
     private BuckleSystem _buckle = default!;
 
     [DataField("shutdownState")]
@@ -21,10 +19,7 @@ public sealed partial class UnbuckleOperator : HTNOperator
     {
         base.Startup(blackboard);
         var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
-        if (!_entManager.TryGetComponent<BuckleComponent>(owner, out var buckle) || !buckle.Buckled)
-            return;
-
-        _buckle.TryUnbuckle(owner, owner, true, buckle);
+        _buckle.Unbuckle(owner, null);
     }
 
     public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
index 6008e301cfeef9efd14f07b8b27f58a5f0eb0e9b..581924c053bef439307001987c943c92b037a01b 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Actions;
+using Content.Shared.Buckle.Components;
 using Content.Shared.Damage;
 using Content.Shared.Damage.ForceSay;
 using Content.Shared.Examine;
@@ -59,6 +60,15 @@ public sealed partial class SleepingSystem : EntitySystem
         SubscribeLocalEvent<SleepingComponent, InteractHandEvent>(OnInteractHand);
 
         SubscribeLocalEvent<ForcedSleepingComponent, ComponentInit>(OnInit);
+        SubscribeLocalEvent<SleepingComponent, UnbuckleAttemptEvent>(OnUnbuckleAttempt);
+    }
+
+    private void OnUnbuckleAttempt(Entity<SleepingComponent> ent, ref UnbuckleAttemptEvent args)
+    {
+        // TODO is this necessary?
+        // Shouldn't the interaction have already been blocked by a general interaction check?
+        if (ent.Owner == args.User)
+            args.Cancelled = true;
     }
 
     private void OnBedSleepAction(Entity<ActionsContainerComponent> ent, ref SleepActionEvent args)
index cf28b56d51f3319b9bbfd782cce20916cdd6696a..ee86e6d4de0f4e51d031c33bc0de4f0bf443e515 100644 (file)
@@ -1,10 +1,15 @@
+using System.Diagnostics.CodeAnalysis;
 using Content.Shared.Interaction;
 using Robust.Shared.GameStates;
 using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Shared.Buckle.Components;
 
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
+/// <summary>
+/// This component allows an entity to be buckled to an entity with a <see cref="StrapComponent"/>.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
 [Access(typeof(SharedBuckleSystem))]
 public sealed partial class BuckleComponent : Component
 {
@@ -14,31 +19,23 @@ public sealed partial class BuckleComponent : Component
     /// across a table two tiles away" problem.
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public float Range = SharedInteractionSystem.InteractionRange / 1.4f;
 
     /// <summary>
     /// True if the entity is buckled, false otherwise.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [AutoNetworkedField]
-    public bool Buckled;
-
-    [ViewVariables]
-    [AutoNetworkedField]
-    public EntityUid? LastEntityBuckledTo;
+    [MemberNotNullWhen(true, nameof(BuckledTo))]
+    public bool Buckled => BuckledTo != null;
 
     /// <summary>
     /// Whether or not collisions should be possible with the entity we are strapped to
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField, AutoNetworkedField]
+    [DataField]
     public bool DontCollide;
 
     /// <summary>
     /// Whether or not we should be allowed to pull the entity we are strapped to
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
     [DataField]
     public bool PullStrap;
 
@@ -47,20 +44,18 @@ public sealed partial class BuckleComponent : Component
     /// be able to unbuckle after recently buckling.
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public TimeSpan Delay = TimeSpan.FromSeconds(0.25f);
 
     /// <summary>
     /// The time that this entity buckled at.
     /// </summary>
-    [ViewVariables]
-    public TimeSpan BuckleTime;
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+    public TimeSpan? BuckleTime;
 
     /// <summary>
     /// The strap that this component is buckled to.
     /// </summary>
-    [ViewVariables]
-    [AutoNetworkedField]
+    [DataField]
     public EntityUid? BuckledTo;
 
     /// <summary>
@@ -68,7 +63,6 @@ public sealed partial class BuckleComponent : Component
     /// <see cref="StrapComponent"/>.
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public int Size = 100;
 
     /// <summary>
@@ -77,11 +71,90 @@ public sealed partial class BuckleComponent : Component
     [ViewVariables] public int? OriginalDrawDepth;
 }
 
+[Serializable, NetSerializable]
+public sealed class BuckleState(NetEntity? buckledTo, bool dontCollide, TimeSpan? buckleTime) : ComponentState
+{
+    public readonly NetEntity? BuckledTo = buckledTo;
+    public readonly bool DontCollide = dontCollide;
+    public readonly TimeSpan? BuckleTime = buckleTime;
+}
+
+
+/// <summary>
+/// Event raised directed at a strap entity before some entity gets buckled to it.
+/// </summary>
+[ByRefEvent]
+public record struct StrapAttemptEvent(
+    Entity<StrapComponent> Strap,
+    Entity<BuckleComponent> Buckle,
+    EntityUid? User,
+    bool Popup)
+{
+    public bool Cancelled;
+}
+
+/// <summary>
+/// Event raised directed at a buckle entity before it gets buckled to some strap entity.
+/// </summary>
+[ByRefEvent]
+public record struct BuckleAttemptEvent(
+    Entity<StrapComponent> Strap,
+    Entity<BuckleComponent> Buckle,
+    EntityUid? User,
+    bool Popup)
+{
+    public bool Cancelled;
+}
+
+/// <summary>
+/// Event raised directed at a strap entity before some entity gets unbuckled from it.
+/// </summary>
+[ByRefEvent]
+public record struct UnstrapAttemptEvent(
+    Entity<StrapComponent> Strap,
+    Entity<BuckleComponent> Buckle,
+    EntityUid? User,
+    bool Popup)
+{
+    public bool Cancelled;
+}
+
+/// <summary>
+/// Event raised directed at a buckle entity before it gets unbuckled.
+/// </summary>
+[ByRefEvent]
+public record struct UnbuckleAttemptEvent(
+    Entity<StrapComponent> Strap,
+    Entity<BuckleComponent> Buckle,
+    EntityUid? User,
+    bool Popup)
+{
+    public bool Cancelled;
+}
+
+/// <summary>
+/// Event raised directed at a strap entity after something has been buckled to it.
+/// </summary>
+[ByRefEvent]
+public readonly record struct StrappedEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
+
+/// <summary>
+/// Event raised directed at a buckle entity after it has been buckled.
+/// </summary>
+[ByRefEvent]
+public readonly record struct BuckledEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
+
+/// <summary>
+/// Event raised directed at a strap entity after something has been unbuckled from it.
+/// </summary>
 [ByRefEvent]
-public record struct BuckleAttemptEvent(EntityUid StrapEntity, EntityUid BuckledEntity, EntityUid UserEntity, bool Buckling, bool Cancelled = false);
+public readonly record struct UnstrappedEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
 
+/// <summary>
+/// Event raised directed at a buckle entity after it has been unbuckled from some strap entity.
+/// </summary>
 [ByRefEvent]
-public readonly record struct BuckleChangeEvent(EntityUid StrapEntity, EntityUid BuckledEntity, bool Buckling);
+public readonly record struct UnbuckledEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
 
 [Serializable, NetSerializable]
 public enum BuckleVisuals
index 9a19cea0c9a7e351d2222e062824533e71f6bbb0..a16d15f8a2c07375cd02b6dc3c86624ef8406d28 100644 (file)
@@ -13,117 +13,77 @@ namespace Content.Shared.Buckle.Components;
 public sealed partial class StrapComponent : Component
 {
     /// <summary>
-    /// The entities that are currently buckled
+    /// The entities that are currently buckled to this strap.
     /// </summary>
-    [AutoNetworkedField]
-    [ViewVariables] // TODO serialization
+    [ViewVariables]
     public HashSet<EntityUid> BuckledEntities = new();
 
     /// <summary>
     /// Entities that this strap accepts and can buckle
     /// If null it accepts any entity
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public EntityWhitelist? Whitelist;
 
     /// <summary>
     /// Entities that this strap does not accept and cannot buckle.
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public EntityWhitelist? Blacklist;
 
     /// <summary>
     /// The change in position to the strapped mob
     /// </summary>
     [DataField, AutoNetworkedField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public StrapPosition Position = StrapPosition.None;
 
-    /// <summary>
-    /// The distance above which a buckled entity will be automatically unbuckled.
-    /// Don't change it unless you really have to
-    /// </summary>
-    /// <remarks>
-    /// Dont set this below 0.2 because that causes audio issues with <see cref="SharedBuckleSystem.OnBuckleMove"/>
-    /// My guess after testing is that the client sets BuckledTo to the strap in *some* ticks for some reason
-    /// whereas the server doesnt, thus the client tries to unbuckle like 15 times because it passes the strap null check
-    /// This is why this needs to be above 0.1 to make the InRange check fail in both client and server.
-    /// </remarks>
-    [DataField, AutoNetworkedField]
-    [ViewVariables(VVAccess.ReadWrite)]
-    public float MaxBuckleDistance = 0.2f;
-
-    /// <summary>
-    /// Gets and clamps the buckle offset to MaxBuckleDistance
-    /// </summary>
-    [ViewVariables]
-    public Vector2 BuckleOffsetClamped => Vector2.Clamp(
-        BuckleOffset,
-        Vector2.One * -MaxBuckleDistance,
-        Vector2.One * MaxBuckleDistance);
-
     /// <summary>
     /// The buckled entity will be offset by this amount from the center of the strap object.
-    /// If this offset it too big, it will be clamped to <see cref="MaxBuckleDistance"/>
     /// </summary>
     [DataField, AutoNetworkedField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public Vector2 BuckleOffset = Vector2.Zero;
 
     /// <summary>
     /// The angle to rotate the player by when they get strapped
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public Angle Rotation;
 
     /// <summary>
     /// The size of the strap which is compared against when buckling entities
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public int Size = 100;
 
     /// <summary>
     /// If disabled, nothing can be buckled on this object, and it will unbuckle anything that's already buckled
     /// </summary>
-    [ViewVariables]
+    [DataField, AutoNetworkedField]
     public bool Enabled = true;
 
     /// <summary>
     /// You can specify the offset the entity will have after unbuckling.
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public Vector2 UnbuckleOffset = Vector2.Zero;
 
     /// <summary>
     /// The sound to be played when a mob is buckled
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public SoundSpecifier BuckleSound  = new SoundPathSpecifier("/Audio/Effects/buckle.ogg");
 
     /// <summary>
     /// The sound to be played when a mob is unbuckled
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public SoundSpecifier UnbuckleSound = new SoundPathSpecifier("/Audio/Effects/unbuckle.ogg");
 
     /// <summary>
     /// ID of the alert to show when buckled
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public ProtoId<AlertPrototype> BuckledAlertType = "Buckled";
-
-    /// <summary>
-    /// The sum of the sizes of all the buckled entities in this strap
-    /// </summary>
-    [AutoNetworkedField]
-    [ViewVariables]
-    public int OccupiedSize;
 }
 
 public enum StrapPosition
index e7bfb53f08c767772259e61562199ac6504b135d..fd6ffd30e39c6036ce4cdd878dc53575bdaee956 100644 (file)
@@ -1,39 +1,46 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Numerics;
 using Content.Shared.Alert;
-using Content.Shared.Bed.Sleep;
 using Content.Shared.Buckle.Components;
 using Content.Shared.Database;
 using Content.Shared.Hands.Components;
 using Content.Shared.IdentityManagement;
-using Content.Shared.Interaction;
-using Content.Shared.Mobs.Components;
 using Content.Shared.Movement.Events;
+using Content.Shared.Movement.Pulling.Events;
 using Content.Shared.Popups;
+using Content.Shared.Pulling.Events;
 using Content.Shared.Standing;
 using Content.Shared.Storage.Components;
 using Content.Shared.Stunnable;
 using Content.Shared.Throwing;
-using Content.Shared.Verbs;
 using Content.Shared.Whitelist;
+using Robust.Shared.Containers;
+using Robust.Shared.GameStates;
+using Robust.Shared.Map;
 using Robust.Shared.Physics.Components;
 using Robust.Shared.Physics.Events;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Utility;
-using PullableComponent = Content.Shared.Movement.Pulling.Components.PullableComponent;
 
 namespace Content.Shared.Buckle;
 
 public abstract partial class SharedBuckleSystem
 {
+    public static ProtoId<AlertCategoryPrototype> BuckledAlertCategory = "Buckled";
+
     [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
 
     private void InitializeBuckle()
     {
-        SubscribeLocalEvent<BuckleComponent, ComponentStartup>(OnBuckleComponentStartup);
         SubscribeLocalEvent<BuckleComponent, ComponentShutdown>(OnBuckleComponentShutdown);
         SubscribeLocalEvent<BuckleComponent, MoveEvent>(OnBuckleMove);
-        SubscribeLocalEvent<BuckleComponent, InteractHandEvent>(OnBuckleInteractHand);
-        SubscribeLocalEvent<BuckleComponent, GetVerbsEvent<InteractionVerb>>(AddUnbuckleVerb);
+        SubscribeLocalEvent<BuckleComponent, EntParentChangedMessage>(OnParentChanged);
+        SubscribeLocalEvent<BuckleComponent, EntGotInsertedIntoContainerMessage>(OnInserted);
+
+        SubscribeLocalEvent<BuckleComponent, StartPullAttemptEvent>(OnPullAttempt);
+        SubscribeLocalEvent<BuckleComponent, BeingPulledAttemptEvent>(OnBeingPulledAttempt);
+        SubscribeLocalEvent<BuckleComponent, PullStartedMessage>(OnPullStarted);
+
         SubscribeLocalEvent<BuckleComponent, InsertIntoEntityStorageAttemptEvent>(OnBuckleInsertIntoEntityStorageAttempt);
 
         SubscribeLocalEvent<BuckleComponent, PreventCollideEvent>(OnBucklePreventCollide);
@@ -41,69 +48,93 @@ public abstract partial class SharedBuckleSystem
         SubscribeLocalEvent<BuckleComponent, StandAttemptEvent>(OnBuckleStandAttempt);
         SubscribeLocalEvent<BuckleComponent, ThrowPushbackAttemptEvent>(OnBuckleThrowPushbackAttempt);
         SubscribeLocalEvent<BuckleComponent, UpdateCanMoveEvent>(OnBuckleUpdateCanMove);
-    }
 
-    [ValidatePrototypeId<AlertCategoryPrototype>]
-    public const string BuckledAlertCategory = "Buckled";
+        SubscribeLocalEvent<BuckleComponent, ComponentGetState>(OnGetState);
+    }
 
-    private void OnBuckleComponentStartup(EntityUid uid, BuckleComponent component, ComponentStartup args)
+    private void OnGetState(Entity<BuckleComponent> ent, ref ComponentGetState args)
     {
-        UpdateBuckleStatus(uid, component);
+        args.State = new BuckleState(GetNetEntity(ent.Comp.BuckledTo), ent.Comp.DontCollide, ent.Comp.BuckleTime);
     }
 
-    private void OnBuckleComponentShutdown(EntityUid uid, BuckleComponent component, ComponentShutdown args)
+    private void OnBuckleComponentShutdown(Entity<BuckleComponent> ent, ref ComponentShutdown args)
     {
-        TryUnbuckle(uid, uid, true, component);
+        Unbuckle(ent!, null);
+    }
+
+    #region Pulling
 
-        component.BuckleTime = default;
+    private void OnPullAttempt(Entity<BuckleComponent> ent, ref StartPullAttemptEvent args)
+    {
+        // Prevent people pulling the chair they're on, etc.
+        if (ent.Comp.BuckledTo == args.Pulled && !ent.Comp.PullStrap)
+            args.Cancel();
     }
 
-    private void OnBuckleMove(EntityUid uid, BuckleComponent component, ref MoveEvent ev)
+    private void OnBeingPulledAttempt(Entity<BuckleComponent> ent, ref BeingPulledAttemptEvent args)
     {
-        if (component.BuckledTo is not { } strapUid)
+        if (args.Cancelled || !ent.Comp.Buckled)
             return;
 
-        if (!TryComp<StrapComponent>(strapUid, out var strapComp))
-            return;
+        if (!CanUnbuckle(ent!, args.Puller, false))
+            args.Cancel();
+    }
 
-        var strapPosition = Transform(strapUid).Coordinates;
-        if (ev.NewPosition.EntityId.IsValid() && ev.NewPosition.InRange(EntityManager, _transform, strapPosition, strapComp.MaxBuckleDistance))
-            return;
+    private void OnPullStarted(Entity<BuckleComponent> ent, ref PullStartedMessage args)
+    {
+        Unbuckle(ent!, args.PullerUid);
+    }
+
+    #endregion
 
-        TryUnbuckle(uid, uid, true, component);
+    #region Transform
+
+    private void OnParentChanged(Entity<BuckleComponent> ent, ref EntParentChangedMessage args)
+    {
+        BuckleTransformCheck(ent, args.Transform);
     }
 
-    private void OnBuckleInteractHand(EntityUid uid, BuckleComponent component, InteractHandEvent args)
+    private void OnInserted(Entity<BuckleComponent> ent, ref EntGotInsertedIntoContainerMessage args)
     {
-        if (!component.Buckled)
-            return;
+        BuckleTransformCheck(ent, Transform(ent));
+    }
 
-        if (TryUnbuckle(uid, args.User, buckleComp: component))
-            args.Handled = true;
+    private void OnBuckleMove(Entity<BuckleComponent> ent, ref MoveEvent ev)
+    {
+        BuckleTransformCheck(ent, ev.Component);
     }
 
-    private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent<InteractionVerb> args)
+    /// <summary>
+    /// Check if the entity should get unbuckled as a result of transform or container changes.
+    /// </summary>
+    private void BuckleTransformCheck(Entity<BuckleComponent> buckle, TransformComponent xform)
     {
-        if (!args.CanAccess || !args.CanInteract || !component.Buckled)
+        if (_gameTiming.ApplyingState)
             return;
 
-        InteractionVerb verb = new()
+        if (buckle.Comp.BuckledTo is not { } strapUid)
+            return;
+
+        if (!TryComp<StrapComponent>(strapUid, out var strapComp))
         {
-            Act = () => TryUnbuckle(uid, args.User, buckleComp: component),
-            Text = Loc.GetString("verb-categories-unbuckle"),
-            Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/unbuckle.svg.192dpi.png"))
-        };
+            Log.Error($"Encountered buckle entity {ToPrettyString(buckle)} without a valid strap entity {ToPrettyString(strapUid)}");
+            SetBuckledTo(buckle, null);
+            return;
+        }
 
-        if (args.Target == args.User && args.Using == null)
+        if (xform.ParentUid != strapUid || _container.IsEntityInContainer(buckle))
         {
-            // A user is left clicking themselves with an empty hand, while buckled.
-            // It is very likely they are trying to unbuckle themselves.
-            verb.Priority = 1;
+            Unbuckle(buckle, (strapUid, strapComp), null);
+            return;
         }
 
-        args.Verbs.Add(verb);
+        var delta = (xform.LocalPosition - strapComp.BuckleOffset).LengthSquared();
+        if (delta > 1e-5)
+            Unbuckle(buckle, (strapUid, strapComp), null);
     }
 
+    #endregion
+
     private void OnBuckleInsertIntoEntityStorageAttempt(EntityUid uid, BuckleComponent component, ref InsertIntoEntityStorageAttemptEvent args)
     {
         if (component.Buckled)
@@ -112,10 +143,7 @@ public abstract partial class SharedBuckleSystem
 
     private void OnBucklePreventCollide(EntityUid uid, BuckleComponent component, ref PreventCollideEvent args)
     {
-        if (args.OtherEntity != component.BuckledTo)
-            return;
-
-        if (component.Buckled || component.DontCollide)
+        if (args.OtherEntity == component.BuckledTo && component.DontCollide)
             args.Cancelled = true;
     }
 
@@ -139,10 +167,7 @@ public abstract partial class SharedBuckleSystem
 
     private void OnBuckleUpdateCanMove(EntityUid uid, BuckleComponent component, UpdateCanMoveEvent args)
     {
-        if (component.LifeStage > ComponentLifeStage.Running)
-            return;
-
-        if (component.Buckled) // buckle shitcode
+        if (component.Buckled)
             args.Cancel();
     }
 
@@ -151,162 +176,139 @@ public abstract partial class SharedBuckleSystem
         return Resolve(uid, ref component, false) && component.Buckled;
     }
 
-    /// <summary>
-    /// Shows or hides the buckled status effect depending on if the
-    /// entity is buckled or not.
-    /// </summary>
-    /// <param name="uid"> Entity that we want to show the alert </param>
-    /// <param name="buckleComp"> buckle component of the entity </param>
-    /// <param name="strapComp"> strap component of the thing we are strapping to </param>
-    private void UpdateBuckleStatus(EntityUid uid, BuckleComponent buckleComp, StrapComponent? strapComp = null)
+    protected void SetBuckledTo(Entity<BuckleComponent> buckle, Entity<StrapComponent?>? strap)
     {
-        Appearance.SetData(uid, StrapVisuals.State, buckleComp.Buckled);
-        if (buckleComp.BuckledTo != null)
-        {
-            if (!Resolve(buckleComp.BuckledTo.Value, ref strapComp))
-                return;
+        if (TryComp(buckle.Comp.BuckledTo, out StrapComponent? old))
+            old.BuckledEntities.Remove(buckle);
 
-            var alertType = strapComp.BuckledAlertType;
-            _alerts.ShowAlert(uid, alertType);
-        }
-        else
+        if (strap is {} strapEnt && Resolve(strapEnt.Owner, ref strapEnt.Comp))
         {
-            _alerts.ClearAlertCategory(uid, BuckledAlertCategory);
-        }
-    }
-
-    /// <summary>
-    /// Sets the <see cref="BuckleComponent.BuckledTo"/> field in the component to a value
-    /// </summary>
-    /// <param name="strapUid"> Value tat with be assigned to the field </param>
-    private void SetBuckledTo(EntityUid buckleUid, EntityUid? strapUid, StrapComponent? strapComp, BuckleComponent buckleComp)
-    {
-        buckleComp.BuckledTo = strapUid;
-
-        if (strapUid == null)
-        {
-            buckleComp.Buckled = false;
+            strapEnt.Comp.BuckledEntities.Add(buckle);
+            _alerts.ShowAlert(buckle, strapEnt.Comp.BuckledAlertType);
         }
         else
         {
-            buckleComp.LastEntityBuckledTo = strapUid;
-            buckleComp.DontCollide = true;
-            buckleComp.Buckled = true;
-            buckleComp.BuckleTime = _gameTiming.CurTime;
+            _alerts.ClearAlertCategory(buckle, BuckledAlertCategory);
         }
 
-        ActionBlocker.UpdateCanMove(buckleUid);
-        UpdateBuckleStatus(buckleUid, buckleComp, strapComp);
-        Dirty(buckleUid, buckleComp);
+        buckle.Comp.BuckledTo = strap;
+        buckle.Comp.BuckleTime = _gameTiming.CurTime;
+        ActionBlocker.UpdateCanMove(buckle);
+        Appearance.SetData(buckle, StrapVisuals.State, buckle.Comp.Buckled);
+        Dirty(buckle);
     }
 
     /// <summary>
     /// Checks whether or not buckling is possible
     /// </summary>
     /// <param name="buckleUid"> Uid of the owner of BuckleComponent </param>
-    /// <param name="userUid">
-    /// Uid of a third party entity,
-    /// i.e, the uid of someone else you are dragging to a chair.
-    /// Can equal buckleUid sometimes
+    /// <param name="user">
+    ///     Uid of a third party entity,
+    ///     i.e, the uid of someone else you are dragging to a chair.
+    ///     Can equal buckleUid sometimes
     /// </param>
     /// <param name="strapUid"> Uid of the owner of strap component </param>
-    private bool CanBuckle(
-        EntityUid buckleUid,
-        EntityUid userUid,
+    /// <param name="strapComp"></param>
+    /// <param name="buckleComp"></param>
+    private bool CanBuckle(EntityUid buckleUid,
+        EntityUid? user,
         EntityUid strapUid,
+        bool popup,
         [NotNullWhen(true)] out StrapComponent? strapComp,
-        BuckleComponent? buckleComp = null)
+        BuckleComponent buckleComp)
     {
         strapComp = null;
-
-        if (userUid == strapUid ||
-            !Resolve(buckleUid, ref buckleComp, false) ||
-            !Resolve(strapUid, ref strapComp, false))
-        {
+        if (!Resolve(strapUid, ref strapComp, false))
             return false;
-        }
 
         // Does it pass the Whitelist
         if (_whitelistSystem.IsWhitelistFail(strapComp.Whitelist, buckleUid) ||
             _whitelistSystem.IsBlacklistPass(strapComp.Blacklist, buckleUid))
         {
-            if (_netManager.IsServer)
-                _popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), userUid, buckleUid, PopupType.Medium);
+            if (_netManager.IsServer && popup && user != null)
+                _popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), user.Value, user.Value, PopupType.Medium);
             return false;
         }
 
-        // Is it within range
-        bool Ignored(EntityUid entity) => entity == buckleUid || entity == userUid || entity == strapUid;
-
-        if (!_interaction.InRangeUnobstructed(buckleUid, strapUid, buckleComp.Range, predicate: Ignored,
+        if (!_interaction.InRangeUnobstructed(buckleUid,
+                strapUid,
+                buckleComp.Range,
+                predicate: entity => entity == buckleUid || entity == user || entity == strapUid,
                 popup: true))
         {
             return false;
         }
 
-        // If in a container
-        if (_container.TryGetContainingContainer(buckleUid, out var ownerContainer))
-        {
-            // And not in the same container as the strap
-            if (!_container.TryGetContainingContainer(strapUid, out var strapContainer) ||
-                ownerContainer != strapContainer)
-            {
-                return false;
-            }
-        }
+        if (!_container.IsInSameOrNoContainer((buckleUid, null, null), (strapUid, null, null)))
+            return false;
 
-        if (!HasComp<HandsComponent>(userUid))
+        if (user != null && !HasComp<HandsComponent>(user))
         {
             // PopupPredicted when
-            if (_netManager.IsServer)
-                _popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), userUid, userUid);
+            if (_netManager.IsServer && popup)
+                _popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), user.Value, user.Value);
             return false;
         }
 
         if (buckleComp.Buckled)
         {
-            var message = Loc.GetString(buckleUid == userUid
+            if (_netManager.IsClient || popup || user == null)
+                return false;
+
+            var message = Loc.GetString(buckleUid == user
                     ? "buckle-component-already-buckled-message"
                     : "buckle-component-other-already-buckled-message",
                 ("owner", Identity.Entity(buckleUid, EntityManager)));
-            if (_netManager.IsServer)
-                _popup.PopupEntity(message, userUid, userUid);
 
+            _popup.PopupEntity(message, user.Value, user.Value);
             return false;
         }
 
+        // Check whether someone is attempting to buckle something to their own child
         var parent = Transform(strapUid).ParentUid;
         while (parent.IsValid())
         {
-            if (parent == userUid)
+            if (parent != buckleUid)
             {
-                var message = Loc.GetString(buckleUid == userUid
-                    ? "buckle-component-cannot-buckle-message"
-                    : "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
-                if (_netManager.IsServer)
-                    _popup.PopupEntity(message, userUid, userUid);
+                parent = Transform(parent).ParentUid;
+                continue;
+            }
 
+            if (_netManager.IsClient || popup || user == null)
                 return false;
-            }
 
-            parent = Transform(parent).ParentUid;
+            var message = Loc.GetString(buckleUid == user
+                    ? "buckle-component-cannot-buckle-message"
+                    : "buckle-component-other-cannot-buckle-message",
+                ("owner", Identity.Entity(buckleUid, EntityManager)));
+
+            _popup.PopupEntity(message, user.Value, user.Value);
+            return false;
         }
 
         if (!StrapHasSpace(strapUid, buckleComp, strapComp))
         {
-            var message = Loc.GetString(buckleUid == userUid
-                ? "buckle-component-cannot-fit-message"
-                : "buckle-component-other-cannot-fit-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
-            if (_netManager.IsServer)
-                _popup.PopupEntity(message, userUid, userUid);
+            if (_netManager.IsClient || popup || user == null)
+                return false;
+
+            var message = Loc.GetString(buckleUid == user
+                    ? "buckle-component-cannot-fit-message"
+                    : "buckle-component-other-cannot-fit-message",
+                ("owner", Identity.Entity(buckleUid, EntityManager)));
+
+            _popup.PopupEntity(message, user.Value, user.Value);
 
             return false;
         }
 
-        var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, true);
-        RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent);
-        RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent);
-        if (attemptEvent.Cancelled)
+        var buckleAttempt = new BuckleAttemptEvent((strapUid, strapComp), (buckleUid, buckleComp), user, popup);
+        RaiseLocalEvent(buckleUid, ref buckleAttempt);
+        if (buckleAttempt.Cancelled)
+            return false;
+
+        var strapAttempt = new StrapAttemptEvent((strapUid, strapComp), (buckleUid, buckleComp), user, popup);
+        RaiseLocalEvent(strapUid, ref strapAttempt);
+        if (strapAttempt.Cancelled)
             return false;
 
         return true;
@@ -315,216 +317,194 @@ public abstract partial class SharedBuckleSystem
     /// <summary>
     /// Attempts to buckle an entity to a strap
     /// </summary>
-    /// <param name="buckleUid"> Uid of the owner of BuckleComponent </param>
-    /// <param name="userUid">
+    /// <param name="buckle"> Uid of the owner of BuckleComponent </param>
+    /// <param name="user">
     /// Uid of a third party entity,
     /// i.e, the uid of someone else you are dragging to a chair.
     /// Can equal buckleUid sometimes
     /// </param>
-    /// <param name="strapUid"> Uid of the owner of strap component </param>
-    public bool TryBuckle(EntityUid buckleUid, EntityUid userUid, EntityUid strapUid, BuckleComponent? buckleComp = null)
+    /// <param name="strap"> Uid of the owner of strap component </param>
+    public bool TryBuckle(EntityUid buckle, EntityUid? user, EntityUid strap, BuckleComponent? buckleComp = null, bool popup = true)
     {
-        if (!Resolve(buckleUid, ref buckleComp, false))
+        if (!Resolve(buckle, ref buckleComp, false))
             return false;
 
-        if (!CanBuckle(buckleUid, userUid, strapUid, out var strapComp, buckleComp))
+        if (!CanBuckle(buckle, user, strap, popup, out var strapComp, buckleComp))
             return false;
 
-        if (!StrapTryAdd(strapUid, buckleUid, buckleComp, false, strapComp))
-        {
-            var message = Loc.GetString(buckleUid == userUid
-                ? "buckle-component-cannot-buckle-message"
-                : "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
-            if (_netManager.IsServer)
-                _popup.PopupEntity(message, userUid, userUid);
-            return false;
-        }
+        Buckle((buckle, buckleComp), (strap, strapComp), user);
+        return true;
+    }
 
-        if (TryComp<AppearanceComponent>(buckleUid, out var appearance))
-            Appearance.SetData(buckleUid, BuckleVisuals.Buckled, true, appearance);
+    private void Buckle(Entity<BuckleComponent> buckle, Entity<StrapComponent> strap, EntityUid? user)
+    {
+        if (user == buckle.Owner)
+            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):player} buckled themselves to {ToPrettyString(strap)}");
+        else if (user != null)
+            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):player} buckled {ToPrettyString(buckle)} to {ToPrettyString(strap)}");
 
-        _rotationVisuals.SetHorizontalAngle(buckleUid, strapComp.Rotation);
+        _audio.PlayPredicted(strap.Comp.BuckleSound, strap, user);
 
-        ReAttach(buckleUid, strapUid, buckleComp, strapComp);
-        SetBuckledTo(buckleUid, strapUid, strapComp, buckleComp);
-        // TODO user is currently set to null because if it isn't the sound fails to play in some situations, fix that
-        _audio.PlayPredicted(strapComp.BuckleSound, strapUid, userUid);
+        SetBuckledTo(buckle, strap!);
+        Appearance.SetData(strap, StrapVisuals.State, true);
+        Appearance.SetData(buckle, BuckleVisuals.Buckled, true);
 
-        var ev = new BuckleChangeEvent(strapUid, buckleUid, true);
-        RaiseLocalEvent(ev.BuckledEntity, ref ev);
-        RaiseLocalEvent(ev.StrapEntity, ref ev);
+        _rotationVisuals.SetHorizontalAngle(buckle.Owner, strap.Comp.Rotation);
 
-        if (TryComp<PullableComponent>(buckleUid, out var ownerPullable))
-        {
-            if (ownerPullable.Puller != null)
-            {
-                _pulling.TryStopPull(buckleUid, ownerPullable);
-            }
-        }
+        var xform = Transform(buckle);
+        var coords = new EntityCoordinates(strap, strap.Comp.BuckleOffset);
+        _transform.SetCoordinates(buckle, xform, coords, rotation: Angle.Zero);
 
-        if (TryComp<PhysicsComponent>(buckleUid, out var physics))
-        {
-            _physics.ResetDynamics(buckleUid, physics);
-        }
+        _joints.SetRelay(buckle, strap);
 
-        if (!buckleComp.PullStrap && TryComp<PullableComponent>(strapUid, out var toPullable))
+        switch (strap.Comp.Position)
         {
-            if (toPullable.Puller == buckleUid)
-            {
-                // can't pull it and buckle to it at the same time
-                _pulling.TryStopPull(strapUid, toPullable);
-            }
+            case StrapPosition.Stand:
+                _standing.Stand(buckle);
+                break;
+            case StrapPosition.Down:
+                _standing.Down(buckle, false, false);
+                break;
         }
 
-        // Logging
-        if (userUid != buckleUid)
-            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} buckled {ToPrettyString(buckleUid)} to {ToPrettyString(strapUid)}");
-        else
-            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} buckled themselves to {ToPrettyString(strapUid)}");
+        var ev = new StrappedEvent(strap, buckle);
+        RaiseLocalEvent(strap, ref ev);
 
-        return true;
+        var gotEv = new BuckledEvent(strap, buckle);
+        RaiseLocalEvent(buckle, ref gotEv);
+
+        if (TryComp<PhysicsComponent>(buckle, out var physics))
+            _physics.ResetDynamics(buckle, physics);
+
+        DebugTools.AssertEqual(xform.ParentUid, strap.Owner);
     }
 
     /// <summary>
     /// Tries to unbuckle the Owner of this component from its current strap.
     /// </summary>
     /// <param name="buckleUid">The entity to unbuckle.</param>
-    /// <param name="userUid">The entity doing the unbuckling.</param>
-    /// <param name="force">
-    /// Whether to force the unbuckling or not. Does not guarantee true to
-    /// be returned, but guarantees the owner to be unbuckled afterwards.
-    /// </param>
+    /// <param name="user">The entity doing the unbuckling.</param>
     /// <param name="buckleComp">The buckle component of the entity to unbuckle.</param>
     /// <returns>
     ///     true if the owner was unbuckled, otherwise false even if the owner
     ///     was previously already unbuckled.
     /// </returns>
-    public bool TryUnbuckle(EntityUid buckleUid, EntityUid userUid, bool force = false, BuckleComponent? buckleComp = null)
+    public bool TryUnbuckle(EntityUid buckleUid,
+        EntityUid? user,
+        BuckleComponent? buckleComp = null,
+        bool popup = true)
+    {
+        return TryUnbuckle((buckleUid, buckleComp), user, popup);
+    }
+
+    public bool TryUnbuckle(Entity<BuckleComponent?> buckle, EntityUid? user, bool popup)
     {
-        if (!Resolve(buckleUid, ref buckleComp, false) ||
-            buckleComp.BuckledTo is not { } strapUid)
+        if (!Resolve(buckle.Owner, ref buckle.Comp))
             return false;
 
-        if (!force)
-        {
-            var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, false);
-            RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent);
-            RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent);
-            if (attemptEvent.Cancelled)
-                return false;
+        if (!CanUnbuckle(buckle, user, popup, out var strap))
+            return false;
 
-            if (_gameTiming.CurTime < buckleComp.BuckleTime + buckleComp.Delay)
-                return false;
+        Unbuckle(buckle!, strap, user);
+        return true;
+    }
 
-            if (!_interaction.InRangeUnobstructed(userUid, strapUid, buckleComp.Range, popup: true))
-                return false;
+    public void Unbuckle(Entity<BuckleComponent?> buckle, EntityUid? user)
+    {
+        if (!Resolve(buckle.Owner, ref buckle.Comp, false))
+            return;
 
-            if (HasComp<SleepingComponent>(buckleUid) && buckleUid == userUid)
-                return false;
+        if (buckle.Comp.BuckledTo is not { } strap)
+            return;
 
-            // If the person is crit or dead in any kind of strap, return. This prevents people from unbuckling themselves while incapacitated.
-            if (_mobState.IsIncapacitated(buckleUid) && userUid == buckleUid)
-                return false;
+        if (!TryComp(strap, out StrapComponent? strapComp))
+        {
+            Log.Error($"Encountered buckle {ToPrettyString(buckle.Owner)} with invalid strap entity {ToPrettyString(strap)}");
+            SetBuckledTo(buckle!, null);
+            return;
         }
 
-        // Logging
-        if (userUid != buckleUid)
-            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} unbuckled {ToPrettyString(buckleUid)} from {ToPrettyString(strapUid)}");
-        else
-            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} unbuckled themselves from {ToPrettyString(strapUid)}");
+        Unbuckle(buckle!, (strap, strapComp), user);
+    }
+
+    private void Unbuckle(Entity<BuckleComponent> buckle, Entity<StrapComponent> strap, EntityUid? user)
+    {
+        if (user == buckle.Owner)
+            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled themselves from {strap}");
+        else if (user != null)
+            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled {buckle} from {strap}");
 
-        SetBuckledTo(buckleUid, null, null, buckleComp);
+        _audio.PlayPredicted(strap.Comp.UnbuckleSound, strap, user);
 
-        if (!TryComp<StrapComponent>(strapUid, out var strapComp))
-            return false;
+        SetBuckledTo(buckle, null);
 
-        var buckleXform = Transform(buckleUid);
-        var oldBuckledXform = Transform(strapUid);
+        var buckleXform = Transform(buckle);
+        var oldBuckledXform = Transform(strap);
 
-        if (buckleXform.ParentUid == strapUid && !Terminating(buckleXform.ParentUid))
+        if (buckleXform.ParentUid == strap.Owner && !Terminating(buckleXform.ParentUid))
         {
-            _container.AttachParentToContainerOrGrid((buckleUid, buckleXform));
+            _container.AttachParentToContainerOrGrid((buckle, buckleXform));
 
-            var oldBuckledToWorldRot = _transform.GetWorldRotation(strapUid);
+            var oldBuckledToWorldRot = _transform.GetWorldRotation(strap);
             _transform.SetWorldRotation(buckleXform, oldBuckledToWorldRot);
 
-            if (strapComp.UnbuckleOffset != Vector2.Zero)
-                buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strapComp.UnbuckleOffset);
+            if (strap.Comp.UnbuckleOffset != Vector2.Zero)
+                buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strap.Comp.UnbuckleOffset);
         }
 
-        if (TryComp(buckleUid, out AppearanceComponent? appearance))
-            Appearance.SetData(buckleUid, BuckleVisuals.Buckled, false, appearance);
-        _rotationVisuals.ResetHorizontalAngle(buckleUid);
+        _rotationVisuals.ResetHorizontalAngle(buckle.Owner);
+        Appearance.SetData(strap, StrapVisuals.State, strap.Comp.BuckledEntities.Count != 0);
+        Appearance.SetData(buckle, BuckleVisuals.Buckled, false);
 
-        if (TryComp<MobStateComponent>(buckleUid, out var mobState)
-            && _mobState.IsIncapacitated(buckleUid, mobState)
-            || HasComp<KnockedDownComponent>(buckleUid))
-        {
-            _standing.Down(buckleUid);
-        }
+        if (HasComp<KnockedDownComponent>(buckle) || _mobState.IsIncapacitated(buckle))
+            _standing.Down(buckle);
         else
-        {
-            _standing.Stand(buckleUid);
-        }
+            _standing.Stand(buckle);
 
-        if (_mobState.IsIncapacitated(buckleUid, mobState))
-        {
-            _standing.Down(buckleUid);
-        }
-        if (strapComp.BuckledEntities.Remove(buckleUid))
-        {
-            strapComp.OccupiedSize -= buckleComp.Size;
-            Dirty(strapUid, strapComp);
-        }
-
-        _joints.RefreshRelay(buckleUid);
-        Appearance.SetData(strapUid, StrapVisuals.State, strapComp.BuckledEntities.Count != 0);
+        _joints.RefreshRelay(buckle);
 
-        // TODO: Buckle listening to moveevents is sussy anyway.
-        if (!TerminatingOrDeleted(strapUid))
-            _audio.PlayPredicted(strapComp.UnbuckleSound, strapUid, userUid);
+        var buckleEv = new UnbuckledEvent(strap, buckle);
+        RaiseLocalEvent(buckle, ref buckleEv);
 
-        var ev = new BuckleChangeEvent(strapUid, buckleUid, false);
-        RaiseLocalEvent(buckleUid, ref ev);
-        RaiseLocalEvent(strapUid, ref ev);
+        var strapEv = new UnstrappedEvent(strap, buckle);
+        RaiseLocalEvent(strap, ref strapEv);
+    }
 
-        return true;
+    public bool CanUnbuckle(Entity<BuckleComponent?> buckle, EntityUid user, bool popup)
+    {
+        return CanUnbuckle(buckle, user, popup, out _);
     }
 
-    /// <summary>
-    /// Makes an entity toggle the buckling status of the owner to a
-    /// specific entity.
-    /// </summary>
-    /// <param name="buckleUid">The entity to buckle/unbuckle from <see cref="to"/>.</param>
-    /// <param name="userUid">The entity doing the buckling/unbuckling.</param>
-    /// <param name="strapUid">
-    /// The entity to toggle the buckle status of the owner to.
-    /// </param>
-    /// <param name="force">
-    /// Whether to force the unbuckling or not, if it happens. Does not
-    /// guarantee true to be returned, but guarantees the owner to be
-    /// unbuckled afterwards.
-    /// </param>
-    /// <param name="buckle">The buckle component of the entity to buckle/unbuckle from <see cref="to"/>.</param>
-    /// <returns>true if the buckling status was changed, false otherwise.</returns>
-    public bool ToggleBuckle(
-        EntityUid buckleUid,
-        EntityUid userUid,
-        EntityUid strapUid,
-        bool force = false,
-        BuckleComponent? buckle = null)
+    private bool CanUnbuckle(Entity<BuckleComponent?> buckle, EntityUid? user, bool popup, out Entity<StrapComponent> strap)
     {
-        if (!Resolve(buckleUid, ref buckle, false))
+        strap = default;
+        if (!Resolve(buckle.Owner, ref buckle.Comp))
             return false;
 
-        if (!buckle.Buckled)
-        {
-            return TryBuckle(buckleUid, userUid, strapUid, buckle);
-        }
-        else
+        if (buckle.Comp.BuckledTo is not { } strapUid)
+            return false;
+
+        if (!TryComp(strapUid, out StrapComponent? strapComp))
         {
-            return TryUnbuckle(buckleUid, userUid, force, buckle);
+            Log.Error($"Encountered buckle {ToPrettyString(buckle.Owner)} with invalid strap entity {ToPrettyString(strap)}");
+            SetBuckledTo(buckle!, null);
+            return false;
         }
 
+        strap = (strapUid, strapComp);
+        if (_gameTiming.CurTime < buckle.Comp.BuckleTime + buckle.Comp.Delay)
+            return false;
+
+        if (user != null && !_interaction.InRangeUnobstructed(user.Value, strap.Owner, buckle.Comp.Range, popup: popup))
+            return false;
+
+        var unbuckleAttempt = new UnbuckleAttemptEvent(strap, buckle!, user, popup);
+        RaiseLocalEvent(buckle, ref unbuckleAttempt);
+        if (unbuckleAttempt.Cancelled)
+            return false;
+
+        var unstrapAttempt = new UnstrapAttemptEvent(strap, buckle!, user, popup);
+        RaiseLocalEvent(strap, ref unstrapAttempt);
+        return !unstrapAttempt.Cancelled;
     }
 }
diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs b/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs
new file mode 100644 (file)
index 0000000..8c2d0b8
--- /dev/null
@@ -0,0 +1,171 @@
+using Content.Shared.Buckle.Components;
+using Content.Shared.DragDrop;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction;
+using Content.Shared.Verbs;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Buckle;
+
+// Partial class containing interaction & verb event handlers
+public abstract partial class SharedBuckleSystem
+{
+    private void InitializeInteraction()
+    {
+        SubscribeLocalEvent<StrapComponent, GetVerbsEvent<InteractionVerb>>(AddStrapVerbs);
+        SubscribeLocalEvent<StrapComponent, InteractHandEvent>(OnStrapInteractHand);
+        SubscribeLocalEvent<StrapComponent, DragDropTargetEvent>(OnStrapDragDropTarget);
+        SubscribeLocalEvent<StrapComponent, CanDropTargetEvent>(OnCanDropTarget);
+
+        SubscribeLocalEvent<BuckleComponent, GetVerbsEvent<InteractionVerb>>(AddUnbuckleVerb);
+    }
+
+    private void OnCanDropTarget(EntityUid uid, StrapComponent component, ref CanDropTargetEvent args)
+    {
+        args.CanDrop = StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component);
+        args.Handled = true;
+    }
+
+    private void OnStrapDragDropTarget(EntityUid uid, StrapComponent component, ref DragDropTargetEvent args)
+    {
+        if (!StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component))
+            return;
+
+        args.Handled = TryBuckle(args.Dragged, args.User, uid, popup: false);
+    }
+
+    private bool StrapCanDragDropOn(
+        EntityUid strapUid,
+        EntityUid userUid,
+        EntityUid targetUid,
+        EntityUid buckleUid,
+        StrapComponent? strapComp = null,
+        BuckleComponent? buckleComp = null)
+    {
+        if (!Resolve(strapUid, ref strapComp, false) ||
+            !Resolve(buckleUid, ref buckleComp, false))
+        {
+            return false;
+        }
+
+        bool Ignored(EntityUid entity) => entity == userUid || entity == buckleUid || entity == targetUid;
+
+        return _interaction.InRangeUnobstructed(targetUid, buckleUid, buckleComp.Range, predicate: Ignored);
+    }
+
+    private void OnStrapInteractHand(EntityUid uid, StrapComponent component, InteractHandEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        if (!TryComp(args.User, out BuckleComponent? buckle))
+            return;
+
+        if (buckle.BuckledTo == null)
+            TryBuckle(args.User, args.User, uid, buckle, popup: true);
+        else if (buckle.BuckledTo == uid)
+            TryUnbuckle(args.User, args.User, buckle, popup: true);
+        else
+            return;
+
+        args.Handled = true; // This generate popups on failure.
+    }
+
+    private void AddStrapVerbs(EntityUid uid, StrapComponent component, GetVerbsEvent<InteractionVerb> args)
+    {
+        if (args.Hands == null || !args.CanAccess || !args.CanInteract || !component.Enabled)
+            return;
+
+        // Note that for whatever bloody reason, buckle component has its own interaction range. Additionally, this
+        // range can be set per-component, so we have to check a modified InRangeUnobstructed for every verb.
+
+        // Add unstrap verbs for every strapped entity.
+        foreach (var entity in component.BuckledEntities)
+        {
+            var buckledComp = Comp<BuckleComponent>(entity);
+
+            if (!_interaction.InRangeUnobstructed(args.User, args.Target, range: buckledComp.Range))
+                continue;
+
+            var verb = new InteractionVerb()
+            {
+                Act = () => TryUnbuckle(entity, args.User, buckleComp: buckledComp),
+                Category = VerbCategory.Unbuckle,
+                Text = entity == args.User
+                    ? Loc.GetString("verb-self-target-pronoun")
+                    : Identity.Name(entity, EntityManager)
+            };
+
+            // In the event that you have more than once entity with the same name strapped to the same object,
+            // these two verbs will be identical according to Verb.CompareTo, and only one with actually be added to
+            // the verb list. However this should rarely ever be a problem. If it ever is, it could be fixed by
+            // appending an integer to verb.Text to distinguish the verbs.
+
+            args.Verbs.Add(verb);
+        }
+
+        // Add a verb to buckle the user.
+        if (TryComp<BuckleComponent>(args.User, out var buckle) &&
+            buckle.BuckledTo != uid &&
+            args.User != uid &&
+            StrapHasSpace(uid, buckle, component) &&
+            _interaction.InRangeUnobstructed(args.User, args.Target, range: buckle.Range))
+        {
+            InteractionVerb verb = new()
+            {
+                Act = () => TryBuckle(args.User, args.User, args.Target, buckle),
+                Category = VerbCategory.Buckle,
+                Text = Loc.GetString("verb-self-target-pronoun")
+            };
+            args.Verbs.Add(verb);
+        }
+
+        // If the user is currently holding/pulling an entity that can be buckled, add a verb for that.
+        if (args.Using is { Valid: true } @using &&
+            TryComp<BuckleComponent>(@using, out var usingBuckle) &&
+            StrapHasSpace(uid, usingBuckle, component) &&
+            _interaction.InRangeUnobstructed(@using, args.Target, range: usingBuckle.Range))
+        {
+            // Check that the entity is unobstructed from the target (ignoring the user).
+            bool Ignored(EntityUid entity) => entity == args.User || entity == args.Target || entity == @using;
+            if (!_interaction.InRangeUnobstructed(@using, args.Target, usingBuckle.Range, predicate: Ignored))
+                return;
+
+            var isPlayer = _playerManager.TryGetSessionByEntity(@using, out var _);
+            InteractionVerb verb = new()
+            {
+                Act = () => TryBuckle(@using, args.User, args.Target, usingBuckle),
+                Category = VerbCategory.Buckle,
+                Text = Identity.Name(@using, EntityManager),
+                // just a held object, the user is probably just trying to sit down.
+                // If the used entity is a person being pulled, prioritize this verb. Conversely, if it is
+                Priority = isPlayer ? 1 : -1
+            };
+
+            args.Verbs.Add(verb);
+        }
+    }
+
+    private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent<InteractionVerb> args)
+    {
+        if (!args.CanAccess || !args.CanInteract || !component.Buckled)
+            return;
+
+        InteractionVerb verb = new()
+        {
+            Act = () => TryUnbuckle(uid, args.User, buckleComp: component),
+            Text = Loc.GetString("verb-categories-unbuckle"),
+            Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/unbuckle.svg.192dpi.png"))
+        };
+
+        if (args.Target == args.User && args.Using == null)
+        {
+            // A user is left clicking themselves with an empty hand, while buckled.
+            // It is very likely they are trying to unbuckle themselves.
+            verb.Priority = 1;
+        }
+
+        args.Verbs.Add(verb);
+    }
+
+}
index 147af42e728578bee1c07eab4e002146df300232..eb23aa973b4bffe90fe00dd3b1625d87e6fd20ca 100644 (file)
@@ -2,39 +2,25 @@
 using Content.Shared.Buckle.Components;
 using Content.Shared.Construction;
 using Content.Shared.Destructible;
-using Content.Shared.DragDrop;
 using Content.Shared.Foldable;
-using Content.Shared.Interaction;
-using Content.Shared.Rotation;
 using Content.Shared.Storage;
-using Content.Shared.Verbs;
 using Robust.Shared.Containers;
 
 namespace Content.Shared.Buckle;
 
 public abstract partial class SharedBuckleSystem
 {
-    [Dependency] private readonly SharedRotationVisualsSystem _rotationVisuals = default!;
-
     private void InitializeStrap()
     {
         SubscribeLocalEvent<StrapComponent, ComponentStartup>(OnStrapStartup);
         SubscribeLocalEvent<StrapComponent, ComponentShutdown>(OnStrapShutdown);
         SubscribeLocalEvent<StrapComponent, ComponentRemove>((e, c, _) => StrapRemoveAll(e, c));
 
-        SubscribeLocalEvent<StrapComponent, EntInsertedIntoContainerMessage>(OnStrapEntModifiedFromContainer);
-        SubscribeLocalEvent<StrapComponent, EntRemovedFromContainerMessage>(OnStrapEntModifiedFromContainer);
-        SubscribeLocalEvent<StrapComponent, GetVerbsEvent<InteractionVerb>>(AddStrapVerbs);
         SubscribeLocalEvent<StrapComponent, ContainerGettingInsertedAttemptEvent>(OnStrapContainerGettingInsertedAttempt);
-        SubscribeLocalEvent<StrapComponent, InteractHandEvent>(OnStrapInteractHand);
         SubscribeLocalEvent<StrapComponent, DestructionEventArgs>((e, c, _) => StrapRemoveAll(e, c));
         SubscribeLocalEvent<StrapComponent, BreakageEventArgs>((e, c, _) => StrapRemoveAll(e, c));
 
-        SubscribeLocalEvent<StrapComponent, DragDropTargetEvent>(OnStrapDragDropTarget);
-        SubscribeLocalEvent<StrapComponent, CanDropTargetEvent>(OnCanDropTarget);
         SubscribeLocalEvent<StrapComponent, FoldAttemptEvent>(OnAttemptFold);
-
-        SubscribeLocalEvent<StrapComponent, MoveEvent>(OnStrapMoveEvent);
         SubscribeLocalEvent<StrapComponent, MachineDeconstructedEvent>((e, c, _) => StrapRemoveAll(e, c));
     }
 
@@ -45,145 +31,17 @@ public abstract partial class SharedBuckleSystem
 
     private void OnStrapShutdown(EntityUid uid, StrapComponent component, ComponentShutdown args)
     {
-        if (LifeStage(uid) > EntityLifeStage.MapInitialized)
-            return;
-
-        StrapRemoveAll(uid, component);
-    }
-
-    private void OnStrapEntModifiedFromContainer(EntityUid uid, StrapComponent component, ContainerModifiedMessage message)
-    {
-        if (_gameTiming.ApplyingState)
-            return;
-
-        foreach (var buckledEntity in component.BuckledEntities)
-        {
-            if (!TryComp<BuckleComponent>(buckledEntity, out var buckleComp))
-            {
-                continue;
-            }
-
-            ContainerModifiedReAttach(buckledEntity, uid, buckleComp, component);
-        }
-    }
-
-    private void ContainerModifiedReAttach(EntityUid buckleUid, EntityUid strapUid, BuckleComponent? buckleComp = null, StrapComponent? strapComp = null)
-    {
-        if (!Resolve(buckleUid, ref buckleComp, false) ||
-            !Resolve(strapUid, ref strapComp, false))
-            return;
-
-        var contained = _container.TryGetContainingContainer(buckleUid, out var ownContainer);
-        var strapContained = _container.TryGetContainingContainer(strapUid, out var strapContainer);
-
-        if (contained != strapContained || ownContainer != strapContainer)
-        {
-            TryUnbuckle(buckleUid, buckleUid, true, buckleComp);
-            return;
-        }
-
-        if (!contained)
-        {
-            ReAttach(buckleUid, strapUid, buckleComp, strapComp);
-        }
+        if (!TerminatingOrDeleted(uid))
+            StrapRemoveAll(uid, component);
     }
 
     private void OnStrapContainerGettingInsertedAttempt(EntityUid uid, StrapComponent component, ContainerGettingInsertedAttemptEvent args)
     {
         // If someone is attempting to put this item inside of a backpack, ensure that it has no entities strapped to it.
-        if (HasComp<StorageComponent>(args.Container.Owner) && component.BuckledEntities.Count != 0)
+        if (args.Container.ID == StorageComponent.ContainerId && component.BuckledEntities.Count != 0)
             args.Cancel();
     }
 
-    private void OnStrapInteractHand(EntityUid uid, StrapComponent component, InteractHandEvent args)
-    {
-        if (args.Handled)
-            return;
-
-        args.Handled = ToggleBuckle(args.User, args.User, uid);
-    }
-
-    private void AddStrapVerbs(EntityUid uid, StrapComponent component, GetVerbsEvent<InteractionVerb> args)
-    {
-        if (args.Hands == null || !args.CanAccess || !args.CanInteract || !component.Enabled)
-            return;
-
-        // Note that for whatever bloody reason, buckle component has its own interaction range. Additionally, this
-        // range can be set per-component, so we have to check a modified InRangeUnobstructed for every verb.
-
-        // Add unstrap verbs for every strapped entity.
-        foreach (var entity in component.BuckledEntities)
-        {
-            var buckledComp = Comp<BuckleComponent>(entity);
-
-            if (!_interaction.InRangeUnobstructed(args.User, args.Target, range: buckledComp.Range))
-                continue;
-
-            var verb = new InteractionVerb()
-            {
-                Act = () => TryUnbuckle(entity, args.User, buckleComp: buckledComp),
-                Category = VerbCategory.Unbuckle,
-                Text = entity == args.User
-                    ? Loc.GetString("verb-self-target-pronoun")
-                    : Comp<MetaDataComponent>(entity).EntityName
-            };
-
-            // In the event that you have more than once entity with the same name strapped to the same object,
-            // these two verbs will be identical according to Verb.CompareTo, and only one with actually be added to
-            // the verb list. However this should rarely ever be a problem. If it ever is, it could be fixed by
-            // appending an integer to verb.Text to distinguish the verbs.
-
-            args.Verbs.Add(verb);
-        }
-
-        // Add a verb to buckle the user.
-        if (TryComp<BuckleComponent>(args.User, out var buckle) &&
-            buckle.BuckledTo != uid &&
-            args.User != uid &&
-            StrapHasSpace(uid, buckle, component) &&
-            _interaction.InRangeUnobstructed(args.User, args.Target, range: buckle.Range))
-        {
-            InteractionVerb verb = new()
-            {
-                Act = () => TryBuckle(args.User, args.User, args.Target, buckle),
-                Category = VerbCategory.Buckle,
-                Text = Loc.GetString("verb-self-target-pronoun")
-            };
-            args.Verbs.Add(verb);
-        }
-
-        // If the user is currently holding/pulling an entity that can be buckled, add a verb for that.
-        if (args.Using is { Valid: true } @using &&
-            TryComp<BuckleComponent>(@using, out var usingBuckle) &&
-            StrapHasSpace(uid, usingBuckle, component) &&
-            _interaction.InRangeUnobstructed(@using, args.Target, range: usingBuckle.Range))
-        {
-            // Check that the entity is unobstructed from the target (ignoring the user).
-            bool Ignored(EntityUid entity) => entity == args.User || entity == args.Target || entity == @using;
-            if (!_interaction.InRangeUnobstructed(@using, args.Target, usingBuckle.Range, predicate: Ignored))
-                return;
-
-            var isPlayer = _playerManager.TryGetSessionByEntity(@using, out var _);
-            InteractionVerb verb = new()
-            {
-                Act = () => TryBuckle(@using, args.User, args.Target, usingBuckle),
-                Category = VerbCategory.Buckle,
-                Text = Comp<MetaDataComponent>(@using).EntityName,
-                // just a held object, the user is probably just trying to sit down.
-                // If the used entity is a person being pulled, prioritize this verb. Conversely, if it is
-                Priority = isPlayer ? 1 : -1
-            };
-
-            args.Verbs.Add(verb);
-        }
-    }
-
-    private void OnCanDropTarget(EntityUid uid, StrapComponent component, ref CanDropTargetEvent args)
-    {
-        args.CanDrop = StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component);
-        args.Handled = true;
-    }
-
     private void OnAttemptFold(EntityUid uid, StrapComponent component, ref FoldAttemptEvent args)
     {
         if (args.Cancelled)
@@ -192,69 +50,6 @@ public abstract partial class SharedBuckleSystem
         args.Cancelled = component.BuckledEntities.Count != 0;
     }
 
-    private void OnStrapDragDropTarget(EntityUid uid, StrapComponent component, ref DragDropTargetEvent args)
-    {
-        if (!StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component))
-            return;
-
-        args.Handled = TryBuckle(args.Dragged, args.User, uid);
-    }
-
-    private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args)
-    {
-        // TODO: This looks dirty af.
-        // On rotation of a strap, reattach all buckled entities.
-        // This fixes buckle offsets and draw depths.
-        // This is mega cursed. Please somebody save me from Mr Buckle's wild ride.
-        // Oh god I'm back here again. Send help.
-
-        // Consider a chair that has a player strapped to it. Then the client receives a new server state, showing
-        // that the player entity has moved elsewhere, and the chair has rotated. If the client applies the player
-        // state, then the chairs transform comp state, and then the buckle state. The transform state will
-        // forcefully teleport the player back to the chair (client-side only). This causes even more issues if the
-        // chair was teleporting in from nullspace after having left PVS.
-        //
-        // One option is to just never trigger re-buckles during state application.
-        // another is to.. just not do this? Like wtf is this code. But I CBF with buckle atm.
-
-        if (_gameTiming.ApplyingState || args.NewRotation == args.OldRotation)
-            return;
-
-        foreach (var buckledEntity in component.BuckledEntities)
-        {
-            if (!TryComp<BuckleComponent>(buckledEntity, out var buckled))
-                continue;
-
-            if (!buckled.Buckled || buckled.LastEntityBuckledTo != uid)
-            {
-                Log.Error($"A moving strap entity {ToPrettyString(uid)} attempted to re-parent an entity that does not 'belong' to it {ToPrettyString(buckledEntity)}");
-                continue;
-            }
-
-            ReAttach(buckledEntity, uid, buckled, component);
-            Dirty(buckledEntity, buckled);
-        }
-    }
-
-    private bool StrapCanDragDropOn(
-        EntityUid strapUid,
-        EntityUid userUid,
-        EntityUid targetUid,
-        EntityUid buckleUid,
-        StrapComponent? strapComp = null,
-        BuckleComponent? buckleComp = null)
-    {
-        if (!Resolve(strapUid, ref strapComp, false) ||
-            !Resolve(buckleUid, ref buckleComp, false))
-        {
-            return false;
-        }
-
-        bool Ignored(EntityUid entity) => entity == userUid || entity == buckleUid || entity == targetUid;
-
-        return _interaction.InRangeUnobstructed(targetUid, buckleUid, buckleComp.Range, predicate: Ignored);
-    }
-
     /// <summary>
     /// Remove everything attached to the strap
     /// </summary>
@@ -264,10 +59,6 @@ public abstract partial class SharedBuckleSystem
         {
             TryUnbuckle(entity, entity, true);
         }
-
-        strapComp.BuckledEntities.Clear();
-        strapComp.OccupiedSize = 0;
-        Dirty(uid, strapComp);
     }
 
     private bool StrapHasSpace(EntityUid strapUid, BuckleComponent buckleComp, StrapComponent? strapComp = null)
@@ -275,30 +66,13 @@ public abstract partial class SharedBuckleSystem
         if (!Resolve(strapUid, ref strapComp, false))
             return false;
 
-        return strapComp.OccupiedSize + buckleComp.Size <= strapComp.Size;
-    }
-
-    /// <summary>
-    /// Try to add an entity to the strap
-    /// </summary>
-    private bool StrapTryAdd(EntityUid strapUid, EntityUid buckleUid, BuckleComponent buckleComp, bool force = false, StrapComponent? strapComp = null)
-    {
-        if (!Resolve(strapUid, ref strapComp, false) ||
-            !strapComp.Enabled)
-            return false;
-
-        if (!force && !StrapHasSpace(strapUid, buckleComp, strapComp))
-            return false;
-
-        if (!strapComp.BuckledEntities.Add(buckleUid))
-            return false;
-
-        strapComp.OccupiedSize += buckleComp.Size;
-
-        Appearance.SetData(strapUid, StrapVisuals.State, true);
+        var avail = strapComp.Size;
+        foreach (var buckle in strapComp.BuckledEntities)
+        {
+            avail -= CompOrNull<BuckleComponent>(buckle)?.Size ?? 0;
+        }
 
-        Dirty(strapUid, strapComp);
-        return true;
+        return avail >= buckleComp.Size;
     }
 
     /// <summary>
@@ -311,6 +85,7 @@ public abstract partial class SharedBuckleSystem
             return;
 
         strapComp.Enabled = enabled;
+        Dirty(strapUid, strapComp);
 
         if (!enabled)
             StrapRemoveAll(strapUid, strapComp);
index 67218657e52e994c6d4f6d6a390cdb14712ee5cc..770fababded6c881adad02bc664458544b0e1c64 100644 (file)
@@ -1,21 +1,17 @@
 using Content.Shared.ActionBlocker;
 using Content.Shared.Administration.Logs;
 using Content.Shared.Alert;
-using Content.Shared.Buckle.Components;
 using Content.Shared.Interaction;
 using Content.Shared.Mobs.Systems;
 using Content.Shared.Popups;
-using Content.Shared.Pulling;
+using Content.Shared.Rotation;
 using Content.Shared.Standing;
-using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.Containers;
-using Robust.Shared.Map;
 using Robust.Shared.Network;
 using Robust.Shared.Physics.Systems;
 using Robust.Shared.Player;
 using Robust.Shared.Timing;
-using PullingSystem = Content.Shared.Movement.Pulling.Systems.PullingSystem;
 
 namespace Content.Shared.Buckle;
 
@@ -36,10 +32,10 @@ public abstract partial class SharedBuckleSystem : EntitySystem
     [Dependency] private readonly SharedInteractionSystem _interaction = default!;
     [Dependency] private readonly SharedJointSystem _joints = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
-    [Dependency] private readonly PullingSystem _pulling = default!;
     [Dependency] private readonly SharedTransformSystem _transform = default!;
     [Dependency] private readonly StandingStateSystem _standing = default!;
     [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+    [Dependency] private readonly SharedRotationVisualsSystem _rotationVisuals = default!;
 
     /// <inheritdoc/>
     public override void Initialize()
@@ -51,45 +47,6 @@ public abstract partial class SharedBuckleSystem : EntitySystem
 
         InitializeBuckle();
         InitializeStrap();
-    }
-
-    /// <summary>
-    /// Reattaches this entity to the strap, modifying its position and rotation.
-    /// </summary>
-    /// <param name="buckleUid">The entity to reattach.</param>
-    /// <param name="strapUid">The entity to reattach the buckleUid entity to.</param>
-    private void ReAttach(
-        EntityUid buckleUid,
-        EntityUid strapUid,
-        BuckleComponent? buckleComp = null,
-        StrapComponent? strapComp = null)
-    {
-        if (!Resolve(strapUid, ref strapComp, false)
-            || !Resolve(buckleUid, ref buckleComp, false))
-            return;
-
-        _transform.SetCoordinates(buckleUid, new EntityCoordinates(strapUid, strapComp.BuckleOffsetClamped));
-
-        var buckleTransform = Transform(buckleUid);
-
-        // Buckle subscribes to move for <reasons> so this might fail.
-        // TODO: Make buckle not do that.
-        if (buckleTransform.ParentUid != strapUid)
-            return;
-
-        _transform.SetLocalRotation(buckleUid, Angle.Zero, buckleTransform);
-        _joints.SetRelay(buckleUid, strapUid);
-
-        switch (strapComp.Position)
-        {
-            case StrapPosition.None:
-                break;
-            case StrapPosition.Stand:
-                _standing.Stand(buckleUid);
-                break;
-            case StrapPosition.Down:
-                _standing.Down(buckleUid, false, false);
-                break;
-        }
+        InitializeInteraction();
     }
 }
index ac01c4e9acbc2be4b5cb5f3d37f824d31f098a43..726cdc24687af190bc976c9adfdbbd47a8c087a8 100644 (file)
@@ -58,7 +58,7 @@ public sealed partial class ClimbSystem : VirtualController
         SubscribeLocalEvent<ClimbingComponent, EntParentChangedMessage>(OnParentChange);
         SubscribeLocalEvent<ClimbingComponent, ClimbDoAfterEvent>(OnDoAfter);
         SubscribeLocalEvent<ClimbingComponent, EndCollideEvent>(OnClimbEndCollide);
-        SubscribeLocalEvent<ClimbingComponent, BuckleChangeEvent>(OnBuckleChange);
+        SubscribeLocalEvent<ClimbingComponent, BuckledEvent>(OnBuckled);
 
         SubscribeLocalEvent<ClimbableComponent, CanDropTargetEvent>(OnCanDragDropOn);
         SubscribeLocalEvent<ClimbableComponent, GetVerbsEvent<AlternativeVerb>>(AddClimbableVerb);
@@ -468,10 +468,8 @@ public sealed partial class ClimbSystem : VirtualController
         Climb(uid, uid, climbable, true, component);
     }
 
-    private void OnBuckleChange(EntityUid uid, ClimbingComponent component, ref BuckleChangeEvent args)
+    private void OnBuckled(EntityUid uid, ClimbingComponent component, ref BuckledEvent args)
     {
-        if (!args.Buckling)
-            return;
         StopClimb(uid, component);
     }
 
index be169deb0e5be7bd4d2a194584ec5114d96511e1..b9f287f1ce45c823961394f91d2b9ccfe9a6fe92 100644 (file)
@@ -71,6 +71,7 @@ namespace Content.Shared.Cuffs
             SubscribeLocalEvent<CuffableComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
             SubscribeLocalEvent<CuffableComponent, BeingPulledAttemptEvent>(OnBeingPulledAttempt);
             SubscribeLocalEvent<CuffableComponent, BuckleAttemptEvent>(OnBuckleAttemptEvent);
+            SubscribeLocalEvent<CuffableComponent, UnbuckleAttemptEvent>(OnUnbuckleAttemptEvent);
             SubscribeLocalEvent<CuffableComponent, GetVerbsEvent<Verb>>(AddUncuffVerb);
             SubscribeLocalEvent<CuffableComponent, UnCuffDoAfterEvent>(OnCuffableDoAfter);
             SubscribeLocalEvent<CuffableComponent, PullStartedMessage>(OnPull);
@@ -195,21 +196,33 @@ namespace Content.Shared.Cuffs
                 args.Cancel();
         }
 
-        private void OnBuckleAttemptEvent(EntityUid uid, CuffableComponent component, ref BuckleAttemptEvent args)
+        private void OnBuckleAttempt(Entity<CuffableComponent> ent, EntityUid? user, ref bool cancelled, bool buckling, bool popup)
         {
-            // if someone else is doing it, let it pass.
-            if (args.UserEntity != uid)
+            if (cancelled || user != ent.Owner)
                 return;
 
-            if (!TryComp<HandsComponent>(uid, out var hands) || component.CuffedHandCount != hands.Count)
+            if (!TryComp<HandsComponent>(ent, out var hands) || ent.Comp.CuffedHandCount != hands.Count)
                 return;
 
-            args.Cancelled = true;
-            var message = args.Buckling
+            cancelled = true;
+            if (!popup)
+                return;
+
+            var message = buckling
                 ? Loc.GetString("handcuff-component-cuff-interrupt-buckled-message")
                 : Loc.GetString("handcuff-component-cuff-interrupt-unbuckled-message");
 
-            _popup.PopupClient(message, uid, args.UserEntity);
+            _popup.PopupClient(message, ent, user);
+        }
+
+        private void OnBuckleAttemptEvent(Entity<CuffableComponent> ent, ref BuckleAttemptEvent args)
+        {
+            OnBuckleAttempt(ent, args.User, ref args.Cancelled, true, args.Popup);
+        }
+
+        private void OnUnbuckleAttemptEvent(Entity<CuffableComponent> ent, ref UnbuckleAttemptEvent args)
+        {
+            OnBuckleAttempt(ent, args.User, ref args.Cancelled, false, args.Popup);
         }
 
         private void OnPull(EntityUid uid, CuffableComponent component, PullMessage args)
index 10baf8165b54ea19da0b9966528ce8b0411ea754..2a846f4f234f047f0af8b032fc17e26ea446406c 100644 (file)
@@ -26,7 +26,7 @@ public sealed class FoldableSystem : EntitySystem
         SubscribeLocalEvent<FoldableComponent, StoreMobInItemContainerAttemptEvent>(OnStoreThisAttempt);
         SubscribeLocalEvent<FoldableComponent, StorageOpenAttemptEvent>(OnFoldableOpenAttempt);
 
-        SubscribeLocalEvent<FoldableComponent, BuckleAttemptEvent>(OnBuckleAttempt);
+        SubscribeLocalEvent<FoldableComponent, StrapAttemptEvent>(OnStrapAttempt);
     }
 
     private void OnHandleState(EntityUid uid, FoldableComponent component, ref AfterAutoHandleStateEvent args)
@@ -53,9 +53,9 @@ public sealed class FoldableSystem : EntitySystem
             args.Cancelled = true;
     }
 
-    public void OnBuckleAttempt(EntityUid uid, FoldableComponent comp, ref BuckleAttemptEvent args)
+    public void OnStrapAttempt(EntityUid uid, FoldableComponent comp, ref StrapAttemptEvent args)
     {
-        if (args.Buckling && comp.IsFolded)
+        if (comp.IsFolded)
             args.Cancelled = true;
     }
 
index 7f73d3190f97869d6094a5f117dd2449f21e3e5a..6fe5582bb17bf559e0b65385dea5503f44f438d1 100644 (file)
@@ -1,7 +1,6 @@
 using System.Numerics;
 using Content.Shared.ActionBlocker;
 using Content.Shared.Buckle.Components;
-using Content.Shared.Mobs.Systems;
 using Content.Shared.Rotatable;
 using JetBrains.Annotations;
 
@@ -83,24 +82,21 @@ namespace Content.Shared.Interaction
             if (!_actionBlockerSystem.CanChangeDirection(user))
                 return false;
 
-            if (EntityManager.TryGetComponent(user, out BuckleComponent? buckle) && buckle.Buckled)
+            if (TryComp(user, out BuckleComponent? buckle) && buckle.BuckledTo is {} strap)
             {
-                var suid = buckle.LastEntityBuckledTo;
-                if (suid != null)
-                {
-                    // We're buckled to another object. Is that object rotatable?
-                    if (TryComp<RotatableComponent>(suid.Value, out var rotatable) && rotatable.RotateWhileAnchored)
-                    {
-                        // Note the assumption that even if unanchored, user can only do spinnychair with an "independent wheel".
-                        // (Since the user being buckled to it holds it down with their weight.)
-                        // This is logically equivalent to RotateWhileAnchored.
-                        // Barstools and office chairs have independent wheels, while regular chairs don't.
-                        _transform.SetWorldRotation(Transform(suid.Value), diffAngle);
-                        return true;
-                    }
-                }
-
-                return false;
+                // What if a person is strapped to a borg?
+                // I'm pretty sure this would allow them to be partially ratatouille'd
+
+                // We're buckled to another object. Is that object rotatable?
+                if (!TryComp<RotatableComponent>(strap, out var rotatable) || !rotatable.RotateWhileAnchored)
+                    return false;
+
+                // Note the assumption that even if unanchored, user can only do spinnychair with an "independent wheel".
+                // (Since the user being buckled to it holds it down with their weight.)
+                // This is logically equivalent to RotateWhileAnchored.
+                // Barstools and office chairs have independent wheels, while regular chairs don't.
+                _transform.SetWorldRotation(Transform(strap), diffAngle);
+                return true;
             }
 
             // user is not buckled in; apply to their transform
index 155cfede01578d1821c305f4f860461965374a4c..9ee8a064e540e4ec25964821bd286b6cadb8c416 100644 (file)
@@ -1,4 +1,5 @@
 using Content.Shared.Bed.Sleep;
+using Content.Shared.Buckle.Components;
 using Content.Shared.CombatMode.Pacification;
 using Content.Shared.Damage.ForceSay;
 using Content.Shared.Emoting;
@@ -10,15 +11,12 @@ using Content.Shared.Item;
 using Content.Shared.Mobs.Components;
 using Content.Shared.Movement.Events;
 using Content.Shared.Pointing;
-using Content.Shared.Projectiles;
 using Content.Shared.Pulling.Events;
 using Content.Shared.Speech;
 using Content.Shared.Standing;
 using Content.Shared.Strip.Components;
 using Content.Shared.Throwing;
-using Content.Shared.Weapons.Ranged.Components;
 using Robust.Shared.Physics.Components;
-using Robust.Shared.Physics.Events;
 
 namespace Content.Shared.Mobs.Systems;
 
@@ -46,6 +44,16 @@ public partial class MobStateSystem
         SubscribeLocalEvent<MobStateComponent, TryingToSleepEvent>(OnSleepAttempt);
         SubscribeLocalEvent<MobStateComponent, CombatModeShouldHandInteractEvent>(OnCombatModeShouldHandInteract);
         SubscribeLocalEvent<MobStateComponent, AttemptPacifiedAttackEvent>(OnAttemptPacifiedAttack);
+
+        SubscribeLocalEvent<MobStateComponent, UnbuckleAttemptEvent>(OnUnbuckleAttempt);
+    }
+
+    private void OnUnbuckleAttempt(Entity<MobStateComponent> ent, ref UnbuckleAttemptEvent args)
+    {
+        // TODO is this necessary?
+        // Shouldn't the interaction have already been blocked by a general interaction check?
+        if (args.User == ent.Owner && IsIncapacitated(ent))
+            args.Cancelled = true;
     }
 
     private void CheckConcious(Entity<MobStateComponent> ent, ref ConsciousAttemptEvent args)
index 29460e1dfc1db95f9d838d0dc7fd32407bb848b2..c0775b4ce2d0cbe09468abba771ad1273f74ba72 100644 (file)
@@ -1,9 +1,6 @@
 namespace Content.Shared.Movement.Pulling.Events;
 
-public sealed class PullStartedMessage : PullMessage
-{
-    public PullStartedMessage(EntityUid pullerUid, EntityUid pullableUid) :
-        base(pullerUid, pullableUid)
-    {
-    }
-}
+/// <summary>
+/// Event raised directed BOTH at the puller and pulled entity when a pull starts.
+/// </summary>
+public sealed class PullStartedMessage(EntityUid pullerUid, EntityUid pullableUid) : PullMessage(pullerUid, pullableUid);
index 47aa34562fbad340a744499a4eb68871163f47d9..6df4d1748392aff237a7dfd8517f88dbe311889e 100644 (file)
@@ -1,13 +1,6 @@
-using Robust.Shared.Physics.Components;
-
-namespace Content.Shared.Movement.Pulling.Events;
+namespace Content.Shared.Movement.Pulling.Events;
 
 /// <summary>
-/// Raised directed on both puller and pullable.
+/// Event raised directed BOTH at the puller and pulled entity when a pull starts.
 /// </summary>
-public sealed class PullStoppedMessage : PullMessage
-{
-    public PullStoppedMessage(EntityUid pullerUid, EntityUid pulledUid) : base(pullerUid, pulledUid)
-    {
-    }
-}
+public sealed class PullStoppedMessage(EntityUid pullerUid, EntityUid pulledUid) : PullMessage(pullerUid, pulledUid);
index 72b87476bb0a1d61d7456eda1037689abe6b4a80..eb2872df9c15f0cdddf1d240d3012e2df619bc1d 100644 (file)
@@ -1,4 +1,3 @@
-using System.Numerics;
 using Content.Shared.ActionBlocker;
 using Content.Shared.Administration.Logs;
 using Content.Shared.Alert;
@@ -16,11 +15,9 @@ using Content.Shared.Movement.Systems;
 using Content.Shared.Popups;
 using Content.Shared.Pulling.Events;
 using Content.Shared.Standing;
-using Content.Shared.Throwing;
 using Content.Shared.Verbs;
 using Robust.Shared.Containers;
 using Robust.Shared.Input.Binding;
-using Robust.Shared.Map;
 using Robust.Shared.Physics;
 using Robust.Shared.Physics.Components;
 using Robust.Shared.Physics.Events;
@@ -68,11 +65,26 @@ public sealed class PullingSystem : EntitySystem
         SubscribeLocalEvent<PullerComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
         SubscribeLocalEvent<PullerComponent, DropHandItemsEvent>(OnDropHandItems);
 
+        SubscribeLocalEvent<PullableComponent, StrappedEvent>(OnBuckled);
+        SubscribeLocalEvent<PullableComponent, BuckledEvent>(OnGotBuckled);
+
         CommandBinds.Builder
             .Bind(ContentKeyFunctions.ReleasePulledObject, InputCmdHandler.FromDelegate(OnReleasePulledObject, handle: false))
             .Register<PullingSystem>();
     }
 
+    private void OnBuckled(Entity<PullableComponent> ent, ref StrappedEvent args)
+    {
+        // Prevent people from pulling the entity they are buckled to
+        if (ent.Comp.Puller == args.Buckle.Owner && !args.Buckle.Comp.PullStrap)
+            StopPulling(ent, ent);
+    }
+
+    private void OnGotBuckled(Entity<PullableComponent> ent, ref BuckledEvent args)
+    {
+        StopPulling(ent, ent);
+    }
+
     private void OnAfterState(Entity<PullerComponent> ent, ref AfterAutoHandleStateEvent args)
     {
         if (ent.Comp.Pulling == null)
@@ -94,7 +106,8 @@ public sealed class PullingSystem : EntitySystem
 
     private void OnPullerContainerInsert(Entity<PullerComponent> ent, ref EntGotInsertedIntoContainerMessage args)
     {
-        if (ent.Comp.Pulling == null) return;
+        if (ent.Comp.Pulling == null)
+            return;
 
         if (!TryComp(ent.Comp.Pulling.Value, out PullableComponent? pulling))
             return;
@@ -228,8 +241,18 @@ public sealed class PullingSystem : EntitySystem
     /// </summary>
     private void StopPulling(EntityUid pullableUid, PullableComponent pullableComp)
     {
+        if (pullableComp.Puller == null)
+            return;
+
         if (!_timing.ApplyingState)
         {
+            // Joint shutdown
+            if (pullableComp.PullJointId != null)
+            {
+                _joints.RemoveJoint(pullableUid, pullableComp.PullJointId);
+                pullableComp.PullJointId = null;
+            }
+
             if (TryComp<PhysicsComponent>(pullableUid, out var pullablePhysics))
             {
                 _physics.SetFixedRotation(pullableUid, pullableComp.PrevFixedRotation, body: pullablePhysics);
@@ -330,15 +353,6 @@ public sealed class PullingSystem : EntitySystem
             return false;
         }
 
-        if (EntityManager.TryGetComponent(puller, out BuckleComponent? buckle))
-        {
-            // Prevent people pulling the chair they're on, etc.
-            if (buckle is { PullStrap: false, Buckled: true } && (buckle.LastEntityBuckledTo == pullableUid))
-            {
-                return false;
-            }
-        }
-
         var getPulled = new BeingPulledAttemptEvent(puller, pullableUid);
         RaiseLocalEvent(pullableUid, getPulled, true);
         var startPull = new StartPullAttemptEvent(puller, pullableUid);
@@ -382,11 +396,8 @@ public sealed class PullingSystem : EntitySystem
         if (!CanPull(pullerUid, pullableUid))
             return false;
 
-        if (!EntityManager.TryGetComponent<PhysicsComponent>(pullerUid, out var pullerPhysics) ||
-            !EntityManager.TryGetComponent<PhysicsComponent>(pullableUid, out var pullablePhysics))
-        {
+        if (!HasComp<PhysicsComponent>(pullerUid) || !TryComp(pullableUid, out PhysicsComponent? pullablePhysics))
             return false;
-        }
 
         // Ensure that the puller is not currently pulling anything.
         if (TryComp<PullableComponent>(pullerComp.Pulling, out var oldPullable)
@@ -431,7 +442,7 @@ public sealed class PullingSystem : EntitySystem
         {
             // Joint startup
             var union = _physics.GetHardAABB(pullerUid).Union(_physics.GetHardAABB(pullableUid, body: pullablePhysics));
-            var length = Math.Max((float) union.Size.X, (float) union.Size.Y) * 0.75f;
+            var length = Math.Max(union.Size.X, union.Size.Y) * 0.75f;
 
             var joint = _joints.CreateDistanceJoint(pullableUid, pullerUid, id: pullableComp.PullJointId);
             joint.CollideConnected = false;
@@ -475,17 +486,6 @@ public sealed class PullingSystem : EntitySystem
         if (msg.Cancelled)
             return false;
 
-        // Stop pulling confirmed!
-        if (!_timing.ApplyingState)
-        {
-            // Joint shutdown
-            if (pullable.PullJointId != null)
-            {
-                _joints.RemoveJoint(pullableUid, pullable.PullJointId);
-                pullable.PullJointId = null;
-            }
-        }
-
         StopPulling(pullableUid, pullable);
         return true;
     }
index 2fcb4fc60bbeadf4f0a6548a70d1f5a833391385..cebae8093b40156eb080d415015c1bcfb6facf20 100644 (file)
@@ -25,7 +25,7 @@ public abstract class SharedWaddleAnimationSystem : EntitySystem
         // Stop moving possibilities
         SubscribeLocalEvent((Entity<WaddleAnimationComponent> ent, ref StunnedEvent _) => StopWaddling(ent));
         SubscribeLocalEvent((Entity<WaddleAnimationComponent> ent, ref DownedEvent _) => StopWaddling(ent));
-        SubscribeLocalEvent((Entity<WaddleAnimationComponent> ent, ref BuckleChangeEvent _) => StopWaddling(ent));
+        SubscribeLocalEvent((Entity<WaddleAnimationComponent> ent, ref BuckledEvent _) => StopWaddling(ent));
         SubscribeLocalEvent<WaddleAnimationComponent, GravityChangedEvent>(OnGravityChanged);
     }
 
index 7c91366937ce158edd0be2b255c4ef00492cf2a0..929ee3a19fb79ee654bb1f9579c2a5248a7b039b 100644 (file)
@@ -17,7 +17,8 @@ public sealed class LegsParalyzedSystem : EntitySystem
     {
         SubscribeLocalEvent<LegsParalyzedComponent, ComponentStartup>(OnStartup);
         SubscribeLocalEvent<LegsParalyzedComponent, ComponentShutdown>(OnShutdown);
-        SubscribeLocalEvent<LegsParalyzedComponent, BuckleChangeEvent>(OnBuckleChange);
+        SubscribeLocalEvent<LegsParalyzedComponent, BuckledEvent>(OnBuckled);
+        SubscribeLocalEvent<LegsParalyzedComponent, UnbuckledEvent>(OnUnbuckled);
         SubscribeLocalEvent<LegsParalyzedComponent, ThrowPushbackAttemptEvent>(OnThrowPushbackAttempt);
         SubscribeLocalEvent<LegsParalyzedComponent, UpdateCanMoveEvent>(OnUpdateCanMoveEvent);
     }
@@ -34,16 +35,14 @@ public sealed class LegsParalyzedSystem : EntitySystem
         _bodySystem.UpdateMovementSpeed(uid);
     }
 
-    private void OnBuckleChange(EntityUid uid, LegsParalyzedComponent component, ref BuckleChangeEvent args)
+    private void OnBuckled(EntityUid uid, LegsParalyzedComponent component, ref BuckledEvent args)
     {
-        if (args.Buckling)
-        {
-            _standingSystem.Stand(args.BuckledEntity);
-        }
-        else
-        {
-            _standingSystem.Down(args.BuckledEntity);
-        }
+        _standingSystem.Stand(uid);
+    }
+
+    private void OnUnbuckled(EntityUid uid, LegsParalyzedComponent component, ref UnbuckledEvent args)
+    {
+        _standingSystem.Down(uid);
     }
 
     private void OnUpdateCanMoveEvent(EntityUid uid, LegsParalyzedComponent component, UpdateCanMoveEvent args)
index 0fb69b4fdbd8bbe062a4c7de418e74f847b7da05..ac5ec1e83e36724128b3663bf22b83dacfb5c20d 100644 (file)
   components:
   - type: Foldable
     folded: true
+  - type: Strap
+    enabled: False
 
 - type: entity
   name: steel bench
index 161ea25bc43aedc49dec289a94c443dfdf8fb002..b3cfe6ade3f011dcedd08097c4357a11049bbf32 100644 (file)
@@ -79,6 +79,8 @@
   components:
   - type: Foldable
     folded: true
+  - type: Strap
+    enabled: False
 
 - type: entity
   id: CheapRollerBed
   components:
   - type: Foldable
     folded: true
+  - type: Strap
+    enabled: False
 
 - type: entity
   id: EmergencyRollerBed
   components:
   - type: Foldable
     folded: true
+  - type: Strap
+    enabled: False