diff --git a/Content.Client/Cuffs/Components/CuffableComponent.cs b/Content.Client/Cuffs/Components/CuffableComponent.cs deleted file mode 100644 index a519a8ad39..0000000000 --- a/Content.Client/Cuffs/Components/CuffableComponent.cs +++ /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().UpdateCanMove(Owner); - - if (_entityManager.TryGetComponent(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(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 index b5d5267965..0000000000 --- a/Content.Client/Cuffs/Components/HandcuffComponent.cs +++ /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().TryGetComponent(Owner, out var sprite)) - { - sprite.LayerSetState(0, new RSI.StateId(state.IconState)); // TODO: safety check to see if RSI contains the state? - } - } - } -} diff --git a/Content.Client/Cuffs/CuffableSystem.cs b/Content.Client/Cuffs/CuffableSystem.cs index a2fa91c1e8..1109d3785f 100644 --- a/Content.Client/Cuffs/CuffableSystem.cs +++ b/Content.Client/Cuffs/CuffableSystem.cs @@ -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(OnShutdown); + SubscribeLocalEvent(OnCuffableHandleState); + SubscribeLocalEvent(OnHandcuffHandleState); + } + + private void OnShutdown(EntityUid uid, CuffableComponent component, ComponentShutdown args) + { + if (TryComp(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(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(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); + } } } + diff --git a/Content.Client/Inventory/StrippableBoundUserInterface.cs b/Content.Client/Inventory/StrippableBoundUserInterface.cs index 83f624c12b..ae6899ee0a 100644 --- a/Content.Client/Inventory/StrippableBoundUserInterface.cs +++ b/Content.Client/Inventory/StrippableBoundUserInterface.cs @@ -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(); _inv = _entMan.EntitySysManager.GetEntitySystem(); + _cuffable = _entMan.System(); 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); } diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs index 5c1d23c188..b1135e1f62 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs @@ -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(); + var cuffableSys = entityManager.System(); // 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 index d20a349a23..0000000000 --- a/Content.Server/Administration/Components/DisarmProneComponent.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Server.Administration.Components; - -/// -/// This is used for forcing someone to be disarmed 100% of the time. -/// -[RegisterComponent] -public sealed class DisarmProneComponent : Component { } diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs index b44af241c9..219b7a86d6 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs @@ -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; diff --git a/Content.Server/Alert/Click/RemoveCuffs.cs b/Content.Server/Alert/Click/RemoveCuffs.cs index de8b4455be..f46ba3c106 100644 --- a/Content.Server/Alert/Click/RemoveCuffs.cs +++ b/Content.Server/Alert/Click/RemoveCuffs.cs @@ -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().TryGetComponent(player, out CuffableComponent? cuffableComponent)) - { - cuffableComponent.TryUncuff(player); - } + var entityManager = IoCManager.Resolve(); + var cuffableSys = entityManager.System(); + cuffableSys.TryUncuff(player, player); } } } diff --git a/Content.Server/Cuffs/Components/CuffableComponent.cs b/Content.Server/Cuffs/Components/CuffableComponent.cs deleted file mode 100644 index 8d36b33f84..0000000000 --- a/Content.Server/Cuffs/Components/CuffableComponent.cs +++ /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(); - } - - 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(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); - } - - /// - /// Add a set of cuffs to an existing CuffedComponent. - /// - public bool TryAddNewCuffs(EntityUid user, EntityUid handcuff) - { - if (!_entMan.HasComponent(handcuff)) - { - Logger.Warning($"Handcuffs being applied to player are missing a {nameof(HandcuffComponent)}!"); - return false; - } - - if (!EntitySystem.Get().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(); - - // Success! - sys.TryDrop(user, handcuff); - - Container.Insert(handcuff); - UpdateHeldItems(handcuff); - return true; - } - - /// - /// Adds virtual cuff items to the user's hands. - /// - 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(); - - 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(hand.HeldEntity)) - continue; - - handSys.DoDrop(Owner, hand, true, handsComponent); - freeHands++; - if (freeHands == 2) - break; - } - - var virtSys = _entMan.EntitySysManager.GetEntitySystem(); - - if (virtSys.TrySpawnVirtualItemInHand(handcuff, Owner, out var virtItem1)) - _entMan.EnsureComponent(virtItem1.Value); - - if (virtSys.TrySpawnVirtualItemInHand(handcuff, Owner, out var virtItem2)) - _entMan.EnsureComponent(virtItem2.Value); - } - - /// - /// Updates the status effect indicator on the HUD. - /// - private void UpdateAlert() - { - if (CanStillInteract) - { - EntitySystem.Get().ClearAlert(Owner, AlertType.Handcuffed); - } - else - { - EntitySystem.Get().ShowAlert(Owner, AlertType.Handcuffed); - } - } - - /// - /// 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. - /// - /// The cuffed entity - /// Optional param for the handcuff entity to remove from the cuffed entity. If null, uses the most recently added handcuff entity. - 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(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().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(); - _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().DeleteInHandsMatching(user, cuffsToRemove); - Container.Remove(cuffsToRemove); - - if (cuff.BreakOnRemove) - { - _entMan.QueueDeleteEntity(cuffsToRemove); - var trash = _entMan.SpawnEntity(cuff.BrokenPrototype, MapCoordinates.Nullspace); - _entMan.EntitySysManager.GetEntitySystem().PickupOrDrop(user, trash); - } - else - { - _entMan.EntitySysManager.GetEntitySystem().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 index e8dda6cd55..0000000000 --- a/Content.Server/Cuffs/Components/HandcuffComponent.cs +++ /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!; - - /// - /// The time it takes to apply a to an entity. - /// - [DataField("cuffTime")] - public float CuffTime { get; set; } = 3.5f; - - /// - /// The time it takes to remove a from an entity. - /// - [DataField("uncuffTime")] - public float UncuffTime { get; set; } = 3.5f; - - /// - /// The time it takes for a cuffed entity to remove from itself. - /// - [DataField("breakoutTime")] - public float BreakoutTime { get; set; } = 30f; - - /// - /// If an entity being cuffed is stunned, this amount of time is subtracted from the time it takes to add/remove their cuffs. - /// - [DataField("stunBonus")] - public float StunBonus { get; set; } = 2f; - - /// - /// Will the cuffs break when removed? - /// - [DataField("breakOnRemove")] - public bool BreakOnRemove { get; set; } - - /// - /// Will the cuffs break when removed? - /// - [DataField("brokenPrototype", customTypeSerializer:typeof(PrototypeIdSerializer))] - public string? BrokenPrototype { get; set; } - - /// - /// The path of the RSI file used for the player cuffed overlay. - /// - [DataField("cuffedRSI")] - public string? CuffedRSI { get; set; } = "Objects/Misc/handcuffs.rsi"; - - /// - /// The iconstate used with the RSI file for the player cuffed overlay. - /// - [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; - - /// - /// Used to prevent DoAfter getting spammed. - /// - public bool Cuffing; - - /// - /// Update the cuffed state of an entity - /// - public async void TryUpdateCuff(EntityUid user, EntityUid target, CuffableComponent cuffs) - { - var cuffTime = CuffTime; - - if (_entities.HasComponent(target)) - { - cuffTime = MathF.Max(0.1f, cuffTime - StunBonus); - } - - if (_entities.HasComponent(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().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))); - } - } - } - } -} diff --git a/Content.Server/Cuffs/CuffableSystem.cs b/Content.Server/Cuffs/CuffableSystem.cs index efb525102d..3891941cad 100644 --- a/Content.Server/Cuffs/CuffableSystem.cs +++ b/Content.Server/Cuffs/CuffableSystem.cs @@ -1,216 +1,44 @@ -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(OnHandCountChanged); - SubscribeLocalEvent(OnUncuffAttempt); - SubscribeLocalEvent>(AddUncuffVerb); - SubscribeLocalEvent(OnCuffAfterInteract); - SubscribeLocalEvent(OnCuffMeleeHit); - SubscribeLocalEvent(OnCuffsRemoved); + SubscribeLocalEvent(OnHandcuffGetState); + SubscribeLocalEvent(OnCuffableGetState); } - private void OnCuffsRemoved(EntityUid uid, CuffableComponent component, EntRemovedFromContainerMessage args) + private void OnHandcuffGetState(EntityUid uid, HandcuffComponent component, ref ComponentGetState args) { - if (args.Container.ID == component.Container.ID) - _virtualSystem.DeleteInHandsMatching(uid, args.Entity); + args.State = new HandcuffComponentState(component.OverlayIconState, component.Cuffing); } - private void AddUncuffVerb(EntityUid uid, CuffableComponent component, GetVerbsEvent args) + private void OnCuffableGetState(EntityUid uid, CuffableComponent component, ref ComponentGetState 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; - } - - private void TryCuffing(EntityUid handcuff, EntityUid user, EntityUid target, HandcuffComponent component) - { - if (component.Cuffing || !EntityManager.TryGetComponent(target, out var cuffed)) - return; - - if (!EntityManager.TryGetComponent(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); - } - - 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); - } - } - - /// - /// Check the current amount of hands the owner has, and if there's less hands than active cuffs we remove some cuffs. - /// - 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(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(entity).WorldPosition = EntityManager.GetComponent(owner).WorldPosition; - } - - if (dirty) - { - UpdateCuffState(owner, cuffable); - } - } - } - - /// - /// Event fired on the User when the User attempts to cuff the Target. - /// Should generate popups on the User. - /// - public sealed class UncuffAttemptEvent : CancellableEntityEventArgs - { - public readonly EntityUid User; - public readonly EntityUid Target; - - public UncuffAttemptEvent(EntityUid user, EntityUid target) - { - 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. } } } diff --git a/Content.Server/Hands/Systems/HandVirtualItemSystem.cs b/Content.Server/Hands/Systems/HandVirtualItemSystem.cs index efb0fab953..6d20420c27 100644 --- a/Content.Server/Hands/Systems/HandVirtualItemSystem.cs +++ b/Content.Server/Hands/Systems/HandVirtualItemSystem.cs @@ -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(user).Coordinates; - virtualItem = EntityManager.SpawnEntity("HandVirtualItem", pos); - var virtualItemComp = EntityManager.GetComponent(virtualItem.Value); - virtualItemComp.BlockingEntity = blockingEnt; - _handsSystem.DoPickup(user, hand, virtualItem.Value); - return true; - } - - /// - /// Deletes all virtual items in a user's hands with - /// the specified blocked entity. - /// - 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); - } - } - } } } diff --git a/Content.Server/Implants/SubdermalImplantSystem.cs b/Content.Server/Implants/SubdermalImplantSystem.cs index 5df9720fa8..918a0c75be 100644 --- a/Content.Server/Implants/SubdermalImplantSystem.cs +++ b/Content.Server/Implants/SubdermalImplantSystem.cs @@ -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(component.ImplantedEntity, out var cuffs) || cuffs.Container.ContainedEntities.Count < 1) return; - if (TryComp(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 diff --git a/Content.Server/Objectives/Conditions/EscapeShuttleCondition.cs b/Content.Server/Objectives/Conditions/EscapeShuttleCondition.cs index 238b30718f..ea86c2c7d6 100644 --- a/Content.Server/Objectives/Conditions/EscapeShuttleCondition.cs +++ b/Content.Server/Objectives/Conditions/EscapeShuttleCondition.cs @@ -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; diff --git a/Content.Server/Strip/StrippableSystem.cs b/Content.Server/Strip/StrippableSystem.cs index 3289629055..282e197d83 100644 --- a/Content.Server/Strip/StrippableSystem.cs +++ b/Content.Server/Strip/StrippableSystem.cs @@ -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; } diff --git a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs index 13f824d1b5..ffb361b2f0 100644 --- a/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs +++ b/Content.Server/Weapons/Melee/MeleeWeaponSystem.cs @@ -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 index 0000000000..3829dfd528 --- /dev/null +++ b/Content.Shared/Administration/Components/DisarmProneComponent.cs @@ -0,0 +1,14 @@ +using Content.Shared.Weapons.Melee; +using Robust.Shared.GameStates; + +namespace Content.Shared.Administration.Components; + +/// +/// This is used for forcing someone to be disarmed 100% of the time. +/// +[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 index 0000000000..14c572b7b8 --- /dev/null +++ b/Content.Shared/Cuffs/Components/CuffableComponent.cs @@ -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 +{ + /// + /// The current RSI for the handcuff layer + /// + [DataField("currentRSI"), ViewVariables(VVAccess.ReadWrite)] + public string? CurrentRSI; + + /// + /// How many of this entity's hands are currently cuffed. + /// + [ViewVariables] + public int CuffedHandCount => Container.ContainedEntities.Count * 2; + + /// + /// The last pair of cuffs that was added to this entity. + /// + [ViewVariables] + public EntityUid LastAddedCuffs => Container.ContainedEntities[^1]; + + /// + /// Container of various handcuffs currently applied to the entity. + /// + [ViewVariables(VVAccess.ReadOnly)] + public Container Container = default!; + + /// + /// Whether or not the entity can still interact (is not cuffed) + /// + [DataField("canStillInteract"), ViewVariables(VVAccess.ReadWrite)] + public bool CanStillInteract = true; + + /// + /// Whether or not the entity is currently in the process of being uncuffed. + /// + [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 index 0000000000..7f0f60bfac --- /dev/null +++ b/Content.Shared/Cuffs/Components/HandcuffComponent.cs @@ -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 +{ + /// + /// The time it takes to cuff an entity. + /// + [DataField("cuffTime"), ViewVariables(VVAccess.ReadWrite)] + public float CuffTime = 3.5f; + + /// + /// The time it takes to uncuff an entity. + /// + [DataField("uncuffTime"), ViewVariables(VVAccess.ReadWrite)] + public float UncuffTime = 3.5f; + + /// + /// The time it takes for a cuffed entity to uncuff itself. + /// + [DataField("breakoutTime"), ViewVariables(VVAccess.ReadWrite)] + public float BreakoutTime = 30f; + + /// + /// If an entity being cuffed is stunned, this amount of time is subtracted from the time it takes to add/remove their cuffs. + /// + [DataField("stunBonus"), ViewVariables(VVAccess.ReadWrite)] + public float StunBonus = 2f; + + /// + /// Will the cuffs break when removed? + /// + [DataField("breakOnRemove"), ViewVariables(VVAccess.ReadWrite)] + public bool BreakOnRemove; + + /// + /// Will the cuffs break when removed? + /// + [DataField("brokenPrototype", customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] + public string? BrokenPrototype; + + /// + /// The path of the RSI file used for the player cuffed overlay. + /// + [DataField("cuffedRSI"), ViewVariables(VVAccess.ReadWrite)] + public string? CuffedRSI = "Objects/Misc/handcuffs.rsi"; + + /// + /// The iconstate used with the RSI file for the player cuffed overlay. + /// + [DataField("bodyIconState"), ViewVariables(VVAccess.ReadWrite)] + public string? OverlayIconState = "body-overlay"; + + /// + /// An opptional color specification for + /// + [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"); + + /// + /// Used to prevent DoAfter getting spammed. + /// + [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; + } +} + +/// +/// Event fired on the User when the User attempts to cuff the Target. +/// Should generate popups on the User. +/// +[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 index 9d7f341542..0000000000 --- a/Content.Shared/Cuffs/Components/SharedCuffableComponent.cs +++ /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!; - - /// - /// How many of this entity's hands are currently cuffed. - /// - [ViewVariables] - public int CuffedHandCount => Container.ContainedEntities.Count * 2; - - public EntityUid LastAddedCuffs => Container.ContainedEntities[^1]; - - public IReadOnlyList StoredEntities => Container.ContainedEntities; - - /// - /// Container of various handcuffs currently applied to the entity. - /// - [ViewVariables(VVAccess.ReadOnly)] - public Container Container { get; set; } = default!; - - protected override void Initialize() - { - base.Initialize(); - - Container = _sysMan.GetEntitySystem().EnsureContainer(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 index 2210c6b890..0000000000 --- a/Content.Shared/Cuffs/Components/SharedHandcuffComponent.cs +++ /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; - } - } - } -} diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs index b5cf8a5868..7c685281a5 100644 --- a/Content.Shared/Cuffs/SharedCuffableSystem.cs +++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs @@ -1,60 +1,155 @@ +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(OnCuffCountChanged); - SubscribeLocalEvent(OnCuffCountChanged); - SubscribeLocalEvent(OnRejuvenate); - SubscribeLocalEvent(HandleStopPull); - SubscribeLocalEvent(HandleMoveAttempt); - SubscribeLocalEvent(CheckAct); - SubscribeLocalEvent(CheckAct); - SubscribeLocalEvent(CheckAct); - SubscribeLocalEvent(OnEquipAttempt); - SubscribeLocalEvent(OnUnequipAttempt); - SubscribeLocalEvent(CheckAct); - SubscribeLocalEvent(CheckAct); - SubscribeLocalEvent(OnBeingPulledAttempt); - SubscribeLocalEvent(OnPull); - SubscribeLocalEvent(OnPull); + SubscribeLocalEvent(OnHandCountChanged); + SubscribeLocalEvent(OnUncuffAttempt); + + SubscribeLocalEvent(OnCuffsRemovedFromContainer); + SubscribeLocalEvent(OnCuffsInsertedIntoContainer); + SubscribeLocalEvent(OnRejuvenate); + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(HandleStopPull); + SubscribeLocalEvent(HandleMoveAttempt); + SubscribeLocalEvent(OnEquipAttempt); + SubscribeLocalEvent(OnUnequipAttempt); + SubscribeLocalEvent(OnBeingPulledAttempt); + SubscribeLocalEvent>(AddUncuffVerb); + SubscribeLocalEvent(OnCuffableDoAfter); + SubscribeLocalEvent(OnPull); + SubscribeLocalEvent(OnPull); + SubscribeLocalEvent(CheckAct); + SubscribeLocalEvent(CheckAct); + SubscribeLocalEvent(CheckAct); + SubscribeLocalEvent(CheckAct); + SubscribeLocalEvent(CheckAct); + + SubscribeLocalEvent(OnCuffAfterInteract); + SubscribeLocalEvent(OnCuffMeleeHit); + SubscribeLocalEvent(OnAddCuffDoAfter); + } - private void OnRejuvenate(EntityUid uid, SharedCuffableComponent component, RejuvenateEvent args) + 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(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(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 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(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); + } + } + + } + + /// + /// Check the current amount of hands the owner has, and if there's less hands than active cuffs we remove some cuffs. + /// + private void OnHandCountChanged(HandCountChangedEvent message) + { + var owner = message.Sender; + + if (!TryComp(owner, out CuffableComponent? cuffable) || + !cuffable.Initialized) + { + return; + } + + var dirty = false; + var handCount = CompOrNull(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); + } + } + + /// + /// Adds virtual cuff items to the user's hands. + /// + 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(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(hand.HeldEntity)) + continue; + + _hands.DoDrop(uid, hand, true, handsComponent); + freeHands++; + if (freeHands == 2) + break; + } + + if (_handVirtualItem.TrySpawnVirtualItemInHand(handcuff, uid, out var virtItem1)) + EnsureComp(virtItem1.Value); + + if (_handVirtualItem.TrySpawnVirtualItemInHand(handcuff, uid, out var virtItem2)) + EnsureComp(virtItem2.Value); + } + + /// + /// Add a set of cuffs to an existing CuffedComponent. + /// + 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(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(target)) + cuffTime = MathF.Max(0.1f, cuffTime - handcuffComponent.StunBonus); + + if (HasComp(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); + } + + /// + /// 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. + /// + /// + /// The cuffed entity + /// Optional param for the handcuff entity to remove from the cuffed entity. If null, uses the most recently added handcuff entity. + /// + /// + 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 GetAllCuffs(CuffableComponent component) + { + return component.Container.ContainedEntities; + } } } diff --git a/Content.Shared/Hands/SharedHandVirtualItemSystem.cs b/Content.Shared/Hands/SharedHandVirtualItemSystem.cs index f4d6558d33..e33ffdf051 100644 --- a/Content.Shared/Hands/SharedHandVirtualItemSystem.cs +++ b/Content.Shared/Hands/SharedHandVirtualItemSystem.cs @@ -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(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(virtualItem.Value); + virtualItemComp.BlockingEntity = blockingEnt; + _hands.DoPickup(user, hand, virtualItem.Value); + return true; + } + + + /// + /// Deletes all virtual items in a user's hands with + /// the specified blocked entity. + /// + 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); } } diff --git a/Resources/Locale/en-US/cuffs/components/handcuff-component.ftl b/Resources/Locale/en-US/cuffs/components/handcuff-component.ftl index 2fa13799cc..8c024f7943 100644 --- a/Resources/Locale/en-US/cuffs/components/handcuff-component.ftl +++ b/Resources/Locale/en-US/cuffs/components/handcuff-component.ftl @@ -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. diff --git a/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml b/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml index 83109c28f4..2d7c6b6c07 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml @@ -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