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 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!; /// /// How many of this entity's hands are currently cuffed. /// [ViewVariables] public int CuffedHandCount => Container.ContainedEntities.Count * 2; public EntityUid LastAddedCuffs => Container.ContainedEntities[^1]; public IReadOnlyList StoredEntities => Container.ContainedEntities; private bool _uncuffing; protected override void Initialize() { base.Initialize(); Owner.EnsureComponentWarn(); } public override ComponentState GetComponentState() { // there are 2 approaches i can think of to handle the handcuff overlay on players // 1 - make the current RSI the handcuff type that's currently active. all handcuffs on the player will appear the same. // 2 - allow for several different player overlays for each different cuff type. // approach #2 would be more difficult/time consuming to do and the payoff doesn't make it worth it. // right now we're doing approach #1. if (CuffedHandCount > 0) { if (_entMan.TryGetComponent(LastAddedCuffs, out var cuffs)) { return new CuffableComponentState(CuffedHandCount, CanStillInteract, cuffs.CuffedRSI, $"{cuffs.OverlayIconState}-{CuffedHandCount}", cuffs.Color); // the iconstate is formatted as blah-2, blah-4, blah-6, etc. // the number corresponds to how many hands are cuffed. } } return new CuffableComponentState(CuffedHandCount, CanStillInteract, "/Objects/Misc/handcuffs.rsi", "body-overlay-2", Color.White); } /// /// Add a set of cuffs to an existing CuffedComponent. /// public bool TryAddNewCuffs(EntityUid user, EntityUid handcuff) { if (!_entMan.HasComponent(handcuff)) { Logger.Warning($"Handcuffs being applied to player are missing a {nameof(HandcuffComponent)}!"); return false; } if (!EntitySystem.Get().InRangeUnobstructed(handcuff, Owner)) { Logger.Warning("Handcuffs being applied to player are obstructed or too far away! This should not happen!"); return true; } var sys = _entMan.EntitySysManager.GetEntitySystem(); // Success! sys.TryDrop(user, handcuff); Container.Insert(handcuff); CanStillInteract = _entMan.TryGetComponent(Owner, out HandsComponent? ownerHands) && ownerHands.Hands.Count() > CuffedHandCount; _entMan.EntitySysManager.GetEntitySystem().UpdateCanMove(Owner); var ev = new CuffedStateChangeEvent(); _entMan.EventBus.RaiseLocalEvent(Owner, ref ev, true); UpdateAlert(); UpdateHeldItems(handcuff); Dirty(_entMan); return true; } public void CuffedStateChanged() { UpdateAlert(); var ev = new CuffedStateChangeEvent(); _entMan.EventBus.RaiseLocalEvent(Owner, ref ev, true); } /// /// Adds virtual cuff items to the user's hands. /// public void UpdateHeldItems(EntityUid handcuff) { // TODO when ecs-ing this, we probably don't just want to use the generic virtual-item entity, and instead // want to add our own item, so that use-in-hand triggers an uncuff attempt and the like. if (!_entMan.TryGetComponent(Owner, out HandsComponent? handsComponent)) return; var handSys = _entMan.EntitySysManager.GetEntitySystem(); var freeHands = 0; foreach (var hand in handSys.EnumerateHands(Owner, handsComponent)) { if (hand.HeldEntity == null) { freeHands++; continue; } // Is this entity removable? (it might be an existing handcuff blocker) if (_entMan.HasComponent(hand.HeldEntity)) continue; handSys.DoDrop(Owner, hand, true, handsComponent); freeHands++; if (freeHands == 2) break; } var virtSys = _entMan.EntitySysManager.GetEntitySystem(); if (virtSys.TrySpawnVirtualItemInHand(handcuff, Owner, out var virtItem1)) _entMan.EnsureComponent(virtItem1.Value); if (virtSys.TrySpawnVirtualItemInHand(handcuff, Owner, out var virtItem2)) _entMan.EnsureComponent(virtItem2.Value); } /// /// Updates the status effect indicator on the HUD. /// private void UpdateAlert() { if (CanStillInteract) { EntitySystem.Get().ClearAlert(Owner, AlertType.Handcuffed); } else { EntitySystem.Get().ShowAlert(Owner, AlertType.Handcuffed); } } /// /// Attempt to uncuff a cuffed entity. Can be called by the cuffed entity, or another entity trying to help uncuff them. /// If the uncuffing succeeds, the cuffs will drop on the floor. /// /// The cuffed entity /// Optional param for the handcuff entity to remove from the cuffed entity. If null, uses the most recently added handcuff entity. public async void TryUncuff(EntityUid user, EntityUid? cuffsToRemove = null) { if (_uncuffing) return; var isOwner = user == Owner; if (cuffsToRemove == null) { if (Container.ContainedEntities.Count == 0) { return; } cuffsToRemove = LastAddedCuffs; } else { if (!Container.ContainedEntities.Contains(cuffsToRemove.Value)) { Logger.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!"); } } if (!_entMan.TryGetComponent(cuffsToRemove, out var cuff)) { Logger.Warning($"A user is trying to remove handcuffs without a {nameof(HandcuffComponent)}. This should never happen!"); return; } var attempt = new UncuffAttemptEvent(user, Owner); _entMan.EventBus.RaiseLocalEvent(user, attempt, true); if (attempt.Cancelled) { return; } if (!isOwner && !EntitySystem.Get().InRangeUnobstructed(user, Owner)) { user.PopupMessage(Loc.GetString("cuffable-component-cannot-remove-cuffs-too-far-message")); return; } user.PopupMessage(Loc.GetString("cuffable-component-start-removing-cuffs-message")); if (isOwner) { SoundSystem.Play(cuff.StartBreakoutSound.GetSound(), Filter.Pvs(Owner, entityManager: _entMan), Owner); } else { SoundSystem.Play(cuff.StartUncuffSound.GetSound(), Filter.Pvs(Owner, entityManager: _entMan), Owner); } var uncuffTime = isOwner ? cuff.BreakoutTime : cuff.UncuffTime; var doAfterEventArgs = new DoAfterEventArgs(user, uncuffTime, target: Owner) { BreakOnUserMove = true, BreakOnTargetMove = true, BreakOnDamage = true, BreakOnStun = true, NeedHand = true }; var doAfterSystem = EntitySystem.Get(); _uncuffing = true; var result = await doAfterSystem.WaitDoAfter(doAfterEventArgs); _uncuffing = false; if (result != DoAfterStatus.Cancelled) { Uncuff(user, cuffsToRemove.Value, cuff, isOwner); } else { user.PopupMessage(Loc.GetString("cuffable-component-remove-cuffs-fail-message")); } } //Lord forgive me for putting this here //Cuff ECS when public void Uncuff(EntityUid user, EntityUid cuffsToRemove, HandcuffComponent cuff, bool isOwner) { SoundSystem.Play(cuff.EndUncuffSound.GetSound(), Filter.Pvs(Owner), Owner); _entMan.EntitySysManager.GetEntitySystem().DeleteInHandsMatching(user, cuffsToRemove); if (cuff.BreakOnRemove) { _entMan.QueueDeleteEntity(cuffsToRemove); var trash = _entMan.SpawnEntity(cuff.BrokenPrototype, MapCoordinates.Nullspace); _entMan.EntitySysManager.GetEntitySystem().PickupOrDrop(user, trash); } else { _entMan.EntitySysManager.GetEntitySystem().PickupOrDrop(user, cuffsToRemove); } CanStillInteract = _entMan.TryGetComponent(Owner, out HandsComponent? handsComponent) && handsComponent.SortedHands.Count() > CuffedHandCount; _entMan.EntitySysManager.GetEntitySystem().UpdateCanMove(Owner); var ev = new CuffedStateChangeEvent(); _entMan.EventBus.RaiseLocalEvent(Owner, ref ev, true); UpdateAlert(); Dirty(_entMan); 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))); } } } } }