2023-03-13 19:34:26 -04:00
using System.Linq ;
2022-04-10 16:48:11 +12:00
using Content.Shared.ActionBlocker ;
2023-03-13 19:34:26 -04:00
using Content.Shared.Administration.Components ;
using Content.Shared.Administration.Logs ;
2023-01-21 02:48:19 +13:00
using Content.Shared.Alert ;
2023-06-28 23:19:56 -04:00
using Content.Shared.Buckle.Components ;
2025-03-29 21:09:34 +01:00
using Content.Shared.CombatMode ;
2021-06-17 00:37:05 +10:00
using Content.Shared.Cuffs.Components ;
2023-03-13 19:34:26 -04:00
using Content.Shared.Database ;
using Content.Shared.DoAfter ;
2023-02-14 00:29:34 +11:00
using Content.Shared.Hands ;
2023-01-21 02:48:19 +13:00
using Content.Shared.Hands.Components ;
2023-03-13 19:34:26 -04:00
using Content.Shared.Hands.EntitySystems ;
using Content.Shared.IdentityManagement ;
using Content.Shared.Interaction ;
using Content.Shared.Interaction.Components ;
2021-10-21 13:03:14 +11:00
using Content.Shared.Interaction.Events ;
using Content.Shared.Inventory.Events ;
2024-01-14 06:18:47 -04:00
using Content.Shared.Inventory.VirtualItem ;
2021-10-21 13:03:14 +11:00
using Content.Shared.Item ;
2022-06-24 17:44:30 +10:00
using Content.Shared.Movement.Events ;
2024-03-19 14:30:56 +11:00
using Content.Shared.Movement.Pulling.Events ;
2023-03-13 19:34:26 -04:00
using Content.Shared.Popups ;
2022-06-15 21:10:03 -04:00
using Content.Shared.Pulling.Events ;
2023-01-21 02:48:19 +13:00
using Content.Shared.Rejuvenate ;
2023-03-13 19:34:26 -04:00
using Content.Shared.Stunnable ;
2024-02-26 03:41:20 +01:00
using Content.Shared.Timing ;
2023-03-13 19:34:26 -04:00
using Content.Shared.Verbs ;
using Content.Shared.Weapons.Melee.Events ;
2023-11-27 22:12:34 +11:00
using Robust.Shared.Audio.Systems ;
2023-01-21 02:48:19 +13:00
using Robust.Shared.Containers ;
2023-03-13 19:34:26 -04:00
using Robust.Shared.Network ;
using Robust.Shared.Player ;
2023-04-03 13:13:48 +12:00
using Robust.Shared.Serialization ;
2024-03-26 21:15:08 +02:00
using Robust.Shared.Utility ;
2024-03-19 14:30:56 +11:00
using PullableComponent = Content . Shared . Movement . Pulling . Components . PullableComponent ;
2021-06-17 00:37:05 +10:00
namespace Content.Shared.Cuffs
{
2023-04-03 13:13:48 +12:00
// TODO remove all the IsServer() checks.
2023-08-22 18:14:33 -07:00
public abstract partial class SharedCuffableSystem : EntitySystem
2021-06-17 00:37:05 +10:00
{
2023-03-13 19:34:26 -04:00
[Dependency] private readonly IComponentFactory _componentFactory = default ! ;
[Dependency] private readonly INetManager _net = default ! ;
[Dependency] private readonly ISharedAdminLogManager _adminLog = default ! ;
2023-03-27 00:15:32 +11:00
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default ! ;
[Dependency] private readonly AlertsSystem _alerts = default ! ;
2023-03-13 19:34:26 -04:00
[Dependency] private readonly SharedAudioSystem _audio = default ! ;
[Dependency] private readonly SharedContainerSystem _container = default ! ;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default ! ;
[Dependency] private readonly SharedHandsSystem _hands = default ! ;
2024-01-14 06:18:47 -04:00
[Dependency] private readonly SharedVirtualItemSystem _virtualItem = default ! ;
2023-03-13 19:34:26 -04:00
[Dependency] private readonly SharedInteractionSystem _interaction = default ! ;
[Dependency] private readonly SharedPopupSystem _popup = default ! ;
[Dependency] private readonly SharedTransformSystem _transform = default ! ;
2024-02-26 03:41:20 +01:00
[Dependency] private readonly UseDelaySystem _delay = default ! ;
2025-03-29 21:09:34 +01:00
[Dependency] private readonly SharedCombatModeSystem _combatMode = default ! ;
2022-04-10 16:48:11 +12:00
2021-06-17 00:37:05 +10:00
public override void Initialize ( )
{
base . Initialize ( ) ;
2023-03-13 19:34:26 -04:00
2024-07-26 14:48:03 -04:00
SubscribeLocalEvent < CuffableComponent , HandCountChangedEvent > ( OnHandCountChanged ) ;
2023-03-13 19:34:26 -04:00
SubscribeLocalEvent < UncuffAttemptEvent > ( OnUncuffAttempt ) ;
SubscribeLocalEvent < CuffableComponent , EntRemovedFromContainerMessage > ( OnCuffsRemovedFromContainer ) ;
SubscribeLocalEvent < CuffableComponent , EntInsertedIntoContainerMessage > ( OnCuffsInsertedIntoContainer ) ;
SubscribeLocalEvent < CuffableComponent , RejuvenateEvent > ( OnRejuvenate ) ;
SubscribeLocalEvent < CuffableComponent , ComponentInit > ( OnStartup ) ;
2024-03-19 14:30:56 +11:00
SubscribeLocalEvent < CuffableComponent , AttemptStopPullingEvent > ( HandleStopPull ) ;
2024-08-07 01:15:35 -04:00
SubscribeLocalEvent < CuffableComponent , RemoveCuffsAlertEvent > ( OnRemoveCuffsAlert ) ;
2023-03-13 19:34:26 -04:00
SubscribeLocalEvent < CuffableComponent , UpdateCanMoveEvent > ( HandleMoveAttempt ) ;
SubscribeLocalEvent < CuffableComponent , IsEquippingAttemptEvent > ( OnEquipAttempt ) ;
SubscribeLocalEvent < CuffableComponent , IsUnequippingAttemptEvent > ( OnUnequipAttempt ) ;
SubscribeLocalEvent < CuffableComponent , BeingPulledAttemptEvent > ( OnBeingPulledAttempt ) ;
2023-06-28 23:19:56 -04:00
SubscribeLocalEvent < CuffableComponent , BuckleAttemptEvent > ( OnBuckleAttemptEvent ) ;
2024-06-20 03:14:18 +12:00
SubscribeLocalEvent < CuffableComponent , UnbuckleAttemptEvent > ( OnUnbuckleAttemptEvent ) ;
2023-03-13 19:34:26 -04:00
SubscribeLocalEvent < CuffableComponent , GetVerbsEvent < Verb > > ( AddUncuffVerb ) ;
2023-04-03 13:13:48 +12:00
SubscribeLocalEvent < CuffableComponent , UnCuffDoAfterEvent > ( OnCuffableDoAfter ) ;
2023-03-13 19:34:26 -04:00
SubscribeLocalEvent < CuffableComponent , PullStartedMessage > ( OnPull ) ;
SubscribeLocalEvent < CuffableComponent , PullStoppedMessage > ( OnPull ) ;
SubscribeLocalEvent < CuffableComponent , DropAttemptEvent > ( CheckAct ) ;
SubscribeLocalEvent < CuffableComponent , PickupAttemptEvent > ( CheckAct ) ;
SubscribeLocalEvent < CuffableComponent , AttackAttemptEvent > ( CheckAct ) ;
SubscribeLocalEvent < CuffableComponent , UseAttemptEvent > ( CheckAct ) ;
2024-06-19 02:30:41 +12:00
SubscribeLocalEvent < CuffableComponent , InteractionAttemptEvent > ( CheckInteract ) ;
2023-03-13 19:34:26 -04:00
SubscribeLocalEvent < HandcuffComponent , AfterInteractEvent > ( OnCuffAfterInteract ) ;
SubscribeLocalEvent < HandcuffComponent , MeleeHitEvent > ( OnCuffMeleeHit ) ;
2023-04-03 13:13:48 +12:00
SubscribeLocalEvent < HandcuffComponent , AddCuffDoAfterEvent > ( OnAddCuffDoAfter ) ;
2023-10-16 01:31:03 -04:00
SubscribeLocalEvent < HandcuffComponent , VirtualItemDeletedEvent > ( OnCuffVirtualItemDeleted ) ;
2023-03-13 19:34:26 -04:00
}
2024-06-19 02:30:41 +12:00
private void CheckInteract ( Entity < CuffableComponent > ent , ref InteractionAttemptEvent args )
{
if ( ! ent . Comp . CanStillInteract )
args . Cancelled = true ;
}
2023-03-13 19:34:26 -04:00
private void OnUncuffAttempt ( ref UncuffAttemptEvent args )
{
if ( args . Cancelled )
return ;
2024-03-26 21:15:08 +02:00
2023-03-13 19:34:26 -04:00
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 )
{
2024-03-26 21:15:08 +02:00
if ( ! TryComp < CuffableComponent > ( args . User , out var cuffable ) )
2023-03-13 19:34:26 -04:00
{
2024-03-26 21:15:08 +02:00
DebugTools . Assert ( $"{args.User} tried to uncuff themselves but they are not cuffable." ) ;
return ;
2023-03-13 19:34:26 -04:00
}
2024-03-26 21:15:08 +02:00
// We temporarily allow interactions so the cuffable system does not block itself.
// It's assumed that this will always be false.
// Otherwise they would not be trying to uncuff themselves.
cuffable . CanStillInteract = true ;
Dirty ( args . User , cuffable ) ;
if ( ! _actionBlocker . CanInteract ( args . User , args . User ) )
args . Cancelled = true ;
cuffable . CanStillInteract = false ;
Dirty ( args . User , cuffable ) ;
2023-03-13 19:34:26 -04:00
}
else
{
// Check if the user can interact.
if ( ! _actionBlocker . CanInteract ( args . User , args . Target ) )
args . Cancelled = true ;
}
2024-02-26 00:10:02 +11:00
if ( args . Cancelled )
2023-03-13 19:34:26 -04:00
{
2024-02-26 00:10:02 +11:00
_popup . PopupClient ( Loc . GetString ( "cuffable-component-cannot-interact-message" ) , args . Target , args . User ) ;
2023-03-13 19:34:26 -04:00
}
}
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 )
2023-01-21 02:48:19 +13:00
{
2023-03-15 13:14:18 +13:00
_container . EmptyContainer ( component . Container , true ) ;
2023-01-21 02:48:19 +13:00
}
2023-03-13 19:34:26 -04:00
private void OnCuffsRemovedFromContainer ( EntityUid uid , CuffableComponent component , EntRemovedFromContainerMessage args )
{
2023-09-08 18:16:05 -07:00
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
if ( args . Container . ID ! = component . Container ? . ID )
2023-03-29 22:04:21 +11:00
return ;
2024-01-14 06:18:47 -04:00
_virtualItem . DeleteInHandsMatching ( uid , args . Entity ) ;
2023-03-29 22:04:21 +11:00
UpdateCuffState ( uid , component ) ;
2023-03-13 19:34:26 -04:00
}
private void OnCuffsInsertedIntoContainer ( EntityUid uid , CuffableComponent component , ContainerModifiedMessage args )
2023-01-21 02:48:19 +13:00
{
if ( args . Container = = component . Container )
UpdateCuffState ( uid , component ) ;
}
2023-03-13 19:34:26 -04:00
public void UpdateCuffState ( EntityUid uid , CuffableComponent component )
2023-01-21 02:48:19 +13:00
{
2023-04-07 11:21:12 -07:00
var canInteract = TryComp ( uid , out HandsComponent ? hands ) & & hands . Hands . Count > component . CuffedHandCount ;
2023-01-21 02:48:19 +13:00
if ( canInteract = = component . CanStillInteract )
return ;
component . CanStillInteract = canInteract ;
2023-10-16 01:31:03 -04:00
Dirty ( uid , component ) ;
2023-03-13 19:34:26 -04:00
_actionBlocker . UpdateCanMove ( uid ) ;
2023-01-21 02:48:19 +13:00
if ( component . CanStillInteract )
2024-05-23 22:43:04 -04:00
_alerts . ClearAlert ( uid , component . CuffedAlert ) ;
2023-01-21 02:48:19 +13:00
else
2024-05-23 22:43:04 -04:00
_alerts . ShowAlert ( uid , component . CuffedAlert ) ;
2023-01-21 02:48:19 +13:00
var ev = new CuffedStateChangeEvent ( ) ;
RaiseLocalEvent ( uid , ref ev ) ;
}
2022-06-15 21:10:03 -04:00
2023-03-13 19:34:26 -04:00
private void OnBeingPulledAttempt ( EntityUid uid , CuffableComponent component , BeingPulledAttemptEvent args )
2022-06-15 21:10:03 -04:00
{
2024-03-19 14:30:56 +11:00
if ( ! TryComp < PullableComponent > ( uid , out var pullable ) )
2022-06-15 21:10:03 -04:00
return ;
if ( pullable . Puller ! = null & & ! component . CanStillInteract ) // If we are being pulled already and cuffed, we can't get pulled again.
args . Cancel ( ) ;
}
2023-03-13 19:34:26 -04:00
2024-06-20 03:14:18 +12:00
private void OnBuckleAttempt ( Entity < CuffableComponent > ent , EntityUid ? user , ref bool cancelled , bool buckling , bool popup )
2023-06-28 23:19:56 -04:00
{
2024-06-20 03:14:18 +12:00
if ( cancelled | | user ! = ent . Owner )
2023-06-28 23:19:56 -04:00
return ;
2024-08-08 05:53:07 -04:00
if ( ! TryComp < HandsComponent > ( ent , out var hands ) | | ent . Comp . CuffedHandCount < hands . Count )
2023-06-28 23:19:56 -04:00
return ;
2024-06-20 03:14:18 +12:00
cancelled = true ;
if ( ! popup )
return ;
var message = buckling
2023-06-28 23:19:56 -04:00
? Loc . GetString ( "handcuff-component-cuff-interrupt-buckled-message" )
: Loc . GetString ( "handcuff-component-cuff-interrupt-unbuckled-message" ) ;
2024-02-26 00:10:02 +11:00
2024-06-20 03:14:18 +12:00
_popup . PopupClient ( message , ent , user ) ;
}
private void OnBuckleAttemptEvent ( Entity < CuffableComponent > ent , ref BuckleAttemptEvent args )
{
OnBuckleAttempt ( ent , args . User , ref args . Cancelled , true , args . Popup ) ;
}
private void OnUnbuckleAttemptEvent ( Entity < CuffableComponent > ent , ref UnbuckleAttemptEvent args )
{
OnBuckleAttempt ( ent , args . User , ref args . Cancelled , false , args . Popup ) ;
2023-06-28 23:19:56 -04:00
}
2023-03-13 19:34:26 -04:00
private void OnPull ( EntityUid uid , CuffableComponent component , PullMessage args )
2022-04-10 16:48:11 +12:00
{
if ( ! component . CanStillInteract )
2023-03-13 19:34:26 -04:00
_actionBlocker . UpdateCanMove ( uid ) ;
2021-08-10 10:34:01 +10:00
}
2023-03-13 19:34:26 -04:00
private void HandleMoveAttempt ( EntityUid uid , CuffableComponent component , UpdateCanMoveEvent args )
2021-08-10 10:34:01 +10:00
{
2024-03-19 14:30:56 +11:00
if ( component . CanStillInteract | | ! EntityManager . TryGetComponent ( uid , out PullableComponent ? pullable ) | | ! pullable . BeingPulled )
2021-08-10 10:34:01 +10:00
return ;
args . Cancel ( ) ;
2021-06-17 00:37:05 +10:00
}
2024-03-19 14:30:56 +11:00
private void HandleStopPull ( EntityUid uid , CuffableComponent component , AttemptStopPullingEvent args )
2021-06-17 00:37:05 +10:00
{
2023-03-13 19:34:26 -04:00
if ( args . User = = null | | ! Exists ( args . User . Value ) )
return ;
2021-06-17 00:37:05 +10:00
2023-03-13 19:34:26 -04:00
if ( args . User . Value = = uid & & ! component . CanStillInteract )
2024-03-19 14:30:56 +11:00
args . Cancelled = true ;
2023-03-13 19:34:26 -04:00
}
2024-08-07 01:15:35 -04:00
private void OnRemoveCuffsAlert ( Entity < CuffableComponent > ent , ref RemoveCuffsAlertEvent args )
{
if ( args . Handled )
return ;
TryUncuff ( ent , ent , cuffable : ent . Comp ) ;
args . Handled = true ;
}
2023-03-13 19:34:26 -04:00
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 ) ;
}
2023-04-03 13:13:48 +12:00
private void OnCuffableDoAfter ( EntityUid uid , CuffableComponent component , UnCuffDoAfterEvent args )
2023-03-13 19:34:26 -04:00
{
if ( args . Args . Target is not { } target | | args . Args . Used is not { } used )
return ;
if ( args . Handled )
return ;
args . Handled = true ;
var user = args . Args . User ;
if ( ! args . Cancelled )
{
Uncuff ( target , user , used , component ) ;
}
2024-02-26 00:10:02 +11:00
else
2023-03-13 19:34:26 -04:00
{
2024-02-26 00:10:02 +11:00
_popup . PopupClient ( Loc . GetString ( "cuffable-component-remove-cuffs-fail-message" ) , user , user ) ;
2023-03-13 19:34:26 -04:00
}
}
private void OnCuffAfterInteract ( EntityUid uid , HandcuffComponent component , AfterInteractEvent args )
{
2023-07-22 23:14:25 +01:00
if ( args . Target is not { Valid : true } target )
2023-03-13 19:34:26 -04:00
return ;
if ( ! args . CanReach )
{
2024-02-26 00:10:02 +11:00
_popup . PopupClient ( Loc . GetString ( "handcuff-component-too-far-away-error" ) , args . User , args . User ) ;
2023-03-13 19:34:26 -04:00
return ;
}
2023-06-02 02:48:42 +02:00
var result = TryCuffing ( args . User , target , uid , component ) ;
args . Handled = result ;
2023-03-13 19:34:26 -04:00
}
private void OnCuffMeleeHit ( EntityUid uid , HandcuffComponent component , MeleeHitEvent args )
{
if ( ! args . HitEntities . Any ( ) )
return ;
2023-03-17 07:13:04 +05:00
TryCuffing ( args . User , args . HitEntities . First ( ) , uid , component ) ;
2023-03-13 19:34:26 -04:00
args . Handled = true ;
}
2023-04-03 13:13:48 +12:00
private void OnAddCuffDoAfter ( EntityUid uid , HandcuffComponent component , AddCuffDoAfterEvent args )
2023-03-13 19:34:26 -04:00
{
var user = args . Args . User ;
2023-03-15 17:07:16 +13:00
if ( ! TryComp < CuffableComponent > ( args . Args . Target , out var cuffable ) )
2023-03-13 19:34:26 -04:00
return ;
2023-03-15 17:07:16 +13:00
var target = args . Args . Target . Value ;
2023-03-13 19:34:26 -04:00
if ( args . Handled )
return ;
args . Handled = true ;
if ( ! args . Cancelled & & TryAddNewCuffs ( target , user , uid , cuffable ) )
{
2024-03-26 21:15:08 +02:00
component . Used = true ;
2023-04-03 13:13:48 +12:00
_audio . PlayPredicted ( component . EndCuffSound , uid , user ) ;
2023-03-13 19:34:26 -04:00
2025-03-20 03:15:44 +01:00
var popupText = ( user = = target )
? "handcuff-component-cuff-self-observer-success-message"
: "handcuff-component-cuff-observer-success-message" ;
_popup . PopupEntity ( Loc . GetString ( popupText ,
( "user" , Identity . Name ( user , EntityManager ) ) , ( "target" , Identity . Entity ( target , EntityManager ) ) ) ,
2023-03-13 19:34:26 -04:00
target , Filter . Pvs ( target , entityManager : EntityManager )
. RemoveWhere ( e = > e . AttachedEntity = = target | | e . AttachedEntity = = user ) , true ) ;
if ( target = = user )
{
2024-02-26 00:10:02 +11:00
_popup . PopupClient ( Loc . GetString ( "handcuff-component-cuff-self-success-message" ) , user , user ) ;
2023-03-13 19:34:26 -04:00
_adminLog . Add ( LogType . Action , LogImpact . Medium ,
$"{ToPrettyString(user):player} has cuffed himself" ) ;
}
else
{
2024-02-26 00:10:02 +11:00
_popup . PopupClient ( Loc . GetString ( "handcuff-component-cuff-other-success-message" ,
2023-03-13 19:34:26 -04:00
( "otherName" , Identity . Name ( target , EntityManager , user ) ) ) , user , user ) ;
2024-02-26 00:10:02 +11:00
_popup . PopupClient ( Loc . GetString ( "handcuff-component-cuff-by-other-success-message" ,
2023-03-13 19:34:26 -04:00
( "otherName" , Identity . Name ( user , EntityManager , target ) ) ) , target , target ) ;
2025-03-20 20:56:51 +01:00
_adminLog . Add ( LogType . Action , LogImpact . High ,
2023-03-13 19:34:26 -04:00
$"{ToPrettyString(user):player} has cuffed {ToPrettyString(target):player}" ) ;
}
}
else
{
if ( target = = user )
{
2024-02-26 00:10:02 +11:00
_popup . PopupClient ( Loc . GetString ( "handcuff-component-cuff-interrupt-self-message" ) , user , user ) ;
2023-03-13 19:34:26 -04:00
}
else
{
2023-04-03 13:13:48 +12:00
// TODO Fix popup message wording
// This message assumes that the user being handcuffed is the one that caused the handcuff to fail.
2024-02-26 00:10:02 +11:00
_popup . PopupClient ( Loc . GetString ( "handcuff-component-cuff-interrupt-message" ,
2023-03-13 19:34:26 -04:00
( "targetName" , Identity . Name ( target , EntityManager , user ) ) ) , user , user ) ;
2024-02-26 00:10:02 +11:00
_popup . PopupClient ( Loc . GetString ( "handcuff-component-cuff-interrupt-other-message" ,
2023-03-13 19:34:26 -04:00
( "otherName" , Identity . Name ( user , EntityManager , target ) ) ) , target , target ) ;
}
}
2023-10-16 01:31:03 -04:00
}
2023-03-13 19:34:26 -04:00
2023-10-16 01:31:03 -04:00
private void OnCuffVirtualItemDeleted ( EntityUid uid , HandcuffComponent component , VirtualItemDeletedEvent args )
{
Uncuff ( args . User , null , uid , cuff : component ) ;
2023-03-13 19:34:26 -04:00
}
/// <summary>
/// Check the current amount of hands the owner has, and if there's less hands than active cuffs we remove some cuffs.
/// </summary>
2024-07-26 14:48:03 -04:00
private void OnHandCountChanged ( Entity < CuffableComponent > ent , ref HandCountChangedEvent message )
2023-03-13 19:34:26 -04:00
{
var dirty = false ;
2024-07-26 14:48:03 -04:00
var handCount = CompOrNull < HandsComponent > ( ent . Owner ) ? . Count ? ? 0 ;
2023-03-13 19:34:26 -04:00
2024-07-26 14:48:03 -04:00
while ( ent . Comp . CuffedHandCount > handCount & & ent . Comp . CuffedHandCount > 0 )
2023-03-13 19:34:26 -04:00
{
dirty = true ;
2024-07-26 14:48:03 -04:00
var handcuffContainer = ent . Comp . Container ;
var handcuffEntity = handcuffContainer . ContainedEntities [ ^ 1 ] ;
2023-03-13 19:34:26 -04:00
2024-07-26 14:48:03 -04:00
_transform . PlaceNextTo ( handcuffEntity , ent . Owner ) ;
2023-03-13 19:34:26 -04:00
}
if ( dirty )
{
2024-07-26 14:48:03 -04:00
UpdateCuffState ( ent . Owner , ent . Comp ) ;
2023-03-13 19:34:26 -04:00
}
}
/// <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.
2023-04-07 11:21:12 -07:00
if ( ! TryComp < HandsComponent > ( uid , out var handsComponent ) )
2023-03-13 19:34:26 -04:00
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 ;
}
2024-01-14 06:18:47 -04:00
if ( _virtualItem . TrySpawnVirtualItemInHand ( handcuff , uid , out var virtItem1 ) )
2023-03-13 19:34:26 -04:00
EnsureComp < UnremoveableComponent > ( virtItem1 . Value ) ;
2024-01-14 06:18:47 -04:00
if ( _virtualItem . TrySpawnVirtualItemInHand ( handcuff , uid , out var virtItem2 ) )
2023-03-13 19:34:26 -04:00
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 ;
2024-11-30 07:58:56 -08:00
// if the amount of hands the target has is equal to or less than the amount of hands that are cuffed
// don't apply the new set of cuffs
// (how would you even end up with more cuffed hands than actual hands? either way accounting for it)
if ( TryComp < HandsComponent > ( target , out var hands ) & & hands . Count < = component . CuffedHandCount )
return false ;
2023-03-13 19:34:26 -04:00
// Success!
_hands . TryDrop ( user , handcuff ) ;
2023-12-27 21:30:03 -08:00
_container . Insert ( handcuff , component . Container ) ;
2023-03-13 19:34:26 -04:00
UpdateHeldItems ( target , handcuff , component ) ;
return true ;
}
2023-06-02 02:48:42 +02:00
/// <returns>False if the target entity isn't cuffable.</returns>
public bool TryCuffing ( EntityUid user , EntityUid target , EntityUid handcuff , HandcuffComponent ? handcuffComponent = null , CuffableComponent ? cuffable = null )
2023-03-13 19:34:26 -04:00
{
if ( ! Resolve ( handcuff , ref handcuffComponent ) | | ! Resolve ( target , ref cuffable , false ) )
2023-06-02 02:48:42 +02:00
return false ;
2023-03-13 19:34:26 -04:00
2023-09-20 10:12:48 +10:00
if ( ! TryComp < HandsComponent > ( target , out var hands ) )
2023-03-13 19:34:26 -04:00
{
2024-02-26 00:10:02 +11:00
_popup . PopupClient ( Loc . GetString ( "handcuff-component-target-has-no-hands-error" ,
( "targetName" , Identity . Name ( target , EntityManager , user ) ) ) , user , user ) ;
2023-06-02 02:48:42 +02:00
return true ;
2023-03-13 19:34:26 -04:00
}
if ( cuffable . CuffedHandCount > = hands . Count )
{
2024-02-26 00:10:02 +11:00
_popup . PopupClient ( Loc . GetString ( "handcuff-component-target-has-no-free-hands-error" ,
( "targetName" , Identity . Name ( target , EntityManager , user ) ) ) , user , user ) ;
2023-06-02 02:48:42 +02:00
return true ;
2023-03-13 19:34:26 -04:00
}
2024-08-09 06:26:04 +00:00
if ( ! _hands . CanDrop ( user , handcuff ) )
{
_popup . PopupClient ( Loc . GetString ( "handcuff-component-cannot-drop-cuffs" , ( "target" , Identity . Name ( target , EntityManager , user ) ) ) , user , user ) ;
return false ;
}
2023-04-03 13:13:48 +12:00
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.
2023-09-11 09:42:41 +10:00
var doAfterEventArgs = new DoAfterArgs ( EntityManager , user , cuffTime , new AddCuffDoAfterEvent ( ) , handcuff , target , handcuff )
2023-04-03 13:13:48 +12:00
{
2024-03-19 12:09:00 +02:00
BreakOnMove = true ,
BreakOnWeightlessMove = false ,
2023-04-03 13:13:48 +12:00
BreakOnDamage = true ,
2024-03-19 12:09:00 +02:00
NeedHand = true ,
2024-06-05 13:14:56 -07:00
DistanceThreshold = 1f // shorter than default but still feels good
2023-04-03 13:13:48 +12:00
} ;
if ( ! _doAfter . TryStartDoAfter ( doAfterEventArgs ) )
2023-06-02 02:48:42 +02:00
return true ;
2023-04-03 13:13:48 +12:00
2025-03-20 03:15:44 +01:00
var popupText = ( user = = target )
? "handcuff-component-start-cuffing-self-observer"
: "handcuff-component-start-cuffing-observer" ;
_popup . PopupEntity ( Loc . GetString ( popupText ,
( "user" , Identity . Name ( user , EntityManager ) ) , ( "target" , Identity . Entity ( target , EntityManager ) ) ) ,
2024-02-26 00:10:02 +11:00
target , Filter . Pvs ( target , entityManager : EntityManager )
2023-03-13 19:34:26 -04:00
. RemoveWhere ( e = > e . AttachedEntity = = target | | e . AttachedEntity = = user ) , true ) ;
2024-02-26 00:10:02 +11:00
if ( target = = user )
{
_popup . PopupClient ( Loc . GetString ( "handcuff-component-target-self" ) , user , user ) ;
}
else
{
_popup . PopupClient ( Loc . GetString ( "handcuff-component-start-cuffing-target-message" ,
( "targetName" , Identity . Name ( target , EntityManager , user ) ) ) , user , user ) ;
2024-12-27 05:34:32 -08:00
_popup . PopupEntity ( Loc . GetString ( "handcuff-component-start-cuffing-by-other-message" ,
2024-02-26 00:10:02 +11:00
( "otherName" , Identity . Name ( user , EntityManager , target ) ) ) , target , target ) ;
2023-03-13 19:34:26 -04:00
}
2023-04-24 22:41:46 +02:00
_audio . PlayPredicted ( handcuffComponent . StartCuffSound , handcuff , user ) ;
2023-06-02 02:48:42 +02:00
return true ;
2023-03-13 19:34:26 -04:00
}
2024-08-02 03:07:46 -04:00
/// <summary>
/// Checks if the target is handcuffed.
/// </summary>
2025-03-20 03:15:44 +01:00
/// /// <param name="target">The entity to be checked</param>
2024-08-02 03:07:46 -04:00
/// <param name="requireFullyCuffed">when true, return false if the target is only partially cuffed (for things with more than 2 hands)</param>
/// <returns></returns>
public bool IsCuffed ( Entity < CuffableComponent > target , bool requireFullyCuffed = true )
{
if ( ! TryComp < HandsComponent > ( target , out var hands ) )
return false ;
if ( target . Comp . CuffedHandCount < = 0 )
return false ;
if ( requireFullyCuffed & & hands . Count > target . Comp . CuffedHandCount )
return false ;
return true ;
}
2023-03-13 19:34:26 -04:00
/// <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 ;
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 ) )
{
2023-10-16 01:31:03 -04:00
Log . Warning ( "A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!" ) ;
2023-03-13 19:34:26 -04:00
}
}
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 ) )
{
2024-02-26 00:10:02 +11:00
_popup . PopupClient ( Loc . GetString ( "cuffable-component-cannot-remove-cuffs-too-far-message" ) , user , user ) ;
2023-03-13 19:34:26 -04:00
return ;
}
2024-06-15 00:01:22 -04:00
var ev = new ModifyUncuffDurationEvent ( user , target , isOwner ? cuff . BreakoutTime : cuff . UncuffTime ) ;
RaiseLocalEvent ( user , ref ev ) ;
var uncuffTime = ev . Duration ;
2024-02-26 03:41:20 +01:00
if ( isOwner )
{
if ( ! TryComp ( cuffsToRemove . Value , out UseDelayComponent ? useDelay ) )
return ;
if ( ! _delay . TryResetDelay ( ( cuffsToRemove . Value , useDelay ) , true ) )
{
return ;
}
}
2023-09-11 09:42:41 +10:00
var doAfterEventArgs = new DoAfterArgs ( EntityManager , user , uncuffTime , new UnCuffDoAfterEvent ( ) , target , target , cuffsToRemove )
2023-03-13 19:34:26 -04:00
{
2024-03-19 12:09:00 +02:00
BreakOnMove = true ,
BreakOnWeightlessMove = false ,
2023-03-13 19:34:26 -04:00
BreakOnDamage = true ,
2023-04-03 13:13:48 +12:00
NeedHand = true ,
RequireCanInteract = false , // Trust in UncuffAttemptEvent
2024-06-05 13:14:56 -07:00
DistanceThreshold = 1f // shorter than default but still feels good
2023-03-13 19:34:26 -04:00
} ;
2023-04-03 13:13:48 +12:00
if ( ! _doAfter . TryStartDoAfter ( doAfterEventArgs ) )
return ;
2025-03-20 20:56:51 +01:00
_adminLog . Add ( LogType . Action , LogImpact . High , $"{ToPrettyString(user):player} is trying to uncuff {ToPrettyString(target):subject}" ) ;
2023-07-04 05:30:09 +08:00
2025-03-20 03:15:44 +01:00
var popupText = user = = target
? "cuffable-component-start-uncuffing-self-observer"
: "cuffable-component-start-uncuffing-observer" ;
2024-12-27 05:34:32 -08:00
_popup . PopupEntity (
2025-03-20 03:15:44 +01:00
Loc . GetString ( popupText ,
2024-12-27 05:34:32 -08:00
( "user" , Identity . Name ( user , EntityManager ) ) ,
2025-03-20 03:15:44 +01:00
( "target" , Identity . Entity ( target , EntityManager ) ) ) ,
2024-12-27 05:34:32 -08:00
target ,
Filter . Pvs ( target , entityManager : EntityManager )
. RemoveWhere ( e = > e . AttachedEntity = = target | | e . AttachedEntity = = user ) ,
true ) ;
2023-05-02 12:14:55 +02:00
2024-02-26 00:10:02 +11:00
if ( target = = user )
{
_popup . PopupClient ( Loc . GetString ( "cuffable-component-start-uncuffing-self" ) , user , user ) ;
2023-05-02 12:14:55 +02:00
}
2024-02-26 00:10:02 +11:00
else
{
_popup . PopupClient ( Loc . GetString ( "cuffable-component-start-uncuffing-target-message" ,
2024-12-27 05:34:32 -08:00
( "targetName" , Identity . Name ( target , EntityManager , user ) ) ) ,
user ,
user ) ;
_popup . PopupEntity ( Loc . GetString ( "cuffable-component-start-uncuffing-by-other-message" ,
( "otherName" , Identity . Name ( user , EntityManager , target ) ) ) ,
target ,
target ) ;
2024-02-26 00:10:02 +11:00
}
2023-04-03 13:13:48 +12:00
_audio . PlayPredicted ( isOwner ? cuff . StartBreakoutSound : cuff . StartUncuffSound , target , user ) ;
2023-03-13 19:34:26 -04:00
}
2023-10-16 01:31:03 -04:00
public void Uncuff ( EntityUid target , EntityUid ? user , EntityUid cuffsToRemove , CuffableComponent ? cuffable = null , HandcuffComponent ? cuff = null )
2023-03-13 19:34:26 -04:00
{
if ( ! Resolve ( target , ref cuffable ) | | ! Resolve ( cuffsToRemove , ref cuff ) )
return ;
2024-03-26 21:15:08 +02:00
if ( ! cuff . Used | | cuff . Removing | | TerminatingOrDeleted ( cuffsToRemove ) | | TerminatingOrDeleted ( target ) )
2023-11-22 13:53:30 +13:00
return ;
2023-10-16 01:31:03 -04:00
if ( user ! = null )
{
var attempt = new UncuffAttemptEvent ( user . Value , target ) ;
RaiseLocalEvent ( user . Value , ref attempt ) ;
if ( attempt . Cancelled )
return ;
}
2023-04-03 13:13:48 +12:00
2023-12-28 18:50:08 -05:00
cuff . Removing = true ;
2024-03-26 21:15:08 +02:00
cuff . Used = false ;
2023-04-24 22:41:46 +02:00
_audio . PlayPredicted ( cuff . EndUncuffSound , target , user ) ;
2023-03-13 19:34:26 -04:00
2023-12-27 21:30:03 -08:00
_container . Remove ( cuffsToRemove , cuffable . Container ) ;
2023-03-13 19:34:26 -04:00
if ( _net . IsServer )
{
2023-05-02 12:14:55 +02:00
// Handles spawning broken cuffs on server to avoid client misprediction
if ( cuff . BreakOnRemove )
{
QueueDel ( cuffsToRemove ) ;
var trash = Spawn ( cuff . BrokenPrototype , Transform ( cuffsToRemove ) . Coordinates ) ;
_hands . PickupOrDrop ( user , trash ) ;
}
else
{
_hands . PickupOrDrop ( user , cuffsToRemove ) ;
}
2024-03-26 21:15:08 +02:00
}
2025-03-29 21:09:34 +01:00
var shoved = false ;
// if combat mode is on, shove the person.
if ( _combatMode . IsInCombatMode ( user ) & & target ! = user & & user ! = null )
{
2025-04-19 11:38:22 +10:00
var eventArgs = new DisarmedEvent ( target , user . Value , 1f ) ;
RaiseLocalEvent ( target , ref eventArgs ) ;
2025-03-29 21:09:34 +01:00
shoved = true ;
}
2024-03-26 21:15:08 +02:00
if ( cuffable . CuffedHandCount = = 0 )
{
if ( user ! = null )
2025-03-29 21:09:34 +01:00
{
if ( shoved )
{
_popup . PopupClient ( Loc . GetString ( "cuffable-component-remove-cuffs-push-success-message" ,
( "otherName" , Identity . Name ( user . Value , EntityManager , user ) ) ) ,
user . Value ,
user . Value ) ;
}
else
{
_popup . PopupClient ( Loc . GetString ( "cuffable-component-remove-cuffs-success-message" ) , user . Value , user . Value ) ;
}
}
2023-05-02 12:14:55 +02:00
2024-03-26 21:15:08 +02:00
if ( target ! = user & & user ! = null )
{
2025-03-20 03:15:44 +01:00
_popup . PopupEntity ( Loc . GetString ( "cuffable-component-remove-cuffs-by-other-success-message" ,
2024-03-26 21:15:08 +02:00
( "otherName" , Identity . Name ( user . Value , EntityManager , user ) ) ) , target , target ) ;
2025-03-20 20:56:51 +01:00
_adminLog . Add ( LogType . Action , LogImpact . High ,
2024-03-26 21:15:08 +02:00
$"{ToPrettyString(user):player} has successfully uncuffed {ToPrettyString(target):player}" ) ;
}
else
2023-03-13 19:34:26 -04:00
{
2025-03-20 20:56:51 +01:00
_adminLog . Add ( LogType . Action , LogImpact . High ,
2024-03-26 21:15:08 +02:00
$"{ToPrettyString(user):player} has successfully uncuffed themselves" ) ;
2023-03-13 19:34:26 -04:00
}
2024-03-26 21:15:08 +02:00
}
else if ( user ! = null )
{
if ( user ! = target )
{
2025-03-20 03:15:44 +01:00
_popup . PopupClient ( Loc . GetString ( "cuffable-component-remove-cuffs-partial-success-message" ,
2024-03-26 21:15:08 +02:00
( "cuffedHandCount" , cuffable . CuffedHandCount ) ,
( "otherName" , Identity . Name ( user . Value , EntityManager , user . Value ) ) ) , user . Value , user . Value ) ;
2025-03-20 03:15:44 +01:00
_popup . PopupEntity ( Loc . GetString (
2024-03-26 21:15:08 +02:00
"cuffable-component-remove-cuffs-by-other-partial-success-message" ,
( "otherName" , Identity . Name ( user . Value , EntityManager , user . Value ) ) ,
( "cuffedHandCount" , cuffable . CuffedHandCount ) ) , target , target ) ;
}
else
2023-03-13 19:34:26 -04:00
{
2025-03-20 03:15:44 +01:00
_popup . PopupClient ( Loc . GetString ( "cuffable-component-remove-cuffs-partial-success-message" ,
2024-03-26 21:15:08 +02:00
( "cuffedHandCount" , cuffable . CuffedHandCount ) ) , user . Value , user . Value ) ;
2023-03-13 19:34:26 -04:00
}
2021-06-17 00:37:05 +10:00
}
2023-12-28 18:50:08 -05:00
cuff . Removing = false ;
2021-06-17 00:37:05 +10:00
}
2021-10-21 13:03:14 +11:00
#region ActionBlocker
2023-03-13 19:34:26 -04:00
private void CheckAct ( EntityUid uid , CuffableComponent component , CancellableEntityEventArgs args )
2021-10-21 13:03:14 +11:00
{
if ( ! component . CanStillInteract )
args . Cancel ( ) ;
}
2023-03-13 19:34:26 -04:00
private void OnEquipAttempt ( EntityUid uid , CuffableComponent component , IsEquippingAttemptEvent args )
2021-10-21 13:03:14 +11:00
{
2022-01-02 06:03:29 +13:00
// is this a self-equip, or are they being stripped?
if ( args . Equipee = = uid )
CheckAct ( uid , component , args ) ;
2021-10-21 13:03:14 +11:00
}
2023-03-13 19:34:26 -04:00
private void OnUnequipAttempt ( EntityUid uid , CuffableComponent component , IsUnequippingAttemptEvent args )
2021-10-21 13:03:14 +11:00
{
2022-01-02 06:03:29 +13:00
// is this a self-equip, or are they being stripped?
if ( args . Unequipee = = uid )
CheckAct ( uid , component , args ) ;
2021-10-21 13:03:14 +11:00
}
#endregion
2023-03-13 19:34:26 -04:00
public IReadOnlyList < EntityUid > GetAllCuffs ( CuffableComponent component )
{
return component . Container . ContainedEntities ;
}
2023-03-15 17:07:16 +13:00
2023-04-03 13:13:48 +12:00
[Serializable, NetSerializable]
2023-08-22 18:14:33 -07:00
private sealed partial class UnCuffDoAfterEvent : SimpleDoAfterEvent
2023-03-15 17:07:16 +13:00
{
}
2023-04-03 13:13:48 +12:00
[Serializable, NetSerializable]
2023-08-22 18:14:33 -07:00
private sealed partial class AddCuffDoAfterEvent : SimpleDoAfterEvent
2023-03-15 17:07:16 +13:00
{
}
2021-06-17 00:37:05 +10:00
}
}