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