diff --git a/Content.Client/GameObjects/Components/ActionBlocking/HandcuffComponent.cs b/Content.Client/GameObjects/Components/ActionBlocking/HandcuffComponent.cs index adc7488425..9cee1ac181 100644 --- a/Content.Client/GameObjects/Components/ActionBlocking/HandcuffComponent.cs +++ b/Content.Client/GameObjects/Components/ActionBlocking/HandcuffComponent.cs @@ -1,4 +1,5 @@ -using Content.Shared.GameObjects.Components.ActionBlocking; +#nullable enable +using Content.Shared.GameObjects.Components.ActionBlocking; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.GameObjects; @@ -6,9 +7,10 @@ using Robust.Shared.GameObjects; namespace Content.Client.GameObjects.Components.ActionBlocking { [RegisterComponent] + [ComponentReference(typeof(SharedHandcuffComponent))] public class HandcuffComponent : SharedHandcuffComponent { - public override void HandleComponentState(ComponentState curState, ComponentState nextState) + public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) { if (curState is not HandcuffedComponentState state) { diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/CuffUnitTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs similarity index 95% rename from Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/CuffUnitTest.cs rename to Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs index 2259df9c43..5910ba4a26 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/CuffUnitTest.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs @@ -18,7 +18,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking [TestFixture] [TestOf(typeof(CuffableComponent))] [TestOf(typeof(HandcuffComponent))] - public class CuffUnitTest : ContentIntegrationTest + public class HandCuffTest : ContentIntegrationTest { private const string PROTOTYPES = @" - type: entity @@ -77,7 +77,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking Assert.True(secondCuffs.TryGetComponent(out secondHandcuff!), $"Second handcuffs has no {nameof(HandcuffComponent)}"); // Test to ensure cuffed players register the handcuffs - cuffed.AddNewCuffs(cuffs); + cuffed.TryAddNewCuffs(human, cuffs); 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 @@ -86,7 +86,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking Assert.True(cuffed.CuffedHandCount == 2 && hands.Hands.Count() == 4, "Player doesn't have correct amount of hands cuffed"); // Test to give a player with 4 hands 2 sets of cuffs - cuffed.AddNewCuffs(secondCuffs); + cuffed.TryAddNewCuffs(human, secondCuffs); Assert.True(cuffed.CuffedHandCount == 4, "Player doesn't have correct amount of hands cuffed"); }); diff --git a/Content.Server/Alert/Click/RemoveCuffs.cs b/Content.Server/Alert/Click/RemoveCuffs.cs new file mode 100644 index 0000000000..d4882a93c3 --- /dev/null +++ b/Content.Server/Alert/Click/RemoveCuffs.cs @@ -0,0 +1,25 @@ +#nullable enable +using Content.Server.GameObjects.Components.ActionBlocking; +using Content.Shared.Alert; +using JetBrains.Annotations; +using Robust.Shared.Serialization; + +namespace Content.Server.Alert.Click +{ + /// + /// Try to remove handcuffs from yourself + /// + [UsedImplicitly] + public class RemoveCuffs : IAlertClick + { + public void ExposeData(ObjectSerializer serializer) {} + + public void AlertClicked(ClickAlertEventArgs args) + { + if (args.Player.TryGetComponent(out CuffableComponent? cuffableComponent)) + { + cuffableComponent.TryUncuff(args.Player); + } + } + } +} diff --git a/Content.Server/GameObjects/Components/ActionBlocking/CuffableComponent.cs b/Content.Server/GameObjects/Components/ActionBlocking/CuffableComponent.cs index e82dada792..61354f3538 100644 --- a/Content.Server/GameObjects/Components/ActionBlocking/CuffableComponent.cs +++ b/Content.Server/GameObjects/Components/ActionBlocking/CuffableComponent.cs @@ -1,15 +1,13 @@ -using System; +#nullable enable +using System; using System.Collections.Generic; using System.Linq; using Content.Server.GameObjects.Components.GUI; -using Content.Server.GameObjects.Components.Items.Storage; using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.EntitySystems.DoAfter; using Content.Server.Interfaces.GameObjects.Components.Items; using Content.Shared.Alert; using Content.Shared.GameObjects.Components.ActionBlocking; -using Content.Shared.GameObjects.Components.Mobs; -using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems.ActionBlocker; using Content.Shared.GameObjects.Verbs; using Content.Shared.Interfaces; @@ -46,24 +44,20 @@ namespace Content.Server.GameObjects.Components.ActionBlocking [ViewVariables(VVAccess.ReadOnly)] private Container _container = default!; - private float _interactRange; - private IHandsComponent _hands; + // TODO: Make a component message + public event Action? OnCuffedStateChanged; - public event Action OnCuffedStateChanged; + private bool _uncuffing; public override void Initialize() { base.Initialize(); _container = ContainerManagerComponent.Ensure(Name, Owner); - _interactRange = SharedInteractionSystem.InteractionRange / 2; Owner.EntityManager.EventBus.SubscribeEvent(EventSource.Local, this, HandleHandCountChange); - if (!Owner.TryGetComponent(out _hands)) - { - Logger.Warning("Player does not have an IHandsComponent!"); - } + Owner.EnsureComponentWarn(); } public override ComponentState GetComponentState() @@ -99,27 +93,35 @@ namespace Content.Server.GameObjects.Components.ActionBlocking /// Add a set of cuffs to an existing CuffedComponent. /// /// - public void AddNewCuffs(IEntity handcuff) + public bool TryAddNewCuffs(IEntity user, IEntity handcuff) { if (!handcuff.HasComponent()) { Logger.Warning($"Handcuffs being applied to player are missing a {nameof(HandcuffComponent)}!"); - return; + return false; } - if (!handcuff.InRangeUnobstructed(Owner, _interactRange)) + if (!handcuff.InRangeUnobstructed(Owner)) { Logger.Warning("Handcuffs being applied to player are obstructed or too far away! This should not happen!"); - return; + return true; + } + + // Success! + if (user.TryGetComponent(out HandsComponent? handsComponent) && handsComponent.IsHolding(handcuff)) + { + // Good lord handscomponent is scuffed, I hope some smug person will fix it someday + handsComponent.Drop(handcuff); } _container.Insert(handcuff); - CanStillInteract = _hands.Hands.Count() > CuffedHandCount; + CanStillInteract = Owner.TryGetComponent(out HandsComponent? ownerHands) && ownerHands.Hands.Count() > CuffedHandCount; OnCuffedStateChanged?.Invoke(); UpdateAlert(); UpdateHeldItems(); Dirty(); + return true; } /// @@ -128,7 +130,7 @@ namespace Content.Server.GameObjects.Components.ActionBlocking private void UpdateHandCount() { var dirty = false; - var handCount = _hands.Hands.Count(); + var handCount = Owner.TryGetComponent(out HandsComponent? handsComponent) ? handsComponent.Hands.Count() : 0; while (CuffedHandCount > handCount && CuffedHandCount > 0) { @@ -142,7 +144,7 @@ namespace Content.Server.GameObjects.Components.ActionBlocking if (dirty) { CanStillInteract = handCount > CuffedHandCount; - OnCuffedStateChanged.Invoke(); + OnCuffedStateChanged?.Invoke(); Dirty(); } } @@ -160,17 +162,19 @@ namespace Content.Server.GameObjects.Components.ActionBlocking /// public void UpdateHeldItems() { - var itemCount = _hands.GetAllHeldItems().Count(); - var freeHandCount = _hands.Hands.Count() - CuffedHandCount; + if (!Owner.TryGetComponent(out HandsComponent? handsComponent)) return; + + var itemCount = handsComponent.GetAllHeldItems().Count(); + var freeHandCount = handsComponent.Hands.Count() - CuffedHandCount; if (freeHandCount < itemCount) { - foreach (var item in _hands.GetAllHeldItems()) + foreach (var item in handsComponent.GetAllHeldItems()) { if (freeHandCount < itemCount) { freeHandCount++; - _hands.Drop(item.Owner, false); + handsComponent.Drop(item.Owner, false); } else { @@ -185,7 +189,7 @@ namespace Content.Server.GameObjects.Components.ActionBlocking /// private void UpdateAlert() { - if (Owner.TryGetComponent(out ServerAlertsComponent status)) + if (Owner.TryGetComponent(out ServerAlertsComponent? status)) { if (CanStillInteract) { @@ -204,8 +208,10 @@ namespace Content.Server.GameObjects.Components.ActionBlocking /// /// 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(IEntity user, IEntity cuffsToRemove = null) + public async void TryUncuff(IEntity user, IEntity? cuffsToRemove = null) { + if (_uncuffing) return; + var isOwner = user == Owner; if (cuffsToRemove == null) @@ -232,13 +238,13 @@ namespace Content.Server.GameObjects.Components.ActionBlocking return; } - if (!isOwner && !user.InRangeUnobstructed(Owner, _interactRange)) + if (!isOwner && !user.InRangeUnobstructed(Owner)) { user.PopupMessage(Loc.GetString("You are too far away to remove the cuffs.")); return; } - if (!cuffsToRemove.InRangeUnobstructed(Owner, _interactRange)) + if (!cuffsToRemove.InRangeUnobstructed(Owner)) { Logger.Warning("Handcuffs being removed from player are obstructed or too far away! This should not happen!"); return; @@ -247,7 +253,17 @@ namespace Content.Server.GameObjects.Components.ActionBlocking user.PopupMessage(Loc.GetString("You start removing the cuffs.")); var audio = EntitySystem.Get(); - audio.PlayFromEntity(isOwner ? cuff.StartBreakoutSound : cuff.StartUncuffSound, Owner); + if (isOwner) + { + if (cuff.StartBreakoutSound != null) + audio.PlayFromEntity(cuff.StartBreakoutSound, Owner); + } + else + { + if (cuff.StartUncuffSound != null) + audio.PlayFromEntity(cuff.StartUncuffSound, Owner); + } + var uncuffTime = isOwner ? cuff.BreakoutTime : cuff.UncuffTime; var doAfterEventArgs = new DoAfterEventArgs(user, uncuffTime) @@ -259,11 +275,16 @@ namespace Content.Server.GameObjects.Components.ActionBlocking }; var doAfterSystem = EntitySystem.Get(); + _uncuffing = true; + var result = await doAfterSystem.DoAfter(doAfterEventArgs); + _uncuffing = false; + if (result != DoAfterStatus.Cancelled) { - audio.PlayFromEntity(cuff.EndUncuffSound, Owner); + if (cuff.EndUncuffSound != null) + audio.PlayFromEntity(cuff.EndUncuffSound, Owner); _container.ForceRemove(cuffsToRemove); cuffsToRemove.Transform.AttachToGridOrMap(); @@ -276,14 +297,14 @@ namespace Content.Server.GameObjects.Components.ActionBlocking cuffsToRemove.Name = cuff.BrokenName; cuffsToRemove.Description = cuff.BrokenDesc; - if (cuffsToRemove.TryGetComponent(out var sprite)) + if (cuffsToRemove.TryGetComponent(out var sprite) && cuff.BrokenState != null) { sprite.LayerSetState(0, cuff.BrokenState); // TODO: safety check to see if RSI contains the state? } } - CanStillInteract = _hands.Hands.Count() > CuffedHandCount; - OnCuffedStateChanged.Invoke(); + CanStillInteract = Owner.TryGetComponent(out HandsComponent? handsComponent) && handsComponent.Hands.Count() > CuffedHandCount; + OnCuffedStateChanged?.Invoke(); UpdateAlert(); Dirty(); diff --git a/Content.Server/GameObjects/Components/ActionBlocking/HandcuffComponent.cs b/Content.Server/GameObjects/Components/ActionBlocking/HandcuffComponent.cs index 9e05ab7cd1..51f0addcee 100644 --- a/Content.Server/GameObjects/Components/ActionBlocking/HandcuffComponent.cs +++ b/Content.Server/GameObjects/Components/ActionBlocking/HandcuffComponent.cs @@ -1,10 +1,10 @@ -using System; +#nullable enable +using System; using System.Threading.Tasks; using Content.Server.GameObjects.Components.GUI; using Content.Server.GameObjects.Components.Mobs; using Content.Server.GameObjects.EntitySystems.DoAfter; using Content.Shared.GameObjects.Components.ActionBlocking; -using Content.Shared.GameObjects.EntitySystems; using Content.Shared.GameObjects.EntitySystems.ActionBlocker; using Content.Shared.Interfaces; using Content.Shared.Interfaces.GameObjects.Components; @@ -14,7 +14,6 @@ using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Localization; -using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Serialization; using Robust.Shared.ViewVariables; @@ -22,6 +21,7 @@ using Robust.Shared.ViewVariables; namespace Content.Server.GameObjects.Components.ActionBlocking { [RegisterComponent] + [ComponentReference(typeof(SharedHandcuffComponent))] public class HandcuffComponent : SharedHandcuffComponent, IAfterInteract { /// @@ -58,31 +58,31 @@ namespace Content.Server.GameObjects.Components.ActionBlocking /// The path of the RSI file used for the player cuffed overlay. /// [ViewVariables] - public string CuffedRSI { get; set; } + public string? CuffedRSI { get; set; } /// /// The iconstate used with the RSI file for the player cuffed overlay. /// [ViewVariables] - public string OverlayIconState { get; set; } + public string? OverlayIconState { get; set; } /// /// The iconstate used for broken handcuffs /// [ViewVariables] - public string BrokenState { get; set; } + public string? BrokenState { get; set; } /// /// The iconstate used for broken handcuffs /// [ViewVariables] - public string BrokenName { get; set; } + public string BrokenName { get; set; } = default!; /// /// The iconstate used for broken handcuffs /// [ViewVariables] - public string BrokenDesc { get; set; } + public string BrokenDesc { get; set; } = default!; [ViewVariables] public bool Broken @@ -102,25 +102,20 @@ namespace Content.Server.GameObjects.Components.ActionBlocking } } - public string StartCuffSound { get; set; } - public string EndCuffSound { get; set; } - public string StartBreakoutSound { get; set; } - public string StartUncuffSound { get; set; } - public string EndUncuffSound { get; set; } + public string? StartCuffSound { get; set; } + public string? EndCuffSound { get; set; } + public string? StartBreakoutSound { get; set; } + public string? StartUncuffSound { get; set; } + public string? EndUncuffSound { get; set; } public Color Color { get; set; } // Non-exposed data fields private bool _isBroken = false; - private float _interactRange; - private AudioSystem _audioSystem; - public override void Initialize() - { - base.Initialize(); - - _audioSystem = EntitySystem.Get(); - _interactRange = SharedInteractionSystem.InteractionRange / 2; - } + /// + /// Used to prevent DoAfter getting spammed. + /// + private bool _cuffing; public override void ExposeData(ObjectSerializer serializer) { @@ -150,6 +145,8 @@ namespace Content.Server.GameObjects.Components.ActionBlocking async Task IAfterInteract.AfterInteract(AfterInteractEventArgs eventArgs) { + if (_cuffing) return true; + if (eventArgs.Target == null || !ActionBlockerSystem.CanUse(eventArgs.User) || !eventArgs.Target.TryGetComponent(out var cuffed)) { return false; @@ -179,7 +176,7 @@ namespace Content.Server.GameObjects.Components.ActionBlocking return true; } - if (!eventArgs.InRangeUnobstructed(_interactRange, ignoreInsideBlocker: true)) + if (!eventArgs.InRangeUnobstructed(ignoreInsideBlocker: true)) { eventArgs.User.PopupMessage(Loc.GetString("You are too far away to use the cuffs!")); return true; @@ -187,7 +184,9 @@ namespace Content.Server.GameObjects.Components.ActionBlocking eventArgs.User.PopupMessage(Loc.GetString("You start cuffing {0:theName}.", eventArgs.Target)); eventArgs.User.PopupMessage(eventArgs.Target, Loc.GetString("{0:theName} starts cuffing you!", eventArgs.User)); - _audioSystem.PlayFromEntity(StartCuffSound, Owner); + + if (StartCuffSound != null) + EntitySystem.Get().PlayFromEntity(StartCuffSound, Owner); TryUpdateCuff(eventArgs.User, eventArgs.Target, cuffed); return true; @@ -214,22 +213,21 @@ namespace Content.Server.GameObjects.Components.ActionBlocking NeedHand = true }; + _cuffing = true; + var result = await EntitySystem.Get().DoAfter(doAfterEventArgs); + _cuffing = false; + if (result != DoAfterStatus.Cancelled) { - _audioSystem.PlayFromEntity(EndCuffSound, Owner); - user.PopupMessage(Loc.GetString("You successfully cuff {0:theName}.", target)); - target.PopupMessage(Loc.GetString("You have been cuffed by {0:theName}!", user)); + if (cuffs.TryAddNewCuffs(user, Owner)) + { + if (EndCuffSound != null) + EntitySystem.Get().PlayFromEntity(EndCuffSound, Owner); - if (user.TryGetComponent(out var hands)) - { - hands.Drop(Owner); - cuffs.AddNewCuffs(Owner); - } - else - { - Logger.Warning("Unable to remove handcuffs from player's hands! This should not happen!"); + user.PopupMessage(Loc.GetString("You successfully cuff {0:theName}.", target)); + target.PopupMessage(Loc.GetString("You have been cuffed by {0:theName}!", user)); } } else diff --git a/Content.Shared/GameObjects/Components/ActionBlocking/SharedHandcuffComponent.cs b/Content.Shared/GameObjects/Components/ActionBlocking/SharedHandcuffComponent.cs index 6ab252ba2b..e55211987a 100644 --- a/Content.Shared/GameObjects/Components/ActionBlocking/SharedHandcuffComponent.cs +++ b/Content.Shared/GameObjects/Components/ActionBlocking/SharedHandcuffComponent.cs @@ -4,7 +4,7 @@ using Robust.Shared.Serialization; namespace Content.Shared.GameObjects.Components.ActionBlocking { - public class SharedHandcuffComponent : Component + public abstract class SharedHandcuffComponent : Component { public override string Name => "Handcuff"; public override uint? NetID => ContentNetIDs.HANDCUFFS; diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml index 5be4593ab4..1c2450b7c8 100644 --- a/Resources/Prototypes/Alerts/alerts.yml +++ b/Resources/Prototypes/Alerts/alerts.yml @@ -89,9 +89,10 @@ - type: alert alertType: Handcuffed + onClick: !type:RemoveCuffs { } icon: /Textures/Interface/Alerts/Handcuffed/Handcuffed.png name: "[color=yellow]Handcuffed[/color]" - description: "You're [color=yellow]handcuffed[/color] and can't use your hands. If anyone drags you, you won't be able to resist.." + description: "You're [color=yellow]handcuffed[/color] and can't use your hands. If anyone drags you, you won't be able to resist." - type: alert alertType: Buckled