]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Cuffable/Handcuff ECS (#14382)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Mon, 13 Mar 2023 23:34:26 +0000 (19:34 -0400)
committerGitHub <noreply@github.com>
Mon, 13 Mar 2023 23:34:26 +0000 (19:34 -0400)
25 files changed:
Content.Client/Cuffs/Components/CuffableComponent.cs [deleted file]
Content.Client/Cuffs/Components/HandcuffComponent.cs [deleted file]
Content.Client/Cuffs/CuffableSystem.cs
Content.Client/Inventory/StrippableBoundUserInterface.cs
Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs
Content.Server/Administration/Components/DisarmProneComponent.cs [deleted file]
Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
Content.Server/Alert/Click/RemoveCuffs.cs
Content.Server/Cuffs/Components/CuffableComponent.cs [deleted file]
Content.Server/Cuffs/Components/HandcuffComponent.cs [deleted file]
Content.Server/Cuffs/CuffableSystem.cs
Content.Server/Hands/Systems/HandVirtualItemSystem.cs
Content.Server/Implants/SubdermalImplantSystem.cs
Content.Server/Objectives/Conditions/EscapeShuttleCondition.cs
Content.Server/Strip/StrippableSystem.cs
Content.Server/Weapons/Melee/MeleeWeaponSystem.cs
Content.Shared/Administration/Components/DisarmProneComponent.cs [new file with mode: 0644]
Content.Shared/Cuffs/Components/CuffableComponent.cs [new file with mode: 0644]
Content.Shared/Cuffs/Components/HandcuffComponent.cs [new file with mode: 0644]
Content.Shared/Cuffs/Components/SharedCuffableComponent.cs [deleted file]
Content.Shared/Cuffs/Components/SharedHandcuffComponent.cs [deleted file]
Content.Shared/Cuffs/SharedCuffableSystem.cs
Content.Shared/Hands/SharedHandVirtualItemSystem.cs
Resources/Locale/en-US/cuffs/components/handcuff-component.ftl
Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml

diff --git a/Content.Client/Cuffs/Components/CuffableComponent.cs b/Content.Client/Cuffs/Components/CuffableComponent.cs
deleted file mode 100644 (file)
index a519a8a..0000000
+++ /dev/null
@@ -1,67 +0,0 @@
-using Content.Shared.ActionBlocker;
-using Content.Shared.Cuffs.Components;
-using Content.Shared.Humanoid;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Utility;
-using Robust.Shared.ViewVariables;
-
-namespace Content.Client.Cuffs.Components
-{
-    [RegisterComponent]
-    [ComponentReference(typeof(SharedCuffableComponent))]
-    public sealed class CuffableComponent : SharedCuffableComponent
-    {
-        [ViewVariables]
-        private string? _currentRSI;
-
-        [Dependency] private readonly IEntityManager _entityManager = default!;
-        [Dependency] private readonly IEntitySystemManager _sysMan = default!;
-
-        public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
-        {
-            if (curState is not CuffableComponentState cuffState)
-            {
-                return;
-            }
-
-            CanStillInteract = cuffState.CanStillInteract;
-            _sysMan.GetEntitySystem<ActionBlockerSystem>().UpdateCanMove(Owner);
-
-            if (_entityManager.TryGetComponent<SpriteComponent>(Owner, out var spriteComponent))
-            {
-                spriteComponent.LayerSetVisible(HumanoidVisualLayers.Handcuffs, cuffState.NumHandsCuffed > 0);
-                spriteComponent.LayerSetColor(HumanoidVisualLayers.Handcuffs, cuffState.Color);
-
-                if (cuffState.NumHandsCuffed > 0)
-                {
-                    if (_currentRSI != cuffState.RSI) // we don't want to keep loading the same RSI
-                    {
-                        _currentRSI = cuffState.RSI;
-
-                        if (_currentRSI != null)
-                        {
-                            spriteComponent.LayerSetState(HumanoidVisualLayers.Handcuffs, new RSI.StateId(cuffState.IconState), new ResourcePath(_currentRSI));
-                        }
-                    }
-                    else
-                    {
-                        spriteComponent.LayerSetState(HumanoidVisualLayers.Handcuffs, new RSI.StateId(cuffState.IconState)); // TODO: safety check to see if RSI contains the state?
-                    }
-                }
-            }
-
-            var ev = new CuffedStateChangeEvent();
-            _entityManager.EventBus.RaiseLocalEvent(Owner, ref ev);
-        }
-
-        protected override void OnRemove()
-        {
-            base.OnRemove();
-            if (_entityManager.TryGetComponent<SpriteComponent>(Owner, out var spriteComponent))
-                spriteComponent.LayerSetVisible(HumanoidVisualLayers.Handcuffs, false);
-        }
-    }
-}
diff --git a/Content.Client/Cuffs/Components/HandcuffComponent.cs b/Content.Client/Cuffs/Components/HandcuffComponent.cs
deleted file mode 100644 (file)
index b5d5267..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-using Content.Shared.Cuffs.Components;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-
-namespace Content.Client.Cuffs.Components
-{
-    [RegisterComponent]
-    [ComponentReference(typeof(SharedHandcuffComponent))]
-    public sealed class HandcuffComponent : SharedHandcuffComponent
-    {
-        public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
-        {
-            if (curState is not HandcuffedComponentState state)
-            {
-                return;
-            }
-
-            if (state.IconState == string.Empty)
-            {
-                return;
-            }
-
-            if (IoCManager.Resolve<IEntityManager>().TryGetComponent<SpriteComponent?>(Owner, out var sprite))
-            {
-                sprite.LayerSetState(0, new RSI.StateId(state.IconState)); // TODO: safety check to see if RSI contains the state?
-            }
-        }
-    }
-}
index a2fa91c1e876f417b154c4264a6bc5feadd687bc..1109d3785f417f429cd802f2f2a47697d8eae2bf 100644 (file)
@@ -1,9 +1,79 @@
+using Content.Shared.ActionBlocker;
 using Content.Shared.Cuffs;
+using Content.Shared.Cuffs.Components;
+using Content.Shared.Humanoid;
+using Robust.Client.GameObjects;
+using Robust.Shared.GameStates;
 
-namespace Content.Client.Cuffs
+namespace Content.Client.Cuffs;
+
+public sealed class CuffableSystem : SharedCuffableSystem
 {
-    public sealed class CuffableSystem : SharedCuffableSystem
+    [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<CuffableComponent, ComponentShutdown>(OnShutdown);
+        SubscribeLocalEvent<CuffableComponent, ComponentHandleState>(OnCuffableHandleState);
+        SubscribeLocalEvent<HandcuffComponent, ComponentHandleState>(OnHandcuffHandleState);
+    }
+
+    private void OnShutdown(EntityUid uid, CuffableComponent component, ComponentShutdown args)
+    {
+        if (TryComp<SpriteComponent>(uid, out var sprite))
+            sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, false);
+    }
+
+    private void OnHandcuffHandleState(EntityUid uid, HandcuffComponent component, ref ComponentHandleState args)
+    {
+        if (args.Current is not HandcuffComponentState state)
+            return;
+
+        component.Cuffing = state.Cuffing;
+
+        if (state.IconState == string.Empty)
+            return;
+
+        if (TryComp<SpriteComponent>(uid, out var sprite))
+        {
+            sprite.LayerSetState(HumanoidVisualLayers.Handcuffs, state.IconState);
+        }
+    }
+
+    private void OnCuffableHandleState(EntityUid uid, CuffableComponent component, ref ComponentHandleState args)
     {
+        if (args.Current is not CuffableComponentState cuffState)
+            return;
 
+        component.CanStillInteract = cuffState.CanStillInteract;
+        component.Uncuffing = cuffState.Uncuffing;
+        _actionBlocker.UpdateCanMove(uid);
+
+        var ev = new CuffedStateChangeEvent();
+        RaiseLocalEvent(uid, ref ev);
+
+        if (!TryComp<SpriteComponent>(uid, out var sprite))
+            return;
+        var cuffed = cuffState.NumHandsCuffed > 0;
+        sprite.LayerSetVisible(HumanoidVisualLayers.Handcuffs, cuffed);
+
+        // if they are not cuffed, that means that we didn't get a valid color,
+        // iconstate, or RSI. that also means we don't need to update the sprites.
+        if (!cuffed)
+            return;
+        sprite.LayerSetColor(HumanoidVisualLayers.Handcuffs, cuffState.Color!.Value);
+
+        if (!Equals(component.CurrentRSI, cuffState.RSI) && cuffState.RSI != null) // we don't want to keep loading the same RSI
+        {
+            component.CurrentRSI = cuffState.RSI;
+            sprite.LayerSetState(HumanoidVisualLayers.Handcuffs, cuffState.IconState, component.CurrentRSI);
+        }
+        else
+        {
+            sprite.LayerSetState(HumanoidVisualLayers.Handcuffs, cuffState.IconState);
+        }
     }
 }
+
index 83f624c12b2158a4cdbd7cddcb514aff47e5e405..ae6899ee0a7b2175ea014ea34fa00da64e48b34b 100644 (file)
@@ -1,4 +1,5 @@
-using Content.Client.Cuffs.Components;
+using System.Linq;
+using Content.Client.Cuffs;
 using Content.Client.Examine;
 using Content.Client.Hands;
 using Content.Client.Strip;
@@ -7,6 +8,8 @@ using Content.Client.UserInterface.Controls;
 using Content.Client.UserInterface.Systems.Hands.Controls;
 using Content.Client.Verbs;
 using Content.Client.Verbs.UI;
+using Content.Shared.Cuffs;
+using Content.Shared.Cuffs.Components;
 using Content.Shared.Ensnaring.Components;
 using Content.Shared.Hands.Components;
 using Content.Shared.IdentityManagement;
@@ -16,7 +19,6 @@ using Content.Shared.Strip.Components;
 using Content.Shared.Verbs;
 using JetBrains.Annotations;
 using Robust.Client.GameObjects;
-using Robust.Client.ResourceManagement;
 using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controls;
 using Robust.Shared.Input;
@@ -32,12 +34,13 @@ namespace Content.Client.Inventory
     public sealed class StrippableBoundUserInterface : BoundUserInterface
     {
         private const int ButtonSeparation = 4;
-        
+
         [Dependency] private readonly IPrototypeManager _protoMan = default!;
         [Dependency] private readonly IEntityManager _entMan = default!;
         [Dependency] private readonly IUserInterfaceManager _ui = default!;
         private ExamineSystem _examine = default!;
         private InventorySystem _inv = default!;
+        private readonly SharedCuffableSystem _cuffable;
 
         [ViewVariables]
         private StrippingMenu? _strippingMenu;
@@ -50,6 +53,7 @@ namespace Content.Client.Inventory
             IoCManager.InjectDependencies(this);
             _examine = _entMan.EntitySysManager.GetEntitySystem<ExamineSystem>();
             _inv = _entMan.EntitySysManager.GetEntitySystem<InventorySystem>();
+            _cuffable = _entMan.System<SharedCuffableSystem>();
             var title = Loc.GetString("strippable-bound-user-interface-stripping-menu-title", ("ownerName", Identity.Name(Owner.Owner, _entMan)));
             _strippingMenu = new StrippingMenu(title, this);
             _strippingMenu.OnClose += Close;
@@ -98,7 +102,7 @@ namespace Content.Client.Inventory
             if (_entMan.TryGetComponent(Owner.Owner, out HandsComponent? handsComp))
             {
                 // good ol hands shit code. there is a GuiHands comparer that does the same thing... but these are hands
-                // and not gui hands... which are different... 
+                // and not gui hands... which are different...
                 foreach (var hand in handsComp.Hands.Values)
                 {
                     if (hand.Location != HandLocation.Right)
@@ -159,10 +163,10 @@ namespace Content.Client.Inventory
             if (_entMan.TryGetComponent(hand.HeldEntity, out HandVirtualItemComponent? virt))
             {
                 button.Blocked = true;
-                if (_entMan.TryGetComponent(Owner.Owner, out CuffableComponent? cuff) && cuff.Container.Contains(virt.BlockingEntity))
+                if (_entMan.TryGetComponent(Owner.Owner, out CuffableComponent? cuff) && _cuffable.GetAllCuffs(cuff).Contains(virt.BlockingEntity))
                     button.BlockedRect.MouseFilter = MouseFilterMode.Ignore;
             }
-            
+
             UpdateEntityIcon(button, hand.HeldEntity);
             _strippingMenu!.HandsContainer.AddChild(button);
         }
index 5c1d23c18836b2f6a983aa013a16978c5bf7621d..b1135e1f6202a9e47512c1ad7d9ec87993d1aa5a 100644 (file)
@@ -1,9 +1,9 @@
 #nullable enable
-using System.Linq;
 using System.Threading.Tasks;
-using Content.Server.Cuffs.Components;
+using Content.Server.Cuffs;
 using Content.Server.Hands.Components;
 using Content.Shared.Body.Components;
+using Content.Shared.Cuffs.Components;
 using NUnit.Framework;
 using Robust.Server.Console;
 using Robust.Shared.GameObjects;
@@ -56,6 +56,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking
                 var coordinates = new MapCoordinates(Vector2.Zero, mapId);
 
                 var entityManager = IoCManager.Resolve<IEntityManager>();
+                var cuffableSys = entityManager.System<CuffableSystem>();
 
                 // Spawn the entities
                 human = entityManager.SpawnEntity("HumanDummy", coordinates);
@@ -75,19 +76,19 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking
                 Assert.True(entityManager.TryGetComponent(secondCuffs, out HandcuffComponent? _), $"Second handcuffs has no {nameof(HandcuffComponent)}");
 
                 // Test to ensure cuffed players register the handcuffs
-                cuffed.TryAddNewCuffs(human, cuffs);
+                cuffableSys.TryAddNewCuffs(human, human, cuffs, cuffed);
                 Assert.True(cuffed.CuffedHandCount > 0,
                     "Handcuffing a player did not result in their hands being cuffed");
 
                 // Test to ensure a player with 4 hands will still only have 2 hands cuffed
-                AddHand(cuffed.Owner);
-                AddHand(cuffed.Owner);
+                AddHand(human);
+                AddHand(human);
 
                 Assert.That(cuffed.CuffedHandCount, Is.EqualTo(2));
-                Assert.That(hands.SortedHands.Count(), Is.EqualTo(4));
+                Assert.That(hands.SortedHands.Count, Is.EqualTo(4));
 
                 // Test to give a player with 4 hands 2 sets of cuffs
-                cuffed.TryAddNewCuffs(human, secondCuffs);
+                cuffableSys.TryAddNewCuffs(human, human, secondCuffs, cuffed);
                 Assert.True(cuffed.CuffedHandCount == 4, "Player doesn't have correct amount of hands cuffed");
             });
 
diff --git a/Content.Server/Administration/Components/DisarmProneComponent.cs b/Content.Server/Administration/Components/DisarmProneComponent.cs
deleted file mode 100644 (file)
index d20a349..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Content.Server.Administration.Components;
-
-/// <summary>
-/// This is used for forcing someone to be disarmed 100% of the time.
-/// </summary>
-[RegisterComponent]
-public sealed class DisarmProneComponent : Component { }
index b44af241c9dc7dea0f9843ad78f54ab9607630be..219b7a86d620df80f21429b77ff9ca2a407c2679 100644 (file)
@@ -24,6 +24,7 @@ using Content.Server.Tabletop;
 using Content.Server.Tabletop.Components;
 using Content.Server.Tools.Systems;
 using Content.Shared.Administration;
+using Content.Shared.Administration.Components;
 using Content.Shared.Body.Components;
 using Content.Shared.Body.Part;
 using Content.Shared.Clothing.Components;
index de8b4455be8d76aace754d1bfe92806f5d528a73..f46ba3c106e4fb45def8bcdd4e65383b8f4459d0 100644 (file)
@@ -1,4 +1,4 @@
-using Content.Server.Cuffs.Components;
+using Content.Server.Cuffs;
 using Content.Shared.Alert;
 using JetBrains.Annotations;
 
@@ -13,10 +13,9 @@ namespace Content.Server.Alert.Click
     {
         public void AlertClicked(EntityUid player)
         {
-            if (IoCManager.Resolve<IEntityManager>().TryGetComponent(player, out CuffableComponent? cuffableComponent))
-            {
-                cuffableComponent.TryUncuff(player);
-            }
+            var entityManager = IoCManager.Resolve<IEntityManager>();
+            var cuffableSys = entityManager.System<CuffableSystem>();
+            cuffableSys.TryUncuff(player, player);
         }
     }
 }
diff --git a/Content.Server/Cuffs/Components/CuffableComponent.cs b/Content.Server/Cuffs/Components/CuffableComponent.cs
deleted file mode 100644 (file)
index 8d36b33..0000000
+++ /dev/null
@@ -1,295 +0,0 @@
-using System.Linq;
-using Content.Server.Administration.Logs;
-using Content.Server.DoAfter;
-using Content.Server.Hands.Components;
-using Content.Server.Hands.Systems;
-using Content.Shared.ActionBlocker;
-using Content.Shared.Alert;
-using Content.Shared.Cuffs.Components;
-using Content.Shared.Hands.Components;
-using Content.Shared.Database;
-using Content.Shared.Hands.EntitySystems;
-using Content.Shared.Interaction;
-using Content.Shared.Interaction.Components;
-using Content.Shared.Popups;
-using Robust.Server.Containers;
-using Robust.Server.GameObjects;
-using Robust.Shared.Audio;
-using Robust.Shared.Containers;
-using Robust.Shared.Player;
-using Content.Server.Recycling.Components;
-using Content.Shared.DoAfter;
-using Robust.Shared.Map;
-
-namespace Content.Server.Cuffs.Components
-{
-    [RegisterComponent]
-    [ComponentReference(typeof(SharedCuffableComponent))]
-    public sealed class CuffableComponent : SharedCuffableComponent
-    {
-        [Dependency] private readonly IEntityManager _entMan = default!;
-        [Dependency] private readonly IEntitySystemManager _sysMan = default!;
-        [Dependency] private readonly IComponentFactory _componentFactory = default!;
-        [Dependency] private readonly IAdminLogManager _adminLogger = default!;
-
-        private bool _uncuffing;
-
-        protected override void Initialize()
-        {
-            base.Initialize();
-
-            Owner.EnsureComponentWarn<HandsComponent>();
-        }
-
-        public override ComponentState GetComponentState()
-        {
-            // there are 2 approaches i can think of to handle the handcuff overlay on players
-            // 1 - make the current RSI the handcuff type that's currently active. all handcuffs on the player will appear the same.
-            // 2 - allow for several different player overlays for each different cuff type.
-            // approach #2 would be more difficult/time consuming to do and the payoff doesn't make it worth it.
-            // right now we're doing approach #1.
-
-            if (CuffedHandCount > 0)
-            {
-                if (_entMan.TryGetComponent<HandcuffComponent?>(LastAddedCuffs, out var cuffs))
-                {
-                    return new CuffableComponentState(CuffedHandCount,
-                       CanStillInteract,
-                       cuffs.CuffedRSI,
-                       $"{cuffs.OverlayIconState}-{CuffedHandCount}",
-                       cuffs.Color);
-                    // the iconstate is formatted as blah-2, blah-4, blah-6, etc.
-                    // the number corresponds to how many hands are cuffed.
-                }
-            }
-
-            return new CuffableComponentState(CuffedHandCount,
-               CanStillInteract,
-               "/Objects/Misc/handcuffs.rsi",
-               "body-overlay-2",
-               Color.White);
-        }
-
-        /// <summary>
-        /// Add a set of cuffs to an existing CuffedComponent.
-        /// </summary>
-        public bool TryAddNewCuffs(EntityUid user, EntityUid handcuff)
-        {
-            if (!_entMan.HasComponent<HandcuffComponent>(handcuff))
-            {
-                Logger.Warning($"Handcuffs being applied to player are missing a {nameof(HandcuffComponent)}!");
-                return false;
-            }
-
-            if (!EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(handcuff, Owner))
-            {
-                Logger.Warning("Handcuffs being applied to player are obstructed or too far away! This should not happen!");
-                return true;
-            }
-
-            var sys = _entMan.EntitySysManager.GetEntitySystem<SharedHandsSystem>();
-
-            // Success!
-            sys.TryDrop(user, handcuff);
-
-            Container.Insert(handcuff);
-            UpdateHeldItems(handcuff);
-            return true;
-        }
-
-        /// <summary>
-        ///     Adds virtual cuff items to the user's hands.
-        /// </summary>
-        public void UpdateHeldItems(EntityUid handcuff)
-        {
-            // TODO when ecs-ing this, we probably don't just want to use the generic virtual-item entity, and instead
-            // want to add our own item, so that use-in-hand triggers an uncuff attempt and the like.
-
-            if (!_entMan.TryGetComponent(Owner, out HandsComponent? handsComponent)) return;
-
-            var handSys = _entMan.EntitySysManager.GetEntitySystem<SharedHandsSystem>();
-
-            var freeHands = 0;
-            foreach (var hand in handSys.EnumerateHands(Owner, handsComponent))
-            {
-                if (hand.HeldEntity == null)
-                {
-                    freeHands++;
-                    continue;
-                }
-
-                // Is this entity removable? (it might be an existing handcuff blocker)
-                if (_entMan.HasComponent<UnremoveableComponent>(hand.HeldEntity))
-                    continue;
-
-                handSys.DoDrop(Owner, hand, true, handsComponent);
-                freeHands++;
-                if (freeHands == 2)
-                    break;
-            }
-
-            var virtSys = _entMan.EntitySysManager.GetEntitySystem<HandVirtualItemSystem>();
-
-            if (virtSys.TrySpawnVirtualItemInHand(handcuff, Owner, out var virtItem1))
-                _entMan.EnsureComponent<UnremoveableComponent>(virtItem1.Value);
-
-            if (virtSys.TrySpawnVirtualItemInHand(handcuff, Owner, out var virtItem2))
-                _entMan.EnsureComponent<UnremoveableComponent>(virtItem2.Value);
-        }
-
-        /// <summary>
-        /// Updates the status effect indicator on the HUD.
-        /// </summary>
-        private void UpdateAlert()
-        {
-            if (CanStillInteract)
-            {
-                EntitySystem.Get<AlertsSystem>().ClearAlert(Owner, AlertType.Handcuffed);
-            }
-            else
-            {
-                EntitySystem.Get<AlertsSystem>().ShowAlert(Owner, AlertType.Handcuffed);
-            }
-        }
-
-        /// <summary>
-        /// Attempt to uncuff a cuffed entity. Can be called by the cuffed entity, or another entity trying to help uncuff them.
-        /// If the uncuffing succeeds, the cuffs will drop on the floor.
-        /// </summary>
-        /// <param name="user">The cuffed entity</param>
-        /// <param name="cuffsToRemove">Optional param for the handcuff entity to remove from the cuffed entity. If null, uses the most recently added handcuff entity.</param>
-        public async void TryUncuff(EntityUid user, EntityUid? cuffsToRemove = null)
-        {
-            if (_uncuffing) return;
-
-            var isOwner = user == Owner;
-
-            if (cuffsToRemove == null)
-            {
-                if (Container.ContainedEntities.Count == 0)
-                {
-                    return;
-                }
-
-                cuffsToRemove = LastAddedCuffs;
-            }
-            else
-            {
-                if (!Container.ContainedEntities.Contains(cuffsToRemove.Value))
-                {
-                    Logger.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!");
-                }
-            }
-
-            if (!_entMan.TryGetComponent<HandcuffComponent?>(cuffsToRemove, out var cuff))
-            {
-                Logger.Warning($"A user is trying to remove handcuffs without a {nameof(HandcuffComponent)}. This should never happen!");
-                return;
-            }
-
-            var attempt = new UncuffAttemptEvent(user, Owner);
-            _entMan.EventBus.RaiseLocalEvent(user, attempt, true);
-
-            if (attempt.Cancelled)
-            {
-                return;
-            }
-
-            if (!isOwner && !EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(user, Owner))
-            {
-                user.PopupMessage(Loc.GetString("cuffable-component-cannot-remove-cuffs-too-far-message"));
-                return;
-            }
-
-            user.PopupMessage(Loc.GetString("cuffable-component-start-removing-cuffs-message"));
-
-            if (isOwner)
-            {
-                SoundSystem.Play(cuff.StartBreakoutSound.GetSound(), Filter.Pvs(Owner, entityManager: _entMan), Owner);
-            }
-            else
-            {
-                SoundSystem.Play(cuff.StartUncuffSound.GetSound(), Filter.Pvs(Owner, entityManager: _entMan), Owner);
-            }
-
-            var uncuffTime = isOwner ? cuff.BreakoutTime : cuff.UncuffTime;
-            var doAfterEventArgs = new DoAfterEventArgs(user, uncuffTime, target: Owner)
-            {
-                BreakOnUserMove = true,
-                BreakOnTargetMove = true,
-                BreakOnDamage = true,
-                BreakOnStun = true,
-                NeedHand = true
-            };
-
-            var doAfterSystem = EntitySystem.Get<DoAfterSystem>();
-            _uncuffing = true;
-
-            var result = await doAfterSystem.WaitDoAfter(doAfterEventArgs);
-
-            _uncuffing = false;
-
-            if (result != DoAfterStatus.Cancelled)
-            {
-                Uncuff(user, cuffsToRemove.Value, cuff, isOwner);
-            }
-            else
-            {
-                user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-fail-message"));
-            }
-        }
-
-        //Lord forgive me for putting this here
-        //Cuff ECS when
-        public void Uncuff(EntityUid user, EntityUid cuffsToRemove, HandcuffComponent cuff, bool isOwner)
-        {
-            SoundSystem.Play(cuff.EndUncuffSound.GetSound(), Filter.Pvs(Owner), Owner);
-
-            _entMan.EntitySysManager.GetEntitySystem<HandVirtualItemSystem>().DeleteInHandsMatching(user, cuffsToRemove);
-            Container.Remove(cuffsToRemove);
-
-            if (cuff.BreakOnRemove)
-            {
-                _entMan.QueueDeleteEntity(cuffsToRemove);
-                var trash = _entMan.SpawnEntity(cuff.BrokenPrototype, MapCoordinates.Nullspace);
-                _entMan.EntitySysManager.GetEntitySystem<SharedHandsSystem>().PickupOrDrop(user, trash);
-            }
-            else
-            {
-                _entMan.EntitySysManager.GetEntitySystem<SharedHandsSystem>().PickupOrDrop(user, cuffsToRemove);
-            }
-
-            if (CuffedHandCount == 0)
-            {
-                user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-success-message"));
-
-                if (!isOwner)
-                {
-                    user.PopupMessage(Owner, Loc.GetString("cuffable-component-remove-cuffs-by-other-success-message", ("otherName", user)));
-                }
-
-                if (user == Owner)
-                {
-                    _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entMan.ToPrettyString(user):player} has successfully uncuffed themselves");
-                }
-                else
-                {
-                    _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entMan.ToPrettyString(user):player} has successfully uncuffed {_entMan.ToPrettyString(Owner):player}");
-                }
-
-            }
-            else
-            {
-                if (!isOwner)
-                {
-                    user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message", ("cuffedHandCount", CuffedHandCount), ("otherName", user)));
-                    user.PopupMessage(Owner, Loc.GetString("cuffable-component-remove-cuffs-by-other-partial-success-message", ("otherName", user), ("cuffedHandCount", CuffedHandCount)));
-                }
-                else
-                {
-                    user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message", ("cuffedHandCount", CuffedHandCount)));
-                }
-            }
-        }
-    }
-}
diff --git a/Content.Server/Cuffs/Components/HandcuffComponent.cs b/Content.Server/Cuffs/Components/HandcuffComponent.cs
deleted file mode 100644 (file)
index e8dda6c..0000000
+++ /dev/null
@@ -1,157 +0,0 @@
-using Content.Server.Administration.Components;
-using Content.Server.Administration.Logs;
-using Content.Server.DoAfter;
-using Content.Shared.Cuffs.Components;
-using Content.Shared.Database;
-using Content.Shared.DoAfter;
-using Content.Shared.Popups;
-using Content.Shared.Stunnable;
-using Robust.Shared.Audio;
-using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
-namespace Content.Server.Cuffs.Components
-{
-    [RegisterComponent]
-    [ComponentReference(typeof(SharedHandcuffComponent))]
-    public sealed class HandcuffComponent : SharedHandcuffComponent
-    {
-        [Dependency] private readonly IEntityManager _entities = default!;
-        [Dependency] private readonly IAdminLogManager _adminLogger = default!;
-
-        /// <summary>
-        ///     The time it takes to apply a <see cref="CuffedComponent"/> to an entity.
-        /// </summary>
-        [DataField("cuffTime")]
-        public float CuffTime { get; set; } = 3.5f;
-
-        /// <summary>
-        ///     The time it takes to remove a <see cref="CuffedComponent"/> from an entity.
-        /// </summary>
-        [DataField("uncuffTime")]
-        public float UncuffTime { get; set; } = 3.5f;
-
-        /// <summary>
-        ///     The time it takes for a cuffed entity to remove <see cref="CuffedComponent"/> from itself.
-        /// </summary>
-        [DataField("breakoutTime")]
-        public float BreakoutTime { get; set; } = 30f;
-
-        /// <summary>
-        ///     If an entity being cuffed is stunned, this amount of time is subtracted from the time it takes to add/remove their cuffs.
-        /// </summary>
-        [DataField("stunBonus")]
-        public float StunBonus { get; set; } = 2f;
-
-        /// <summary>
-        ///     Will the cuffs break when removed?
-        /// </summary>
-        [DataField("breakOnRemove")]
-        public bool BreakOnRemove { get; set; }
-
-        /// <summary>
-        ///     Will the cuffs break when removed?
-        /// </summary>
-        [DataField("brokenPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
-        public string? BrokenPrototype { get; set; }
-
-        /// <summary>
-        ///     The path of the RSI file used for the player cuffed overlay.
-        /// </summary>
-        [DataField("cuffedRSI")]
-        public string? CuffedRSI { get; set; } = "Objects/Misc/handcuffs.rsi";
-
-        /// <summary>
-        ///     The iconstate used with the RSI file for the player cuffed overlay.
-        /// </summary>
-        [DataField("bodyIconState")]
-        public string? OverlayIconState { get; set; } = "body-overlay";
-
-        [DataField("startCuffSound")]
-        public SoundSpecifier StartCuffSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_start.ogg");
-
-        [DataField("endCuffSound")]
-        public SoundSpecifier EndCuffSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_end.ogg");
-
-        [DataField("startBreakoutSound")]
-        public SoundSpecifier StartBreakoutSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_breakout_start.ogg");
-
-        [DataField("startUncuffSound")]
-        public SoundSpecifier StartUncuffSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_takeoff_start.ogg");
-
-        [DataField("endUncuffSound")]
-        public SoundSpecifier EndUncuffSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_takeoff_end.ogg");
-        [DataField("color")]
-        public Color Color { get; set; } = Color.White;
-
-        /// <summary>
-        ///     Used to prevent DoAfter getting spammed.
-        /// </summary>
-        public bool Cuffing;
-
-        /// <summary>
-        /// Update the cuffed state of an entity
-        /// </summary>
-        public async void TryUpdateCuff(EntityUid user, EntityUid target, CuffableComponent cuffs)
-        {
-            var cuffTime = CuffTime;
-
-            if (_entities.HasComponent<StunnedComponent>(target))
-            {
-                cuffTime = MathF.Max(0.1f, cuffTime - StunBonus);
-            }
-
-            if (_entities.HasComponent<DisarmProneComponent>(target))
-                cuffTime = 0.0f; // cuff them instantly.
-
-            var doAfterEventArgs = new DoAfterEventArgs(user, cuffTime, default, target)
-            {
-                BreakOnTargetMove = true,
-                BreakOnUserMove = true,
-                BreakOnDamage = true,
-                BreakOnStun = true,
-                NeedHand = true
-            };
-
-            Cuffing = true;
-
-            var result = await EntitySystem.Get<DoAfterSystem>().WaitDoAfter(doAfterEventArgs);
-
-            Cuffing = false;
-
-            // TODO these pop-ups need third-person variants (i.e. {$user} is cuffing {$target}!
-
-            if (result != DoAfterStatus.Cancelled)
-            {
-                if (cuffs.TryAddNewCuffs(user, Owner))
-                {
-                    SoundSystem.Play(EndCuffSound.GetSound(), Filter.Pvs(Owner), Owner);
-                    if (target == user)
-                    {
-                        user.PopupMessage(Loc.GetString("handcuff-component-cuff-self-success-message"));
-                        _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entities.ToPrettyString(user):player} has cuffed himself");
-                    }
-                    else
-                    {
-                        user.PopupMessage(Loc.GetString("handcuff-component-cuff-other-success-message",("otherName", target)));
-                        target.PopupMessage(Loc.GetString("handcuff-component-cuff-by-other-success-message", ("otherName", user)));
-                        _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{_entities.ToPrettyString(user):player} has cuffed {_entities.ToPrettyString(target):player}");
-                    }
-                }
-            }
-            else
-            {
-                if (target == user)
-                {
-                    user.PopupMessage(Loc.GetString("handcuff-component-cuff-interrupt-self-message"));
-                }
-                else
-                {
-                    user.PopupMessage(Loc.GetString("handcuff-component-cuff-interrupt-message",("targetName", target)));
-                    target.PopupMessage(Loc.GetString("handcuff-component-cuff-interrupt-other-message",("otherName", user)));
-                }
-            }
-        }
-    }
-}
index efb525102dfb01aa6d1deebcc818017f3c8cf637..3891941caded667a108f756da81ed16dc7de1b8c 100644 (file)
-using System.Linq;
-using Content.Server.Cuffs.Components;
-using Content.Server.Hands.Components;
-using Content.Shared.ActionBlocker;
 using Content.Shared.Cuffs;
-using Content.Shared.Hands;
-using Content.Shared.Popups;
-using Content.Shared.Verbs;
-using Content.Shared.Weapons.Melee.Events;
 using JetBrains.Annotations;
-using Robust.Shared.Player;
-using Content.Shared.Interaction;
-using Robust.Shared.Audio;
-using Robust.Shared.Containers;
-using Content.Server.Hands.Systems;
-using Content.Shared.Mobs.Systems;
+using Content.Shared.Cuffs.Components;
+using Robust.Shared.GameStates;
 
 namespace Content.Server.Cuffs
 {
     [UsedImplicitly]
     public sealed class CuffableSystem : SharedCuffableSystem
     {
-        [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
-        [Dependency] private readonly HandVirtualItemSystem _virtualSystem = default!;
-        [Dependency] private readonly SharedAudioSystem _audio = default!;
-        [Dependency] private readonly MobStateSystem _mobState = default!;
-        [Dependency] private readonly SharedPopupSystem _popup = default!;
-
         public override void Initialize()
         {
             base.Initialize();
 
-            SubscribeLocalEvent<HandCountChangedEvent>(OnHandCountChanged);
-            SubscribeLocalEvent<UncuffAttemptEvent>(OnUncuffAttempt);
-            SubscribeLocalEvent<CuffableComponent, GetVerbsEvent<Verb>>(AddUncuffVerb);
-            SubscribeLocalEvent<HandcuffComponent, AfterInteractEvent>(OnCuffAfterInteract);
-            SubscribeLocalEvent<HandcuffComponent, MeleeHitEvent>(OnCuffMeleeHit);
-            SubscribeLocalEvent<CuffableComponent, EntRemovedFromContainerMessage>(OnCuffsRemoved);
-        }
-
-        private void OnCuffsRemoved(EntityUid uid, CuffableComponent component, EntRemovedFromContainerMessage args)
-        {
-            if (args.Container.ID == component.Container.ID)
-                _virtualSystem.DeleteInHandsMatching(uid, args.Entity);
-        }
-
-        private void AddUncuffVerb(EntityUid uid, CuffableComponent component, GetVerbsEvent<Verb> args)
-        {
-            // Can the user access the cuffs, and is there even anything to uncuff?
-            if (!args.CanAccess || component.CuffedHandCount == 0 || args.Hands == null)
-                return;
-
-            // We only check can interact if the user is not uncuffing themselves. As a result, the verb will show up
-            // when the user is incapacitated & trying to uncuff themselves, but TryUncuff() will still fail when
-            // attempted.
-            if (args.User != args.Target && !args.CanInteract)
-                return;
-
-            Verb verb = new()
-            {
-                Act = () => component.TryUncuff(args.User),
-                DoContactInteraction = true,
-                Text = Loc.GetString("uncuff-verb-get-data-text")
-            };
-            //TODO VERB ICON add uncuffing symbol? may re-use the alert symbol showing that you are currently cuffed?
-            args.Verbs.Add(verb);
-        }
-
-        private void OnCuffAfterInteract(EntityUid uid, HandcuffComponent component, AfterInteractEvent args)
-        {
-            if (args.Target is not {Valid: true} target)
-                return;
-
-            if (!args.CanReach)
-            {
-                _popup.PopupEntity(Loc.GetString("handcuff-component-too-far-away-error"), args.User, args.User);
-                return;
-            }
-
-            TryCuffing(uid, args.User, args.Target.Value, component);
-            args.Handled = true;
+            SubscribeLocalEvent<HandcuffComponent, ComponentGetState>(OnHandcuffGetState);
+            SubscribeLocalEvent<CuffableComponent, ComponentGetState>(OnCuffableGetState);
         }
 
-        private void TryCuffing(EntityUid handcuff, EntityUid user, EntityUid target, HandcuffComponent component)
+        private void OnHandcuffGetState(EntityUid uid, HandcuffComponent component, ref ComponentGetState args)
         {
-            if (component.Cuffing || !EntityManager.TryGetComponent<CuffableComponent>(target, out var cuffed))
-                return;
-
-            if (!EntityManager.TryGetComponent<HandsComponent?>(target, out var hands))
-            {
-                _popup.PopupEntity(Loc.GetString("handcuff-component-target-has-no-hands-error",("targetName", target)), user, user);
-                return;
-            }
-
-            if (cuffed.CuffedHandCount >= hands.Count)
-            {
-                _popup.PopupEntity(Loc.GetString("handcuff-component-target-has-no-free-hands-error",("targetName", target)), user, user);
-                return;
-            }
-
-            // TODO these messages really need third-party variants. I.e., "{$user} starts cuffing {$target}!"
-            if (target == user)
-            {
-                _popup.PopupEntity(Loc.GetString("handcuff-component-target-self"), user, user);
-            }
-            else
-            {
-                _popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-target-message",("targetName", target)), user, user);
-                _popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-by-other-message",("otherName", user)), target, target);
-            }
-
-            _audio.PlayPvs(component.StartCuffSound, handcuff);
-
-            component.TryUpdateCuff(user, target, cuffed);
+            args.State = new HandcuffComponentState(component.OverlayIconState, component.Cuffing);
         }
 
-        private void OnCuffMeleeHit(EntityUid uid, HandcuffComponent component, MeleeHitEvent args)
-        {
-            if (!args.HitEntities.Any())
-                return;
-
-            TryCuffing(uid, args.User, args.HitEntities.First(), component);
-            args.Handled = true;
-        }
-
-
-        private void OnUncuffAttempt(UncuffAttemptEvent args)
-        {
-            if (args.Cancelled)
-            {
-                return;
-            }
-            if (!EntityManager.EntityExists(args.User))
-            {
-                // Should this even be possible?
-                args.Cancel();
-                return;
-            }
-            // If the user is the target, special logic applies.
-            // This is because the CanInteract blocking of the cuffs prevents self-uncuff.
-            if (args.User == args.Target)
-            {
-                // This UncuffAttemptEvent check should probably be In MobStateSystem, not here?
-                if (_mobState.IsIncapacitated(args.User))
-                {
-                    args.Cancel();
-                }
-                else
-                {
-                    // TODO Find a way for cuffable to check ActionBlockerSystem.CanInteract() without blocking itself
-                }
-            }
-            else
-            {
-                // Check if the user can interact.
-                if (!_actionBlockerSystem.CanInteract(args.User, args.Target))
-                {
-                    args.Cancel();
-                }
-            }
-            if (args.Cancelled)
-            {
-                _popup.PopupEntity(Loc.GetString("cuffable-component-cannot-interact-message"), args.Target, args.User);
-            }
-        }
-
-        /// <summary>
-        ///     Check the current amount of hands the owner has, and if there's less hands than active cuffs we remove some cuffs.
-        /// </summary>
-        private void OnHandCountChanged(HandCountChangedEvent message)
-        {
-            var owner = message.Sender;
-
-            if (!EntityManager.TryGetComponent(owner, out CuffableComponent? cuffable) ||
-                !cuffable.Initialized)
-            {
-                return;
-            }
-
-            var dirty = false;
-            var handCount = EntityManager.GetComponentOrNull<HandsComponent>(owner)?.Count ?? 0;
-
-            while (cuffable.CuffedHandCount > handCount && cuffable.CuffedHandCount > 0)
-            {
-                dirty = true;
-
-                var container = cuffable.Container;
-                var entity = container.ContainedEntities[^1];
-
-                container.Remove(entity);
-                EntityManager.GetComponent<TransformComponent>(entity).WorldPosition = EntityManager.GetComponent<TransformComponent>(owner).WorldPosition;
-            }
-
-            if (dirty)
-            {
-                UpdateCuffState(owner, cuffable);
-            }
-        }
-    }
-
-    /// <summary>
-    /// Event fired on the User when the User attempts to cuff the Target.
-    /// Should generate popups on the User.
-    /// </summary>
-    public sealed class UncuffAttemptEvent : CancellableEntityEventArgs
-    {
-        public readonly EntityUid User;
-        public readonly EntityUid Target;
-
-        public UncuffAttemptEvent(EntityUid user, EntityUid target)
+        private void OnCuffableGetState(EntityUid uid, CuffableComponent component, ref ComponentGetState args)
         {
-            User = user;
-            Target = target;
+            // there are 2 approaches i can think of to handle the handcuff overlay on players
+            // 1 - make the current RSI the handcuff type that's currently active. all handcuffs on the player will appear the same.
+            // 2 - allow for several different player overlays for each different cuff type.
+            // approach #2 would be more difficult/time consuming to do and the payoff doesn't make it worth it.
+            // right now we're doing approach #1.
+            HandcuffComponent? cuffs = null;
+            if (component.CuffedHandCount > 0)
+                TryComp(component.LastAddedCuffs, out cuffs);
+            args.State = new CuffableComponentState(component.CuffedHandCount,
+                component.CanStillInteract,
+                component.Uncuffing,
+                cuffs?.CuffedRSI,
+                $"{cuffs?.OverlayIconState}-{component.CuffedHandCount}",
+                cuffs?.Color);
+            // the iconstate is formatted as blah-2, blah-4, blah-6, etc.
+            // the number corresponds to how many hands are cuffed.
         }
     }
 }
index efb0fab953193dcf7385abc06fd908cbaeef397a..6d20420c271533072c0b4eabe1e87b03c27e3dbf 100644 (file)
@@ -9,39 +9,6 @@ namespace Content.Server.Hands.Systems
     [UsedImplicitly]
     public sealed class HandVirtualItemSystem : SharedHandVirtualItemSystem
     {
-        [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
 
-        public bool TrySpawnVirtualItemInHand(EntityUid blockingEnt, EntityUid user) => TrySpawnVirtualItemInHand(blockingEnt, user, out _);
-
-        public bool TrySpawnVirtualItemInHand(EntityUid blockingEnt, EntityUid user, [NotNullWhen(true)] out EntityUid? virtualItem)
-        {
-            if (!_handsSystem.TryGetEmptyHand(user, out var hand))
-            {
-                virtualItem = null;
-                return false;
-            }
-
-            var pos = EntityManager.GetComponent<TransformComponent>(user).Coordinates;
-            virtualItem = EntityManager.SpawnEntity("HandVirtualItem", pos);
-            var virtualItemComp = EntityManager.GetComponent<HandVirtualItemComponent>(virtualItem.Value);
-            virtualItemComp.BlockingEntity = blockingEnt;
-            _handsSystem.DoPickup(user, hand, virtualItem.Value);
-            return true;
-        }
-
-        /// <summary>
-        ///     Deletes all virtual items in a user's hands with
-        ///     the specified blocked entity.
-        /// </summary>
-        public void DeleteInHandsMatching(EntityUid user, EntityUid matching)
-        {
-            foreach (var hand in _handsSystem.EnumerateHands(user))
-            {
-                if (TryComp(hand.HeldEntity, out HandVirtualItemComponent? virt) && virt.BlockingEntity == matching)
-                {
-                    Delete(virt, user);
-                }
-            }
-        }
     }
 }
index 5df9720fa89f9d9a38c309be9dc96026f157ecb5..918a0c75be2830e2b095d08ac1696fd127629ab9 100644 (file)
@@ -1,4 +1,5 @@
-using Content.Server.Cuffs.Components;
+using Content.Server.Cuffs;
+using Content.Shared.Cuffs.Components;
 using Content.Shared.Implants;
 using Content.Shared.Implants.Components;
 using Content.Shared.Interaction.Events;
@@ -9,6 +10,7 @@ namespace Content.Server.Implants;
 
 public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
 {
+    [Dependency] private readonly CuffableSystem _cuffable = default!;
     [Dependency] private readonly SharedContainerSystem _container = default!;
 
     public override void Initialize()
@@ -26,10 +28,7 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem
         if (!TryComp<CuffableComponent>(component.ImplantedEntity, out var cuffs) || cuffs.Container.ContainedEntities.Count < 1)
             return;
 
-        if (TryComp<HandcuffComponent>(cuffs.LastAddedCuffs, out var cuff))
-        {
-            cuffs.Uncuff(component.ImplantedEntity.Value, cuffs.LastAddedCuffs, cuff, true);
-        }
+        _cuffable.Uncuff(component.ImplantedEntity.Value, cuffs.LastAddedCuffs, cuffs.LastAddedCuffs);
     }
 
     #region Relays
index 238b30718f6f781cdf8b8b4774a070413b636a44..ea86c2c7d64d9a5c852ca5093016a5318d2e9f00 100644 (file)
@@ -1,6 +1,6 @@
-using Content.Server.Cuffs.Components;
 using Content.Server.Objectives.Interfaces;
 using Content.Server.Station.Components;
+using Content.Shared.Cuffs.Components;
 using JetBrains.Annotations;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Utility;
index 3289629055e50e70b9085adfc1204d09655c6581..282e197d83e52a6abeff813aa780856164e95c83 100644 (file)
@@ -1,4 +1,4 @@
-using Content.Server.Cuffs.Components;
+using System.Linq;
 using Content.Server.DoAfter;
 using Content.Server.Ensnaring;
 using Content.Server.Hands.Components;
@@ -14,6 +14,8 @@ using Content.Shared.Verbs;
 using Robust.Server.GameObjects;
 using System.Threading;
 using Content.Server.Administration.Logs;
+using Content.Shared.Cuffs;
+using Content.Shared.Cuffs.Components;
 using Content.Shared.Database;
 using Content.Shared.DoAfter;
 using Content.Shared.Ensnaring.Components;
@@ -25,6 +27,7 @@ namespace Content.Server.Strip
 {
     public sealed class StrippableSystem : SharedStrippableSystem
     {
+        [Dependency] private readonly SharedCuffableSystem _cuffable = default!;
         [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
         [Dependency] private readonly InventorySystem _inventorySystem = default!;
         [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
@@ -95,9 +98,9 @@ namespace Content.Server.Strip
             // is the target a handcuff?
             if (TryComp(hand.HeldEntity, out HandVirtualItemComponent? virt)
                 && TryComp(target, out CuffableComponent? cuff)
-                && cuff.Container.Contains(virt.BlockingEntity))
+                && _cuffable.GetAllCuffs(cuff).Contains(virt.BlockingEntity))
             {
-                cuff.TryUncuff(user, virt.BlockingEntity);
+                _cuffable.TryUncuff(target, user, virt.BlockingEntity, cuffable: cuff);
                 return;
             }
 
index 13f824d1b51e27e4253f36273ac0d415b18d6571..ffb361b2f0ad7ada3b58509ff7013646b7fc4444 100644 (file)
@@ -11,6 +11,7 @@ using Content.Server.Contests;
 using Content.Server.Examine;
 using Content.Server.Hands.Components;
 using Content.Server.Movement.Systems;
+using Content.Shared.Administration.Components;
 using Content.Shared.CombatMode;
 using Content.Shared.Damage;
 using Content.Shared.Database;
diff --git a/Content.Shared/Administration/Components/DisarmProneComponent.cs b/Content.Shared/Administration/Components/DisarmProneComponent.cs
new file mode 100644 (file)
index 0000000..3829dfd
--- /dev/null
@@ -0,0 +1,14 @@
+using Content.Shared.Weapons.Melee;
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Administration.Components;
+
+/// <summary>
+/// This is used for forcing someone to be disarmed 100% of the time.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedMeleeWeaponSystem))]
+public sealed class DisarmProneComponent : Component
+{
+
+}
diff --git a/Content.Shared/Cuffs/Components/CuffableComponent.cs b/Content.Shared/Cuffs/Components/CuffableComponent.cs
new file mode 100644 (file)
index 0000000..14c572b
--- /dev/null
@@ -0,0 +1,72 @@
+using Robust.Shared.Containers;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Cuffs.Components;
+
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedCuffableSystem))]
+public sealed class CuffableComponent : Component
+{
+    /// <summary>
+    /// The current RSI for the handcuff layer
+    /// </summary>
+    [DataField("currentRSI"), ViewVariables(VVAccess.ReadWrite)]
+    public string? CurrentRSI;
+
+    /// <summary>
+    /// How many of this entity's hands are currently cuffed.
+    /// </summary>
+    [ViewVariables]
+    public int CuffedHandCount => Container.ContainedEntities.Count * 2;
+
+    /// <summary>
+    /// The last pair of cuffs that was added to this entity.
+    /// </summary>
+    [ViewVariables]
+    public EntityUid LastAddedCuffs => Container.ContainedEntities[^1];
+
+    /// <summary>
+    ///     Container of various handcuffs currently applied to the entity.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadOnly)]
+    public Container Container = default!;
+
+    /// <summary>
+    /// Whether or not the entity can still interact (is not cuffed)
+    /// </summary>
+    [DataField("canStillInteract"), ViewVariables(VVAccess.ReadWrite)]
+    public bool CanStillInteract = true;
+
+    /// <summary>
+    /// Whether or not the entity is currently in the process of being uncuffed.
+    /// </summary>
+    [DataField("uncuffing"), ViewVariables(VVAccess.ReadWrite)]
+    public bool Uncuffing;
+}
+
+[Serializable, NetSerializable]
+public sealed class CuffableComponentState : ComponentState
+{
+    public readonly bool CanStillInteract;
+    public readonly bool Uncuffing;
+    public readonly int NumHandsCuffed;
+    public readonly string? RSI;
+    public readonly string? IconState;
+    public readonly Color? Color;
+
+    public CuffableComponentState(int numHandsCuffed, bool canStillInteract,  bool uncuffing, string? rsiPath, string? iconState, Color? color)
+    {
+        NumHandsCuffed = numHandsCuffed;
+        CanStillInteract = canStillInteract;
+        Uncuffing = uncuffing;
+        RSI = rsiPath;
+        IconState = iconState;
+        Color = color;
+    }
+}
+
+[ByRefEvent]
+public readonly record struct CuffedStateChangeEvent;
+
diff --git a/Content.Shared/Cuffs/Components/HandcuffComponent.cs b/Content.Shared/Cuffs/Components/HandcuffComponent.cs
new file mode 100644 (file)
index 0000000..7f0f60b
--- /dev/null
@@ -0,0 +1,113 @@
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Cuffs.Components;
+
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(SharedCuffableSystem))]
+public sealed class HandcuffComponent : Component
+{
+    /// <summary>
+    ///     The time it takes to cuff an entity.
+    /// </summary>
+    [DataField("cuffTime"), ViewVariables(VVAccess.ReadWrite)]
+    public float CuffTime = 3.5f;
+
+    /// <summary>
+    ///     The time it takes to uncuff an entity.
+    /// </summary>
+    [DataField("uncuffTime"), ViewVariables(VVAccess.ReadWrite)]
+    public float UncuffTime = 3.5f;
+
+    /// <summary>
+    ///     The time it takes for a cuffed entity to uncuff itself.
+    /// </summary>
+    [DataField("breakoutTime"), ViewVariables(VVAccess.ReadWrite)]
+    public float BreakoutTime = 30f;
+
+    /// <summary>
+    ///     If an entity being cuffed is stunned, this amount of time is subtracted from the time it takes to add/remove their cuffs.
+    /// </summary>
+    [DataField("stunBonus"), ViewVariables(VVAccess.ReadWrite)]
+    public float StunBonus = 2f;
+
+    /// <summary>
+    ///     Will the cuffs break when removed?
+    /// </summary>
+    [DataField("breakOnRemove"), ViewVariables(VVAccess.ReadWrite)]
+    public bool BreakOnRemove;
+
+    /// <summary>
+    ///     Will the cuffs break when removed?
+    /// </summary>
+    [DataField("brokenPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>)), ViewVariables(VVAccess.ReadWrite)]
+    public string? BrokenPrototype;
+
+    /// <summary>
+    ///     The path of the RSI file used for the player cuffed overlay.
+    /// </summary>
+    [DataField("cuffedRSI"), ViewVariables(VVAccess.ReadWrite)]
+    public string? CuffedRSI = "Objects/Misc/handcuffs.rsi";
+
+    /// <summary>
+    ///     The iconstate used with the RSI file for the player cuffed overlay.
+    /// </summary>
+    [DataField("bodyIconState"), ViewVariables(VVAccess.ReadWrite)]
+    public string? OverlayIconState = "body-overlay";
+
+    /// <summary>
+    /// An opptional color specification for <see cref="OverlayIconState"/>
+    /// </summary>
+    [DataField("color"), ViewVariables(VVAccess.ReadWrite)]
+    public Color Color = Color.White;
+
+    [DataField("startCuffSound"), ViewVariables(VVAccess.ReadWrite)]
+    public SoundSpecifier StartCuffSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_start.ogg");
+
+    [DataField("endCuffSound"), ViewVariables(VVAccess.ReadWrite)]
+    public SoundSpecifier EndCuffSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_end.ogg");
+
+    [DataField("startBreakoutSound"), ViewVariables(VVAccess.ReadWrite)]
+    public SoundSpecifier StartBreakoutSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_breakout_start.ogg");
+
+    [DataField("startUncuffSound"), ViewVariables(VVAccess.ReadWrite)]
+    public SoundSpecifier StartUncuffSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_takeoff_start.ogg");
+
+    [DataField("endUncuffSound"), ViewVariables(VVAccess.ReadWrite)]
+    public SoundSpecifier EndUncuffSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_takeoff_end.ogg");
+
+    /// <summary>
+    ///     Used to prevent DoAfter getting spammed.
+    /// </summary>
+    [DataField("cuffing"), ViewVariables(VVAccess.ReadWrite)]
+    public bool Cuffing;
+}
+
+[Serializable, NetSerializable]
+public sealed class HandcuffComponentState : ComponentState
+{
+    public readonly string? IconState;
+    public readonly bool Cuffing;
+
+    public HandcuffComponentState(string? iconState, bool cuffing)
+    {
+        IconState = iconState;
+        Cuffing = cuffing;
+    }
+}
+
+/// <summary>
+/// Event fired on the User when the User attempts to cuff the Target.
+/// Should generate popups on the User.
+/// </summary>
+[ByRefEvent]
+public record struct UncuffAttemptEvent(EntityUid User, EntityUid Target)
+{
+    public readonly EntityUid User = User;
+    public readonly EntityUid Target = Target;
+    public bool Cancelled = false;
+}
diff --git a/Content.Shared/Cuffs/Components/SharedCuffableComponent.cs b/Content.Shared/Cuffs/Components/SharedCuffableComponent.cs
deleted file mode 100644 (file)
index 9d7f341..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-using Robust.Shared.Containers;
-using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Cuffs.Components
-{
-    [ByRefEvent]
-    public readonly struct CuffedStateChangeEvent { }
-
-    [NetworkedComponent()]
-    public abstract class SharedCuffableComponent : Component
-    {
-        [Dependency] private readonly IEntitySystemManager _sysMan = default!;
-        [Dependency] private readonly IComponentFactory _componentFactory = default!;
-
-        /// <summary>
-        /// How many of this entity's hands are currently cuffed.
-        /// </summary>
-        [ViewVariables]
-        public int CuffedHandCount => Container.ContainedEntities.Count * 2;
-
-        public EntityUid LastAddedCuffs => Container.ContainedEntities[^1];
-
-        public IReadOnlyList<EntityUid> StoredEntities => Container.ContainedEntities;
-
-        /// <summary>
-        ///     Container of various handcuffs currently applied to the entity.
-        /// </summary>
-        [ViewVariables(VVAccess.ReadOnly)]
-        public Container Container { get; set; } = default!;
-
-        protected override void Initialize()
-        {
-            base.Initialize();
-
-            Container = _sysMan.GetEntitySystem<SharedContainerSystem>().EnsureContainer<Container>(Owner, _componentFactory.GetComponentName(GetType()));
-        }
-
-        [ViewVariables]
-        public bool CanStillInteract { get; set; } = true;
-
-        [Serializable, NetSerializable]
-        protected sealed class CuffableComponentState : ComponentState
-        {
-            public bool CanStillInteract { get; }
-            public int NumHandsCuffed { get; }
-            public string? RSI { get; }
-            public string IconState { get; }
-            public Color Color { get; }
-
-            public CuffableComponentState(int numHandsCuffed, bool canStillInteract, string? rsiPath, string iconState, Color color)
-            {
-                NumHandsCuffed = numHandsCuffed;
-                CanStillInteract = canStillInteract;
-                RSI = rsiPath;
-                IconState = iconState;
-                Color = color;
-            }
-        }
-    }
-}
diff --git a/Content.Shared/Cuffs/Components/SharedHandcuffComponent.cs b/Content.Shared/Cuffs/Components/SharedHandcuffComponent.cs
deleted file mode 100644 (file)
index 2210c6b..0000000
+++ /dev/null
@@ -1,20 +0,0 @@
-using Robust.Shared.GameStates;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Cuffs.Components
-{
-    [NetworkedComponent()]
-    public abstract class SharedHandcuffComponent : Component
-    {
-        [Serializable, NetSerializable]
-        protected sealed class HandcuffedComponentState : ComponentState
-        {
-            public string? IconState { get; }
-
-            public HandcuffedComponentState(string? iconState)
-            {
-                IconState = iconState;
-            }
-        }
-    }
-}
index b5cf8a5868f5f06a8961253d3b9d1ad993d15975..7c685281a5d2bde572d88dadafe1c0b2e0bc26f6 100644 (file)
+using System.Linq;
 using Content.Shared.ActionBlocker;
+using Content.Shared.Administration.Components;
+using Content.Shared.Administration.Logs;
 using Content.Shared.Alert;
 using Content.Shared.Cuffs.Components;
-using Content.Shared.DragDrop;
+using Content.Shared.Database;
+using Content.Shared.DoAfter;
 using Content.Shared.Hands;
 using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction;
+using Content.Shared.Interaction.Components;
 using Content.Shared.Interaction.Events;
 using Content.Shared.Inventory.Events;
 using Content.Shared.Item;
+using Content.Shared.Mobs.Systems;
 using Content.Shared.Movement.Events;
 using Content.Shared.Physics.Pull;
+using Content.Shared.Popups;
 using Content.Shared.Pulling.Components;
 using Content.Shared.Pulling.Events;
 using Content.Shared.Rejuvenate;
+using Content.Shared.Stunnable;
+using Content.Shared.Verbs;
+using Content.Shared.Weapons.Melee.Events;
 using Robust.Shared.Containers;
+using Robust.Shared.Network;
+using Robust.Shared.Player;
+using Robust.Shared.Timing;
 
 namespace Content.Shared.Cuffs
 {
     public abstract class SharedCuffableSystem : EntitySystem
     {
-        [Dependency] private readonly ActionBlockerSystem _blocker = default!;
-        [Dependency] private readonly SharedContainerSystem _container = default!;
+        [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
         [Dependency] private readonly AlertsSystem _alerts = default!;
+        [Dependency] private readonly IComponentFactory _componentFactory = default!;
+        [Dependency] private readonly IGameTiming _timing = default!;
+        [Dependency] private readonly INetManager _net = default!;
+        [Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
+        [Dependency] private readonly MobStateSystem _mobState = default!;
+        [Dependency] private readonly SharedAudioSystem _audio = default!;
+        [Dependency] private readonly SharedContainerSystem _container = default!;
+        [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
+        [Dependency] private readonly SharedHandsSystem _hands = default!;
+        [Dependency] private readonly SharedHandVirtualItemSystem _handVirtualItem = default!;
+        [Dependency] private readonly SharedInteractionSystem _interaction = default!;
+        [Dependency] private readonly SharedPopupSystem _popup = default!;
+        [Dependency] private readonly SharedTransformSystem _transform = default!;
 
         public override void Initialize()
         {
             base.Initialize();
-            SubscribeLocalEvent<SharedCuffableComponent, EntRemovedFromContainerMessage>(OnCuffCountChanged);
-            SubscribeLocalEvent<SharedCuffableComponent, EntInsertedIntoContainerMessage>(OnCuffCountChanged);
-            SubscribeLocalEvent<SharedCuffableComponent, RejuvenateEvent>(OnRejuvenate);
-
-            SubscribeLocalEvent<SharedCuffableComponent, StopPullingEvent>(HandleStopPull);
-            SubscribeLocalEvent<SharedCuffableComponent, UpdateCanMoveEvent>(HandleMoveAttempt);
-            SubscribeLocalEvent<SharedCuffableComponent, AttackAttemptEvent>(CheckAct);
-            SubscribeLocalEvent<SharedCuffableComponent, UseAttemptEvent>(CheckAct);
-            SubscribeLocalEvent<SharedCuffableComponent, InteractionAttemptEvent>(CheckAct);
-            SubscribeLocalEvent<SharedCuffableComponent, IsEquippingAttemptEvent>(OnEquipAttempt);
-            SubscribeLocalEvent<SharedCuffableComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
-            SubscribeLocalEvent<SharedCuffableComponent, DropAttemptEvent>(CheckAct);
-            SubscribeLocalEvent<SharedCuffableComponent, PickupAttemptEvent>(CheckAct);
-            SubscribeLocalEvent<SharedCuffableComponent, BeingPulledAttemptEvent>(OnBeingPulledAttempt);
-            SubscribeLocalEvent<SharedCuffableComponent, PullStartedMessage>(OnPull);
-            SubscribeLocalEvent<SharedCuffableComponent, PullStoppedMessage>(OnPull);
-        }
-
-        private void OnRejuvenate(EntityUid uid, SharedCuffableComponent component, RejuvenateEvent args)
+
+            SubscribeLocalEvent<HandCountChangedEvent>(OnHandCountChanged);
+            SubscribeLocalEvent<UncuffAttemptEvent>(OnUncuffAttempt);
+
+            SubscribeLocalEvent<CuffableComponent, EntRemovedFromContainerMessage>(OnCuffsRemovedFromContainer);
+            SubscribeLocalEvent<CuffableComponent, EntInsertedIntoContainerMessage>(OnCuffsInsertedIntoContainer);
+            SubscribeLocalEvent<CuffableComponent, RejuvenateEvent>(OnRejuvenate);
+            SubscribeLocalEvent<CuffableComponent, ComponentInit>(OnStartup);
+            SubscribeLocalEvent<CuffableComponent, StopPullingEvent>(HandleStopPull);
+            SubscribeLocalEvent<CuffableComponent, UpdateCanMoveEvent>(HandleMoveAttempt);
+            SubscribeLocalEvent<CuffableComponent, IsEquippingAttemptEvent>(OnEquipAttempt);
+            SubscribeLocalEvent<CuffableComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
+            SubscribeLocalEvent<CuffableComponent, BeingPulledAttemptEvent>(OnBeingPulledAttempt);
+            SubscribeLocalEvent<CuffableComponent, GetVerbsEvent<Verb>>(AddUncuffVerb);
+            SubscribeLocalEvent<CuffableComponent, DoAfterEvent>(OnCuffableDoAfter);
+            SubscribeLocalEvent<CuffableComponent, PullStartedMessage>(OnPull);
+            SubscribeLocalEvent<CuffableComponent, PullStoppedMessage>(OnPull);
+            SubscribeLocalEvent<CuffableComponent, DropAttemptEvent>(CheckAct);
+            SubscribeLocalEvent<CuffableComponent, PickupAttemptEvent>(CheckAct);
+            SubscribeLocalEvent<CuffableComponent, AttackAttemptEvent>(CheckAct);
+            SubscribeLocalEvent<CuffableComponent, UseAttemptEvent>(CheckAct);
+            SubscribeLocalEvent<CuffableComponent, InteractionAttemptEvent>(CheckAct);
+
+            SubscribeLocalEvent<HandcuffComponent, AfterInteractEvent>(OnCuffAfterInteract);
+            SubscribeLocalEvent<HandcuffComponent, MeleeHitEvent>(OnCuffMeleeHit);
+            SubscribeLocalEvent<HandcuffComponent, DoAfterEvent>(OnAddCuffDoAfter);
+
+        }
+
+        private void OnUncuffAttempt(ref UncuffAttemptEvent args)
+        {
+            if (args.Cancelled)
+            {
+                return;
+            }
+            if (!Exists(args.User) || Deleted(args.User))
+            {
+                // Should this even be possible?
+                args.Cancelled = true;
+                return;
+            }
+
+            // If the user is the target, special logic applies.
+            // This is because the CanInteract blocking of the cuffs prevents self-uncuff.
+            if (args.User == args.Target)
+            {
+                // This UncuffAttemptEvent check should probably be In MobStateSystem, not here?
+                if (_mobState.IsIncapacitated(args.User))
+                {
+                    args.Cancelled = true;
+                }
+                else
+                {
+                    // TODO Find a way for cuffable to check ActionBlockerSystem.CanInteract() without blocking itself
+                }
+            }
+            else
+            {
+                // Check if the user can interact.
+                if (!_actionBlocker.CanInteract(args.User, args.Target))
+                {
+                    args.Cancelled = true;
+                }
+            }
+
+            if (args.Cancelled && _net.IsServer)
+            {
+                _popup.PopupEntity(Loc.GetString("cuffable-component-cannot-interact-message"), args.Target, args.User);
+            }
+        }
+
+        private void OnStartup(EntityUid uid, CuffableComponent component, ComponentInit args)
+        {
+            component.Container = _container.EnsureContainer<Container>(uid, _componentFactory.GetComponentName(component.GetType()));
+        }
+
+        private void OnRejuvenate(EntityUid uid, CuffableComponent component, RejuvenateEvent args)
         {
             _container.EmptyContainer(component.Container, true, attachToGridOrMap: true);
         }
 
-        private void OnCuffCountChanged(EntityUid uid, SharedCuffableComponent component, ContainerModifiedMessage args)
+        private void OnCuffsRemovedFromContainer(EntityUid uid, CuffableComponent component, EntRemovedFromContainerMessage args)
+        {
+            if (args.Container.ID == component.Container.ID)
+            {
+                _handVirtualItem.DeleteInHandsMatching(uid, args.Entity);
+                UpdateCuffState(uid, component);
+            }
+        }
+
+        private void OnCuffsInsertedIntoContainer(EntityUid uid, CuffableComponent component, ContainerModifiedMessage args)
         {
             if (args.Container == component.Container)
                 UpdateCuffState(uid, component);
         }
 
-        public void UpdateCuffState(EntityUid uid, SharedCuffableComponent component)
+        public void UpdateCuffState(EntityUid uid, CuffableComponent component)
         {
             var canInteract = TryComp(uid, out SharedHandsComponent? hands) && hands.Hands.Count > component.CuffedHandCount;
 
@@ -63,7 +158,7 @@ namespace Content.Shared.Cuffs
 
             component.CanStillInteract = canInteract;
             Dirty(component);
-            _blocker.UpdateCanMove(uid);
+            _actionBlocker.UpdateCanMove(uid);
 
             if (component.CanStillInteract)
                 _alerts.ClearAlert(uid, AlertType.Handcuffed);
@@ -74,7 +169,7 @@ namespace Content.Shared.Cuffs
             RaiseLocalEvent(uid, ref ev);
         }
 
-        private void OnBeingPulledAttempt(EntityUid uid, SharedCuffableComponent component, BeingPulledAttemptEvent args)
+        private void OnBeingPulledAttempt(EntityUid uid, CuffableComponent component, BeingPulledAttemptEvent args)
         {
             if (!TryComp<SharedPullableComponent>(uid, out var pullable))
                 return;
@@ -82,13 +177,14 @@ namespace Content.Shared.Cuffs
             if (pullable.Puller != null && !component.CanStillInteract) // If we are being pulled already and cuffed, we can't get pulled again.
                 args.Cancel();
         }
-        private void OnPull(EntityUid uid, SharedCuffableComponent component, PullMessage args)
+
+        private void OnPull(EntityUid uid, CuffableComponent component, PullMessage args)
         {
             if (!component.CanStillInteract)
-                _blocker.UpdateCanMove(uid);
+                _actionBlocker.UpdateCanMove(uid);
         }
 
-        private void HandleMoveAttempt(EntityUid uid, SharedCuffableComponent component, UpdateCanMoveEvent args)
+        private void HandleMoveAttempt(EntityUid uid, CuffableComponent component, UpdateCanMoveEvent args)
         {
             if (component.CanStillInteract || !EntityManager.TryGetComponent(uid, out SharedPullableComponent? pullable) || !pullable.BeingPulled)
                 return;
@@ -96,32 +192,466 @@ namespace Content.Shared.Cuffs
             args.Cancel();
         }
 
-        private void HandleStopPull(EntityUid uid, SharedCuffableComponent component, StopPullingEvent args)
+        private void HandleStopPull(EntityUid uid, CuffableComponent component, StopPullingEvent args)
         {
-            if (args.User == null || !EntityManager.EntityExists(args.User.Value)) return;
+            if (args.User == null || !Exists(args.User.Value))
+                return;
 
-            if (args.User.Value == component.Owner && !component.CanStillInteract)
-            {
+            if (args.User.Value == uid && !component.CanStillInteract)
                 args.Cancel();
+        }
+
+        private void AddUncuffVerb(EntityUid uid, CuffableComponent component, GetVerbsEvent<Verb> args)
+        {
+            // Can the user access the cuffs, and is there even anything to uncuff?
+            if (!args.CanAccess || component.CuffedHandCount == 0 || args.Hands == null)
+                return;
+
+            // We only check can interact if the user is not uncuffing themselves. As a result, the verb will show up
+            // when the user is incapacitated & trying to uncuff themselves, but TryUncuff() will still fail when
+            // attempted.
+            if (args.User != args.Target && !args.CanInteract)
+                return;
+
+            Verb verb = new()
+            {
+                Act = () => TryUncuff(uid, args.User, cuffable: component),
+                DoContactInteraction = true,
+                Text = Loc.GetString("uncuff-verb-get-data-text")
+            };
+            //TODO VERB ICON add uncuffing symbol? may re-use the alert symbol showing that you are currently cuffed?
+            args.Verbs.Add(verb);
+        }
+
+        private void OnCuffableDoAfter(EntityUid uid, CuffableComponent component, DoAfterEvent args)
+        {
+            if (args.Args.Target is not { } target || args.Args.Used is not { } used)
+                return;
+            if (args.Handled)
+                return;
+            args.Handled = true;
+
+            component.Uncuffing = false;
+            Dirty(component);
+
+            var user = args.Args.User;
+
+            if (!args.Cancelled)
+            {
+                Uncuff(target, user, used, component);
+            }
+            else if (_net.IsServer)
+            {
+                _popup.PopupEntity(Loc.GetString("cuffable-component-remove-cuffs-fail-message"), user, user);
+            }
+        }
+
+        private void OnCuffAfterInteract(EntityUid uid, HandcuffComponent component, AfterInteractEvent args)
+        {
+            if (args.Target is not {Valid: true} target)
+                return;
+
+            if (!args.CanReach)
+            {
+                if (_net.IsServer)
+                    _popup.PopupEntity(Loc.GetString("handcuff-component-too-far-away-error"), args.User, args.User);
+                return;
+            }
+
+            TryCuffing(args.User, target, uid, component);
+            args.Handled = true;
+        }
+
+        private void OnCuffMeleeHit(EntityUid uid, HandcuffComponent component, MeleeHitEvent args)
+        {
+            if (!args.HitEntities.Any())
+                return;
+
+            TryCuffing(uid, args.User, args.HitEntities.First(), component);
+            args.Handled = true;
+        }
+
+        private void OnAddCuffDoAfter(EntityUid uid, HandcuffComponent component, DoAfterEvent args)
+        {
+            var user = args.Args.User;
+            var target = args.Args.Target!.Value;
+
+            if (!TryComp<CuffableComponent>(target, out var cuffable))
+                return;
+
+            if (args.Handled)
+                return;
+            args.Handled = true;
+            component.Cuffing = false;
+
+            if (!args.Cancelled && TryAddNewCuffs(target, user, uid, cuffable))
+            {
+                _audio.PlayPvs(component.EndCuffSound, uid);
+                if (!_net.IsServer)
+                    return;
+
+                _popup.PopupEntity(Loc.GetString("handcuff-component-cuff-observer-success-message",
+                        ("user", Identity.Name(user, EntityManager)), ("target", Identity.Name(target, EntityManager))),
+                    target, Filter.Pvs(target, entityManager: EntityManager)
+                        .RemoveWhere(e => e.AttachedEntity == target || e.AttachedEntity == user), true);
+
+                if (target == user)
+                {
+                    _popup.PopupEntity(Loc.GetString("handcuff-component-cuff-self-success-message"), user, user);
+                    _adminLog.Add(LogType.Action, LogImpact.Medium,
+                        $"{ToPrettyString(user):player} has cuffed himself");
+                }
+                else
+                {
+                    _popup.PopupEntity(Loc.GetString("handcuff-component-cuff-other-success-message",
+                        ("otherName", Identity.Name(target, EntityManager, user))), user, user);
+                    _popup.PopupEntity(Loc.GetString("handcuff-component-cuff-by-other-success-message",
+                        ("otherName", Identity.Name(user, EntityManager, target))), target, target);
+                    _adminLog.Add(LogType.Action, LogImpact.Medium,
+                        $"{ToPrettyString(user):player} has cuffed {ToPrettyString(target):player}");
+                }
+            }
+            else
+            {
+                if (!_net.IsServer)
+                    return;
+                if (target == user)
+                {
+                    _popup.PopupEntity(Loc.GetString("handcuff-component-cuff-interrupt-self-message"), user, user);
+                }
+                else
+                {
+                    _popup.PopupEntity(Loc.GetString("handcuff-component-cuff-interrupt-message",
+                        ("targetName", Identity.Name(target, EntityManager, user))), user, user);
+                    _popup.PopupEntity(Loc.GetString("handcuff-component-cuff-interrupt-other-message",
+                        ("otherName", Identity.Name(user, EntityManager, target))), target, target);
+                }
+            }
+
+        }
+
+        /// <summary>
+        ///     Check the current amount of hands the owner has, and if there's less hands than active cuffs we remove some cuffs.
+        /// </summary>
+        private void OnHandCountChanged(HandCountChangedEvent message)
+        {
+            var owner = message.Sender;
+
+            if (!TryComp(owner, out CuffableComponent? cuffable) ||
+                !cuffable.Initialized)
+            {
+                return;
+            }
+
+            var dirty = false;
+            var handCount = CompOrNull<SharedHandsComponent>(owner)?.Count ?? 0;
+
+            while (cuffable.CuffedHandCount > handCount && cuffable.CuffedHandCount > 0)
+            {
+                dirty = true;
+
+                var container = cuffable.Container;
+                var entity = container.ContainedEntities[^1];
+
+                container.Remove(entity);
+                _transform.SetWorldPosition(entity, _transform.GetWorldPosition(owner));
+            }
+
+            if (dirty)
+            {
+                UpdateCuffState(owner, cuffable);
+            }
+        }
+
+        /// <summary>
+        ///     Adds virtual cuff items to the user's hands.
+        /// </summary>
+        private void UpdateHeldItems(EntityUid uid, EntityUid handcuff, CuffableComponent? component = null)
+        {
+            if (!Resolve(uid, ref component))
+                return;
+
+            // TODO we probably don't just want to use the generic virtual-item entity, and instead
+            // want to add our own item, so that use-in-hand triggers an uncuff attempt and the like.
+
+            if (!TryComp<SharedHandsComponent>(uid, out var handsComponent))
+                return;
+
+            var freeHands = 0;
+            foreach (var hand in _hands.EnumerateHands(uid, handsComponent))
+            {
+                if (hand.HeldEntity == null)
+                {
+                    freeHands++;
+                    continue;
+                }
+
+                // Is this entity removable? (it might be an existing handcuff blocker)
+                if (HasComp<UnremoveableComponent>(hand.HeldEntity))
+                    continue;
+
+                _hands.DoDrop(uid, hand, true, handsComponent);
+                freeHands++;
+                if (freeHands == 2)
+                    break;
+            }
+
+            if (_handVirtualItem.TrySpawnVirtualItemInHand(handcuff, uid, out var virtItem1))
+                EnsureComp<UnremoveableComponent>(virtItem1.Value);
+
+            if (_handVirtualItem.TrySpawnVirtualItemInHand(handcuff, uid, out var virtItem2))
+                EnsureComp<UnremoveableComponent>(virtItem2.Value);
+        }
+
+        /// <summary>
+        /// Add a set of cuffs to an existing CuffedComponent.
+        /// </summary>
+        public bool TryAddNewCuffs(EntityUid target, EntityUid user, EntityUid handcuff, CuffableComponent? component = null, HandcuffComponent? cuff = null)
+        {
+            if (!Resolve(target, ref component) || !Resolve(handcuff, ref cuff))
+                return false;
+
+            if (!_interaction.InRangeUnobstructed(handcuff, target))
+                return false;
+
+            // Success!
+            _hands.TryDrop(user, handcuff);
+
+            component.Container.Insert(handcuff);
+            UpdateHeldItems(target, handcuff, component);
+            return true;
+        }
+
+        public void TryCuffing(EntityUid user, EntityUid target, EntityUid handcuff, HandcuffComponent? handcuffComponent = null, CuffableComponent? cuffable = null)
+        {
+            if (!Resolve(handcuff, ref handcuffComponent) || !Resolve(target, ref cuffable, false))
+                return;
+
+            if (handcuffComponent.Cuffing)
+                return;
+
+            if (!TryComp<SharedHandsComponent?>(target, out var hands))
+            {
+                if (_net.IsServer)
+                {
+                    _popup.PopupEntity(Loc.GetString("handcuff-component-target-has-no-hands-error",
+                        ("targetName", Identity.Name(target, EntityManager, user))), user, user);
+                }
+                return;
+            }
+
+            if (cuffable.CuffedHandCount >= hands.Count)
+            {
+                if (_net.IsServer)
+                {
+                    _popup.PopupEntity(Loc.GetString("handcuff-component-target-has-no-free-hands-error",
+                        ("targetName", Identity.Name(target, EntityManager, user))), user, user);
+                }
+                return;
+            }
+
+            if (_net.IsServer)
+            {
+                _popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-observer",
+                    ("user", Identity.Name(user, EntityManager)), ("target", Identity.Name(target, EntityManager))),
+                    target, Filter.Pvs(target, entityManager: EntityManager)
+                    .RemoveWhere(e => e.AttachedEntity == target || e.AttachedEntity == user), true);
+
+                if (target == user)
+                {
+                    _popup.PopupEntity(Loc.GetString("handcuff-component-target-self"), user, user);
+                }
+                else
+                {
+                    _popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-target-message",
+                        ("targetName", Identity.Name(target, EntityManager, user))), user, user);
+                    _popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-by-other-message",
+                        ("otherName", Identity.Name(user, EntityManager, target))), target, target);
+                }
+            }
+
+            _audio.PlayPvs(handcuffComponent.StartCuffSound, handcuff);
+
+            var cuffTime = handcuffComponent.CuffTime;
+
+            if (HasComp<StunnedComponent>(target))
+                cuffTime = MathF.Max(0.1f, cuffTime - handcuffComponent.StunBonus);
+
+            if (HasComp<DisarmProneComponent>(target))
+                cuffTime = 0.0f; // cuff them instantly.
+
+            var doAfterEventArgs = new DoAfterEventArgs(user, cuffTime, default, target, handcuff)
+            {
+                RaiseOnUser = false,
+                RaiseOnTarget = false,
+                RaiseOnUsed = true,
+                BreakOnTargetMove = true,
+                BreakOnUserMove = true,
+                BreakOnDamage = true,
+                BreakOnStun = true,
+                NeedHand = true
+            };
+
+            handcuffComponent.Cuffing = true;
+            if (_net.IsServer)
+                _doAfter.DoAfter(doAfterEventArgs);
+        }
+
+        /// <summary>
+        /// Attempt to uncuff a cuffed entity. Can be called by the cuffed entity, or another entity trying to help uncuff them.
+        /// If the uncuffing succeeds, the cuffs will drop on the floor.
+        /// </summary>
+        /// <param name="target"></param>
+        /// <param name="user">The cuffed entity</param>
+        /// <param name="cuffsToRemove">Optional param for the handcuff entity to remove from the cuffed entity. If null, uses the most recently added handcuff entity.</param>
+        /// <param name="cuffable"></param>
+        /// <param name="cuff"></param>
+        public void TryUncuff(EntityUid target, EntityUid user, EntityUid? cuffsToRemove = null, CuffableComponent? cuffable = null, HandcuffComponent? cuff = null)
+        {
+            if (!Resolve(target, ref cuffable))
+                return;
+
+            if (cuffable.Uncuffing)
+                return;
+
+            var isOwner = user == target;
+
+            if (cuffsToRemove == null)
+            {
+                if (cuffable.Container.ContainedEntities.Count == 0)
+                {
+                    return;
+                }
+
+                cuffsToRemove = cuffable.LastAddedCuffs;
+            }
+            else
+            {
+                if (!cuffable.Container.ContainedEntities.Contains(cuffsToRemove.Value))
+                {
+                    Logger.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!");
+                }
+            }
+
+            if (!Resolve(cuffsToRemove.Value, ref cuff))
+                return;
+
+            var attempt = new UncuffAttemptEvent(user, target);
+            RaiseLocalEvent(user, ref attempt, true);
+
+            if (attempt.Cancelled)
+            {
+                return;
+            }
+
+            if (!isOwner && !_interaction.InRangeUnobstructed(user, target))
+            {
+                if (_net.IsServer)
+                    _popup.PopupEntity(Loc.GetString("cuffable-component-cannot-remove-cuffs-too-far-message"), user, user);
+                return;
+            }
+
+            if (_net.IsServer)
+                _popup.PopupEntity(Loc.GetString("cuffable-component-start-removing-cuffs-message"), user, user);
+
+            _audio.PlayPredicted(isOwner ? cuff.StartBreakoutSound : cuff.StartUncuffSound, target, user);
+
+            var uncuffTime = isOwner ? cuff.BreakoutTime : cuff.UncuffTime;
+            var doAfterEventArgs = new DoAfterEventArgs(user, uncuffTime, default, target, cuffsToRemove)
+            {
+                RaiseOnTarget = true,
+                RaiseOnUsed = false,
+                RaiseOnUser = false,
+                BreakOnUserMove = true,
+                BreakOnTargetMove = true,
+                BreakOnDamage = true,
+                BreakOnStun = true,
+                NeedHand = true
+            };
+
+            cuffable.Uncuffing = true;
+            Dirty(cuffable);
+            if (_net.IsServer)
+                _doAfter.DoAfter(doAfterEventArgs);
+        }
+
+        public void Uncuff(EntityUid target, EntityUid user, EntityUid cuffsToRemove, CuffableComponent? cuffable = null, HandcuffComponent? cuff = null)
+        {
+            if (!Resolve(target, ref cuffable) || !Resolve(cuffsToRemove, ref cuff))
+                return;
+
+            _audio.PlayPvs(cuff.EndUncuffSound, target);
+
+            cuffable.Container.Remove(cuffsToRemove);
+
+            if (cuff.BreakOnRemove)
+            {
+                QueueDel(cuffsToRemove);
+                var trash = Spawn(cuff.BrokenPrototype, Transform(cuffsToRemove).Coordinates);
+                _hands.PickupOrDrop(user, trash);
+            }
+            else
+            {
+                _hands.PickupOrDrop(user, cuffsToRemove);
+            }
+
+            // Only play popups on server because popups suck
+            if (_net.IsServer)
+            {
+                if (cuffable.CuffedHandCount == 0)
+                {
+                    _popup.PopupEntity(Loc.GetString("cuffable-component-remove-cuffs-success-message"), user, user);
+
+                    if (target != user)
+                    {
+                        _popup.PopupEntity(Loc.GetString("cuffable-component-remove-cuffs-by-other-success-message",
+                            ("otherName", Identity.Name(user, EntityManager, user))), target, target);
+                        _adminLog.Add(LogType.Action, LogImpact.Medium,
+                            $"{ToPrettyString(user):player} has successfully uncuffed {ToPrettyString(target):player}");
+                    }
+                    else
+                    {
+                        _adminLog.Add(LogType.Action, LogImpact.Medium,
+                            $"{ToPrettyString(user):player} has successfully uncuffed themselves");
+                    }
+                }
+                else
+                {
+                    if (user != target)
+                    {
+                        _popup.PopupEntity(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message",
+                            ("cuffedHandCount", cuffable.CuffedHandCount),
+                            ("otherName", Identity.Name(user, EntityManager, user))), user, user);
+                        _popup.PopupEntity(Loc.GetString(
+                            "cuffable-component-remove-cuffs-by-other-partial-success-message",
+                            ("otherName", Identity.Name(user, EntityManager, user)),
+                            ("cuffedHandCount", cuffable.CuffedHandCount)), target, target);
+                    }
+                    else
+                    {
+                        _popup.PopupEntity(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message",
+                            ("cuffedHandCount", cuffable.CuffedHandCount)), user, user);
+                    }
+                }
             }
         }
 
         #region ActionBlocker
 
-        private void CheckAct(EntityUid uid, SharedCuffableComponent component, CancellableEntityEventArgs args)
+        private void CheckAct(EntityUid uid, CuffableComponent component, CancellableEntityEventArgs args)
         {
             if (!component.CanStillInteract)
                 args.Cancel();
         }
 
-        private void OnEquipAttempt(EntityUid uid, SharedCuffableComponent component, IsEquippingAttemptEvent args)
+        private void OnEquipAttempt(EntityUid uid, CuffableComponent component, IsEquippingAttemptEvent args)
         {
             // is this a self-equip, or are they being stripped?
             if (args.Equipee == uid)
                 CheckAct(uid, component, args);
         }
 
-        private void OnUnequipAttempt(EntityUid uid, SharedCuffableComponent component, IsUnequippingAttemptEvent args)
+        private void OnUnequipAttempt(EntityUid uid, CuffableComponent component, IsUnequippingAttemptEvent args)
         {
             // is this a self-equip, or are they being stripped?
             if (args.Unequipee == uid)
@@ -129,5 +659,10 @@ namespace Content.Shared.Cuffs
         }
 
         #endregion
+
+        public IReadOnlyList<EntityUid> GetAllCuffs(CuffableComponent component)
+        {
+            return component.Container.ContainedEntities;
+        }
     }
 }
index f4d6558d337e0fdaae1a74f1c999ca9536960b27..e33ffdf051898fa20b8c823bd845167c92bd9b23 100644 (file)
@@ -1,11 +1,17 @@
+using System.Diagnostics.CodeAnalysis;
 using Content.Shared.Hands.Components;
+using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Interaction;
 using Content.Shared.Inventory.Events;
+using Robust.Shared.Network;
 
 namespace Content.Shared.Hands;
 
 public abstract class SharedHandVirtualItemSystem : EntitySystem
 {
+    [Dependency] private readonly INetManager _net = default!;
+    [Dependency] private readonly SharedHandsSystem _hands = default!;
+
     public override void Initialize()
     {
         base.Initialize();
@@ -14,6 +20,43 @@ public abstract class SharedHandVirtualItemSystem : EntitySystem
         SubscribeLocalEvent<HandVirtualItemComponent, BeforeRangedInteractEvent>(HandleBeforeInteract);
     }
 
+    public bool TrySpawnVirtualItemInHand(EntityUid blockingEnt, EntityUid user)
+    {
+        return TrySpawnVirtualItemInHand(blockingEnt, user, out _);
+    }
+
+    public bool TrySpawnVirtualItemInHand(EntityUid blockingEnt, EntityUid user, [NotNullWhen(true)] out EntityUid? virtualItem)
+    {
+        if (!_hands.TryGetEmptyHand(user, out var hand))
+        {
+            virtualItem = null;
+            return false;
+        }
+
+        var pos = Transform(user).Coordinates;
+        virtualItem = Spawn("HandVirtualItem", pos);
+        var virtualItemComp = EntityManager.GetComponent<HandVirtualItemComponent>(virtualItem.Value);
+        virtualItemComp.BlockingEntity = blockingEnt;
+        _hands.DoPickup(user, hand, virtualItem.Value);
+        return true;
+    }
+
+
+    /// <summary>
+    ///     Deletes all virtual items in a user's hands with
+    ///     the specified blocked entity.
+    /// </summary>
+    public void DeleteInHandsMatching(EntityUid user, EntityUid matching)
+    {
+        foreach (var hand in _hands.EnumerateHands(user))
+        {
+            if (TryComp(hand.HeldEntity, out HandVirtualItemComponent? virt) && virt.BlockingEntity == matching)
+            {
+                Delete(virt, user);
+            }
+        }
+    }
+
     private void OnBeingEquippedAttempt(EntityUid uid, HandVirtualItemComponent component, BeingEquippedAttemptEvent args)
     {
         args.Cancel();
@@ -34,10 +77,10 @@ public abstract class SharedHandVirtualItemSystem : EntitySystem
     public void Delete(HandVirtualItemComponent comp, EntityUid user)
     {
         var userEv = new VirtualItemDeletedEvent(comp.BlockingEntity, user);
-        RaiseLocalEvent(user, userEv, false);
+        RaiseLocalEvent(user, userEv);
         var targEv = new VirtualItemDeletedEvent(comp.BlockingEntity, user);
-        RaiseLocalEvent(comp.BlockingEntity, targEv, false);
+        RaiseLocalEvent(comp.BlockingEntity, targEv);
 
-        EntityManager.QueueDeleteEntity(comp.Owner);
+        QueueDel(comp.Owner);
     }
 }
index 2fa13799cc4d6d35d7a352a326dbee07d44d7a6a..8c024f7943f266556e141163dc7bef5efaa02812 100644 (file)
@@ -3,8 +3,10 @@ handcuff-component-cuffs-broken-error = The cuffs are broken!
 handcuff-component-target-has-no-hands-error = {$targetName} has no hands!
 handcuff-component-target-has-no-free-hands-error = {$targetName} has no free hands!
 handcuff-component-too-far-away-error = You are too far away to use the cuffs!
+handcuff-component-start-cuffing-observer = {$user} starts cuffing {$target}!
 handcuff-component-start-cuffing-target-message = You start cuffing {$targetName}.
 handcuff-component-start-cuffing-by-other-message = {$otherName} starts cuffing you!
+handcuff-component-cuff-observer-success-message = {$user} cuffs {$target}.
 handcuff-component-cuff-other-success-message = You successfully cuff {$otherName}.
 handcuff-component-cuff-by-other-success-message = You have been cuffed by {$otherName}!
 handcuff-component-cuff-self-success-message = You cuff yourself.
index 83109c28f41eec6c9a1946abf6613a9edab76eb8..2d7c6b6c07c48a49dd6eebee827c5b1023eea185 100644 (file)
@@ -8,7 +8,7 @@
     size: 3
   - type: Handcuff
     cuffedRSI: Objects/Misc/handcuffs.rsi
-    iconState: body-overlay
+    bodyIconState: body-overlay
   - type: Sprite
     sprite: Objects/Misc/handcuffs.rsi
     state: handcuff