SharedCuffableSystem.cs 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798
  1. using System.Linq;
  2. using Content.Shared.ActionBlocker;
  3. using Content.Shared.Administration.Components;
  4. using Content.Shared.Administration.Logs;
  5. using Content.Shared.Alert;
  6. using Content.Shared.Buckle.Components;
  7. using Content.Shared.Cuffs.Components;
  8. using Content.Shared.Database;
  9. using Content.Shared.DoAfter;
  10. using Content.Shared.Hands;
  11. using Content.Shared.Hands.Components;
  12. using Content.Shared.Hands.EntitySystems;
  13. using Content.Shared.IdentityManagement;
  14. using Content.Shared.Interaction;
  15. using Content.Shared.Interaction.Components;
  16. using Content.Shared.Interaction.Events;
  17. using Content.Shared.Inventory.Events;
  18. using Content.Shared.Inventory.VirtualItem;
  19. using Content.Shared.Item;
  20. using Content.Shared.Movement.Events;
  21. using Content.Shared.Movement.Pulling.Events;
  22. using Content.Shared.Popups;
  23. using Content.Shared.Pulling.Events;
  24. using Content.Shared.Rejuvenate;
  25. using Content.Shared.Stunnable;
  26. using Content.Shared.Timing;
  27. using Content.Shared.Verbs;
  28. using Content.Shared.Weapons.Melee.Events;
  29. using Robust.Shared.Audio.Systems;
  30. using Robust.Shared.Containers;
  31. using Robust.Shared.Network;
  32. using Robust.Shared.Player;
  33. using Robust.Shared.Serialization;
  34. using Robust.Shared.Utility;
  35. using PullableComponent = Content.Shared.Movement.Pulling.Components.PullableComponent;
  36. namespace Content.Shared.Cuffs
  37. {
  38. // TODO remove all the IsServer() checks.
  39. public abstract partial class SharedCuffableSystem : EntitySystem
  40. {
  41. [Dependency] private readonly IComponentFactory _componentFactory = default!;
  42. [Dependency] private readonly INetManager _net = default!;
  43. [Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
  44. [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
  45. [Dependency] private readonly AlertsSystem _alerts = default!;
  46. [Dependency] private readonly SharedAudioSystem _audio = default!;
  47. [Dependency] private readonly SharedContainerSystem _container = default!;
  48. [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
  49. [Dependency] private readonly SharedHandsSystem _hands = default!;
  50. [Dependency] private readonly SharedVirtualItemSystem _virtualItem = default!;
  51. [Dependency] private readonly SharedInteractionSystem _interaction = default!;
  52. [Dependency] private readonly SharedPopupSystem _popup = default!;
  53. [Dependency] private readonly SharedTransformSystem _transform = default!;
  54. [Dependency] private readonly UseDelaySystem _delay = default!;
  55. public override void Initialize()
  56. {
  57. base.Initialize();
  58. SubscribeLocalEvent<CuffableComponent, HandCountChangedEvent>(OnHandCountChanged);
  59. SubscribeLocalEvent<UncuffAttemptEvent>(OnUncuffAttempt);
  60. SubscribeLocalEvent<CuffableComponent, EntRemovedFromContainerMessage>(OnCuffsRemovedFromContainer);
  61. SubscribeLocalEvent<CuffableComponent, EntInsertedIntoContainerMessage>(OnCuffsInsertedIntoContainer);
  62. SubscribeLocalEvent<CuffableComponent, RejuvenateEvent>(OnRejuvenate);
  63. SubscribeLocalEvent<CuffableComponent, ComponentInit>(OnStartup);
  64. SubscribeLocalEvent<CuffableComponent, AttemptStopPullingEvent>(HandleStopPull);
  65. SubscribeLocalEvent<CuffableComponent, RemoveCuffsAlertEvent>(OnRemoveCuffsAlert);
  66. SubscribeLocalEvent<CuffableComponent, UpdateCanMoveEvent>(HandleMoveAttempt);
  67. SubscribeLocalEvent<CuffableComponent, IsEquippingAttemptEvent>(OnEquipAttempt);
  68. SubscribeLocalEvent<CuffableComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
  69. SubscribeLocalEvent<CuffableComponent, BeingPulledAttemptEvent>(OnBeingPulledAttempt);
  70. SubscribeLocalEvent<CuffableComponent, BuckleAttemptEvent>(OnBuckleAttemptEvent);
  71. SubscribeLocalEvent<CuffableComponent, UnbuckleAttemptEvent>(OnUnbuckleAttemptEvent);
  72. SubscribeLocalEvent<CuffableComponent, GetVerbsEvent<Verb>>(AddUncuffVerb);
  73. SubscribeLocalEvent<CuffableComponent, UnCuffDoAfterEvent>(OnCuffableDoAfter);
  74. SubscribeLocalEvent<CuffableComponent, PullStartedMessage>(OnPull);
  75. SubscribeLocalEvent<CuffableComponent, PullStoppedMessage>(OnPull);
  76. SubscribeLocalEvent<CuffableComponent, DropAttemptEvent>(CheckAct);
  77. SubscribeLocalEvent<CuffableComponent, PickupAttemptEvent>(CheckAct);
  78. SubscribeLocalEvent<CuffableComponent, AttackAttemptEvent>(CheckAct);
  79. SubscribeLocalEvent<CuffableComponent, UseAttemptEvent>(CheckAct);
  80. SubscribeLocalEvent<CuffableComponent, InteractionAttemptEvent>(CheckInteract);
  81. SubscribeLocalEvent<HandcuffComponent, AfterInteractEvent>(OnCuffAfterInteract);
  82. SubscribeLocalEvent<HandcuffComponent, MeleeHitEvent>(OnCuffMeleeHit);
  83. SubscribeLocalEvent<HandcuffComponent, AddCuffDoAfterEvent>(OnAddCuffDoAfter);
  84. SubscribeLocalEvent<HandcuffComponent, VirtualItemDeletedEvent>(OnCuffVirtualItemDeleted);
  85. }
  86. private void CheckInteract(Entity<CuffableComponent> ent, ref InteractionAttemptEvent args)
  87. {
  88. if (!ent.Comp.CanStillInteract)
  89. args.Cancelled = true;
  90. }
  91. private void OnUncuffAttempt(ref UncuffAttemptEvent args)
  92. {
  93. if (args.Cancelled)
  94. return;
  95. if (!Exists(args.User) || Deleted(args.User))
  96. {
  97. // Should this even be possible?
  98. args.Cancelled = true;
  99. return;
  100. }
  101. // If the user is the target, special logic applies.
  102. // This is because the CanInteract blocking of the cuffs prevents self-uncuff.
  103. if (args.User == args.Target)
  104. {
  105. if (!TryComp<CuffableComponent>(args.User, out var cuffable))
  106. {
  107. DebugTools.Assert($"{args.User} tried to uncuff themselves but they are not cuffable.");
  108. return;
  109. }
  110. // We temporarily allow interactions so the cuffable system does not block itself.
  111. // It's assumed that this will always be false.
  112. // Otherwise they would not be trying to uncuff themselves.
  113. cuffable.CanStillInteract = true;
  114. Dirty(args.User, cuffable);
  115. if (!_actionBlocker.CanInteract(args.User, args.User))
  116. args.Cancelled = true;
  117. cuffable.CanStillInteract = false;
  118. Dirty(args.User, cuffable);
  119. }
  120. else
  121. {
  122. // Check if the user can interact.
  123. if (!_actionBlocker.CanInteract(args.User, args.Target))
  124. args.Cancelled = true;
  125. }
  126. if (args.Cancelled)
  127. {
  128. _popup.PopupClient(Loc.GetString("cuffable-component-cannot-interact-message"), args.Target, args.User);
  129. }
  130. }
  131. private void OnStartup(EntityUid uid, CuffableComponent component, ComponentInit args)
  132. {
  133. component.Container = _container.EnsureContainer<Container>(uid, _componentFactory.GetComponentName(component.GetType()));
  134. }
  135. private void OnRejuvenate(EntityUid uid, CuffableComponent component, RejuvenateEvent args)
  136. {
  137. _container.EmptyContainer(component.Container, true);
  138. }
  139. private void OnCuffsRemovedFromContainer(EntityUid uid, CuffableComponent component, EntRemovedFromContainerMessage args)
  140. {
  141. // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
  142. if (args.Container.ID != component.Container?.ID)
  143. return;
  144. _virtualItem.DeleteInHandsMatching(uid, args.Entity);
  145. UpdateCuffState(uid, component);
  146. }
  147. private void OnCuffsInsertedIntoContainer(EntityUid uid, CuffableComponent component, ContainerModifiedMessage args)
  148. {
  149. if (args.Container == component.Container)
  150. UpdateCuffState(uid, component);
  151. }
  152. public void UpdateCuffState(EntityUid uid, CuffableComponent component)
  153. {
  154. var canInteract = TryComp(uid, out HandsComponent? hands) && hands.Hands.Count > component.CuffedHandCount;
  155. if (canInteract == component.CanStillInteract)
  156. return;
  157. component.CanStillInteract = canInteract;
  158. Dirty(uid, component);
  159. _actionBlocker.UpdateCanMove(uid);
  160. if (component.CanStillInteract)
  161. _alerts.ClearAlert(uid, component.CuffedAlert);
  162. else
  163. _alerts.ShowAlert(uid, component.CuffedAlert);
  164. var ev = new CuffedStateChangeEvent();
  165. RaiseLocalEvent(uid, ref ev);
  166. }
  167. private void OnBeingPulledAttempt(EntityUid uid, CuffableComponent component, BeingPulledAttemptEvent args)
  168. {
  169. if (!TryComp<PullableComponent>(uid, out var pullable))
  170. return;
  171. if (pullable.Puller != null && !component.CanStillInteract) // If we are being pulled already and cuffed, we can't get pulled again.
  172. args.Cancel();
  173. }
  174. private void OnBuckleAttempt(Entity<CuffableComponent> ent, EntityUid? user, ref bool cancelled, bool buckling, bool popup)
  175. {
  176. if (cancelled || user != ent.Owner)
  177. return;
  178. if (!TryComp<HandsComponent>(ent, out var hands) || ent.Comp.CuffedHandCount < hands.Count)
  179. return;
  180. cancelled = true;
  181. if (!popup)
  182. return;
  183. var message = buckling
  184. ? Loc.GetString("handcuff-component-cuff-interrupt-buckled-message")
  185. : Loc.GetString("handcuff-component-cuff-interrupt-unbuckled-message");
  186. _popup.PopupClient(message, ent, user);
  187. }
  188. private void OnBuckleAttemptEvent(Entity<CuffableComponent> ent, ref BuckleAttemptEvent args)
  189. {
  190. OnBuckleAttempt(ent, args.User, ref args.Cancelled, true, args.Popup);
  191. }
  192. private void OnUnbuckleAttemptEvent(Entity<CuffableComponent> ent, ref UnbuckleAttemptEvent args)
  193. {
  194. OnBuckleAttempt(ent, args.User, ref args.Cancelled, false, args.Popup);
  195. }
  196. private void OnPull(EntityUid uid, CuffableComponent component, PullMessage args)
  197. {
  198. if (!component.CanStillInteract)
  199. _actionBlocker.UpdateCanMove(uid);
  200. }
  201. private void HandleMoveAttempt(EntityUid uid, CuffableComponent component, UpdateCanMoveEvent args)
  202. {
  203. if (component.CanStillInteract || !EntityManager.TryGetComponent(uid, out PullableComponent? pullable) || !pullable.BeingPulled)
  204. return;
  205. args.Cancel();
  206. }
  207. private void HandleStopPull(EntityUid uid, CuffableComponent component, AttemptStopPullingEvent args)
  208. {
  209. if (args.User == null || !Exists(args.User.Value))
  210. return;
  211. if (args.User.Value == uid && !component.CanStillInteract)
  212. args.Cancelled = true;
  213. }
  214. private void OnRemoveCuffsAlert(Entity<CuffableComponent> ent, ref RemoveCuffsAlertEvent args)
  215. {
  216. if (args.Handled)
  217. return;
  218. TryUncuff(ent, ent, cuffable: ent.Comp);
  219. args.Handled = true;
  220. }
  221. private void AddUncuffVerb(EntityUid uid, CuffableComponent component, GetVerbsEvent<Verb> args)
  222. {
  223. // Can the user access the cuffs, and is there even anything to uncuff?
  224. if (!args.CanAccess || component.CuffedHandCount == 0 || args.Hands == null)
  225. return;
  226. // We only check can interact if the user is not uncuffing themselves. As a result, the verb will show up
  227. // when the user is incapacitated & trying to uncuff themselves, but TryUncuff() will still fail when
  228. // attempted.
  229. if (args.User != args.Target && !args.CanInteract)
  230. return;
  231. Verb verb = new()
  232. {
  233. Act = () => TryUncuff(uid, args.User, cuffable: component),
  234. DoContactInteraction = true,
  235. Text = Loc.GetString("uncuff-verb-get-data-text")
  236. };
  237. //TODO VERB ICON add uncuffing symbol? may re-use the alert symbol showing that you are currently cuffed?
  238. args.Verbs.Add(verb);
  239. }
  240. private void OnCuffableDoAfter(EntityUid uid, CuffableComponent component, UnCuffDoAfterEvent args)
  241. {
  242. if (args.Args.Target is not { } target || args.Args.Used is not { } used)
  243. return;
  244. if (args.Handled)
  245. return;
  246. args.Handled = true;
  247. var user = args.Args.User;
  248. if (!args.Cancelled)
  249. {
  250. Uncuff(target, user, used, component);
  251. }
  252. else
  253. {
  254. _popup.PopupClient(Loc.GetString("cuffable-component-remove-cuffs-fail-message"), user, user);
  255. }
  256. }
  257. private void OnCuffAfterInteract(EntityUid uid, HandcuffComponent component, AfterInteractEvent args)
  258. {
  259. if (args.Target is not { Valid: true } target)
  260. return;
  261. if (!args.CanReach)
  262. {
  263. _popup.PopupClient(Loc.GetString("handcuff-component-too-far-away-error"), args.User, args.User);
  264. return;
  265. }
  266. var result = TryCuffing(args.User, target, uid, component);
  267. args.Handled = result;
  268. }
  269. private void OnCuffMeleeHit(EntityUid uid, HandcuffComponent component, MeleeHitEvent args)
  270. {
  271. if (!args.HitEntities.Any())
  272. return;
  273. TryCuffing(args.User, args.HitEntities.First(), uid, component);
  274. args.Handled = true;
  275. }
  276. private void OnAddCuffDoAfter(EntityUid uid, HandcuffComponent component, AddCuffDoAfterEvent args)
  277. {
  278. var user = args.Args.User;
  279. if (!TryComp<CuffableComponent>(args.Args.Target, out var cuffable))
  280. return;
  281. var target = args.Args.Target.Value;
  282. if (args.Handled)
  283. return;
  284. args.Handled = true;
  285. if (!args.Cancelled && TryAddNewCuffs(target, user, uid, cuffable))
  286. {
  287. component.Used = true;
  288. _audio.PlayPredicted(component.EndCuffSound, uid, user);
  289. var popupText = (user == target)
  290. ? "handcuff-component-cuff-self-observer-success-message"
  291. : "handcuff-component-cuff-observer-success-message";
  292. _popup.PopupEntity(Loc.GetString(popupText,
  293. ("user", Identity.Name(user, EntityManager)), ("target", Identity.Entity(target, EntityManager))),
  294. target, Filter.Pvs(target, entityManager: EntityManager)
  295. .RemoveWhere(e => e.AttachedEntity == target || e.AttachedEntity == user), true);
  296. if (target == user)
  297. {
  298. _popup.PopupClient(Loc.GetString("handcuff-component-cuff-self-success-message"), user, user);
  299. _adminLog.Add(LogType.Action, LogImpact.Medium,
  300. $"{ToPrettyString(user):player} has cuffed himself");
  301. }
  302. else
  303. {
  304. _popup.PopupClient(Loc.GetString("handcuff-component-cuff-other-success-message",
  305. ("otherName", Identity.Name(target, EntityManager, user))), user, user);
  306. _popup.PopupClient(Loc.GetString("handcuff-component-cuff-by-other-success-message",
  307. ("otherName", Identity.Name(user, EntityManager, target))), target, target);
  308. _adminLog.Add(LogType.Action, LogImpact.Medium,
  309. $"{ToPrettyString(user):player} has cuffed {ToPrettyString(target):player}");
  310. }
  311. }
  312. else
  313. {
  314. if (target == user)
  315. {
  316. _popup.PopupClient(Loc.GetString("handcuff-component-cuff-interrupt-self-message"), user, user);
  317. }
  318. else
  319. {
  320. // TODO Fix popup message wording
  321. // This message assumes that the user being handcuffed is the one that caused the handcuff to fail.
  322. _popup.PopupClient(Loc.GetString("handcuff-component-cuff-interrupt-message",
  323. ("targetName", Identity.Name(target, EntityManager, user))), user, user);
  324. _popup.PopupClient(Loc.GetString("handcuff-component-cuff-interrupt-other-message",
  325. ("otherName", Identity.Name(user, EntityManager, target))), target, target);
  326. }
  327. }
  328. }
  329. private void OnCuffVirtualItemDeleted(EntityUid uid, HandcuffComponent component, VirtualItemDeletedEvent args)
  330. {
  331. Uncuff(args.User, null, uid, cuff: component);
  332. }
  333. /// <summary>
  334. /// Check the current amount of hands the owner has, and if there's less hands than active cuffs we remove some cuffs.
  335. /// </summary>
  336. private void OnHandCountChanged(Entity<CuffableComponent> ent, ref HandCountChangedEvent message)
  337. {
  338. var dirty = false;
  339. var handCount = CompOrNull<HandsComponent>(ent.Owner)?.Count ?? 0;
  340. while (ent.Comp.CuffedHandCount > handCount && ent.Comp.CuffedHandCount > 0)
  341. {
  342. dirty = true;
  343. var handcuffContainer = ent.Comp.Container;
  344. var handcuffEntity = handcuffContainer.ContainedEntities[^1];
  345. _transform.PlaceNextTo(handcuffEntity, ent.Owner);
  346. }
  347. if (dirty)
  348. {
  349. UpdateCuffState(ent.Owner, ent.Comp);
  350. }
  351. }
  352. /// <summary>
  353. /// Adds virtual cuff items to the user's hands.
  354. /// </summary>
  355. private void UpdateHeldItems(EntityUid uid, EntityUid handcuff, CuffableComponent? component = null)
  356. {
  357. if (!Resolve(uid, ref component))
  358. return;
  359. // TODO we probably don't just want to use the generic virtual-item entity, and instead
  360. // want to add our own item, so that use-in-hand triggers an uncuff attempt and the like.
  361. if (!TryComp<HandsComponent>(uid, out var handsComponent))
  362. return;
  363. var freeHands = 0;
  364. foreach (var hand in _hands.EnumerateHands(uid, handsComponent))
  365. {
  366. if (hand.HeldEntity == null)
  367. {
  368. freeHands++;
  369. continue;
  370. }
  371. // Is this entity removable? (it might be an existing handcuff blocker)
  372. if (HasComp<UnremoveableComponent>(hand.HeldEntity))
  373. continue;
  374. _hands.DoDrop(uid, hand, true, handsComponent);
  375. freeHands++;
  376. if (freeHands == 2)
  377. break;
  378. }
  379. if (_virtualItem.TrySpawnVirtualItemInHand(handcuff, uid, out var virtItem1))
  380. EnsureComp<UnremoveableComponent>(virtItem1.Value);
  381. if (_virtualItem.TrySpawnVirtualItemInHand(handcuff, uid, out var virtItem2))
  382. EnsureComp<UnremoveableComponent>(virtItem2.Value);
  383. }
  384. /// <summary>
  385. /// Add a set of cuffs to an existing CuffedComponent.
  386. /// </summary>
  387. public bool TryAddNewCuffs(EntityUid target, EntityUid user, EntityUid handcuff, CuffableComponent? component = null, HandcuffComponent? cuff = null)
  388. {
  389. if (!Resolve(target, ref component) || !Resolve(handcuff, ref cuff))
  390. return false;
  391. if (!_interaction.InRangeUnobstructed(handcuff, target))
  392. return false;
  393. // if the amount of hands the target has is equal to or less than the amount of hands that are cuffed
  394. // don't apply the new set of cuffs
  395. // (how would you even end up with more cuffed hands than actual hands? either way accounting for it)
  396. if (TryComp<HandsComponent>(target, out var hands) && hands.Count <= component.CuffedHandCount)
  397. return false;
  398. // Success!
  399. _hands.TryDrop(user, handcuff);
  400. _container.Insert(handcuff, component.Container);
  401. UpdateHeldItems(target, handcuff, component);
  402. return true;
  403. }
  404. /// <returns>False if the target entity isn't cuffable.</returns>
  405. public bool TryCuffing(EntityUid user, EntityUid target, EntityUid handcuff, HandcuffComponent? handcuffComponent = null, CuffableComponent? cuffable = null)
  406. {
  407. if (!Resolve(handcuff, ref handcuffComponent) || !Resolve(target, ref cuffable, false))
  408. return false;
  409. if (!TryComp<HandsComponent>(target, out var hands))
  410. {
  411. _popup.PopupClient(Loc.GetString("handcuff-component-target-has-no-hands-error",
  412. ("targetName", Identity.Name(target, EntityManager, user))), user, user);
  413. return true;
  414. }
  415. if (cuffable.CuffedHandCount >= hands.Count)
  416. {
  417. _popup.PopupClient(Loc.GetString("handcuff-component-target-has-no-free-hands-error",
  418. ("targetName", Identity.Name(target, EntityManager, user))), user, user);
  419. return true;
  420. }
  421. if (!_hands.CanDrop(user, handcuff))
  422. {
  423. _popup.PopupClient(Loc.GetString("handcuff-component-cannot-drop-cuffs", ("target", Identity.Name(target, EntityManager, user))), user, user);
  424. return false;
  425. }
  426. var cuffTime = handcuffComponent.CuffTime;
  427. if (HasComp<StunnedComponent>(target))
  428. cuffTime = MathF.Max(0.1f, cuffTime - handcuffComponent.StunBonus);
  429. if (HasComp<DisarmProneComponent>(target))
  430. cuffTime = 0.0f; // cuff them instantly.
  431. var doAfterEventArgs = new DoAfterArgs(EntityManager, user, cuffTime, new AddCuffDoAfterEvent(), handcuff, target, handcuff)
  432. {
  433. BreakOnMove = true,
  434. BreakOnWeightlessMove = false,
  435. BreakOnDamage = true,
  436. NeedHand = true,
  437. DistanceThreshold = 1f // shorter than default but still feels good
  438. };
  439. if (!_doAfter.TryStartDoAfter(doAfterEventArgs))
  440. return true;
  441. var popupText = (user == target)
  442. ? "handcuff-component-start-cuffing-self-observer"
  443. : "handcuff-component-start-cuffing-observer";
  444. _popup.PopupEntity(Loc.GetString(popupText,
  445. ("user", Identity.Name(user, EntityManager)), ("target", Identity.Entity(target, EntityManager))),
  446. target, Filter.Pvs(target, entityManager: EntityManager)
  447. .RemoveWhere(e => e.AttachedEntity == target || e.AttachedEntity == user), true);
  448. if (target == user)
  449. {
  450. _popup.PopupClient(Loc.GetString("handcuff-component-target-self"), user, user);
  451. }
  452. else
  453. {
  454. _popup.PopupClient(Loc.GetString("handcuff-component-start-cuffing-target-message",
  455. ("targetName", Identity.Name(target, EntityManager, user))), user, user);
  456. _popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-by-other-message",
  457. ("otherName", Identity.Name(user, EntityManager, target))), target, target);
  458. }
  459. _audio.PlayPredicted(handcuffComponent.StartCuffSound, handcuff, user);
  460. return true;
  461. }
  462. /// <summary>
  463. /// Checks if the target is handcuffed.
  464. /// </summary>
  465. /// /// <param name="target">The entity to be checked</param>
  466. /// <param name="requireFullyCuffed">when true, return false if the target is only partially cuffed (for things with more than 2 hands)</param>
  467. /// <returns></returns>
  468. public bool IsCuffed(Entity<CuffableComponent> target, bool requireFullyCuffed = true)
  469. {
  470. if (!TryComp<HandsComponent>(target, out var hands))
  471. return false;
  472. if (target.Comp.CuffedHandCount <= 0)
  473. return false;
  474. if (requireFullyCuffed && hands.Count > target.Comp.CuffedHandCount)
  475. return false;
  476. return true;
  477. }
  478. /// <summary>
  479. /// Attempt to uncuff a cuffed entity. Can be called by the cuffed entity, or another entity trying to help uncuff them.
  480. /// If the uncuffing succeeds, the cuffs will drop on the floor.
  481. /// </summary>
  482. /// <param name="target"></param>
  483. /// <param name="user">The cuffed entity</param>
  484. /// <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>
  485. /// <param name="cuffable"></param>
  486. /// <param name="cuff"></param>
  487. public void TryUncuff(EntityUid target, EntityUid user, EntityUid? cuffsToRemove = null, CuffableComponent? cuffable = null, HandcuffComponent? cuff = null)
  488. {
  489. if (!Resolve(target, ref cuffable))
  490. return;
  491. var isOwner = user == target;
  492. if (cuffsToRemove == null)
  493. {
  494. if (cuffable.Container.ContainedEntities.Count == 0)
  495. {
  496. return;
  497. }
  498. cuffsToRemove = cuffable.LastAddedCuffs;
  499. }
  500. else
  501. {
  502. if (!cuffable.Container.ContainedEntities.Contains(cuffsToRemove.Value))
  503. {
  504. Log.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!");
  505. }
  506. }
  507. if (!Resolve(cuffsToRemove.Value, ref cuff))
  508. return;
  509. var attempt = new UncuffAttemptEvent(user, target);
  510. RaiseLocalEvent(user, ref attempt, true);
  511. if (attempt.Cancelled)
  512. {
  513. return;
  514. }
  515. if (!isOwner && !_interaction.InRangeUnobstructed(user, target))
  516. {
  517. _popup.PopupClient(Loc.GetString("cuffable-component-cannot-remove-cuffs-too-far-message"), user, user);
  518. return;
  519. }
  520. var ev = new ModifyUncuffDurationEvent(user, target, isOwner ? cuff.BreakoutTime : cuff.UncuffTime);
  521. RaiseLocalEvent(user, ref ev);
  522. var uncuffTime = ev.Duration;
  523. if (isOwner)
  524. {
  525. if (!TryComp(cuffsToRemove.Value, out UseDelayComponent? useDelay))
  526. return;
  527. if (!_delay.TryResetDelay((cuffsToRemove.Value, useDelay), true))
  528. {
  529. return;
  530. }
  531. }
  532. var doAfterEventArgs = new DoAfterArgs(EntityManager, user, uncuffTime, new UnCuffDoAfterEvent(), target, target, cuffsToRemove)
  533. {
  534. BreakOnMove = true,
  535. BreakOnWeightlessMove = false,
  536. BreakOnDamage = true,
  537. NeedHand = true,
  538. RequireCanInteract = false, // Trust in UncuffAttemptEvent
  539. DistanceThreshold = 1f // shorter than default but still feels good
  540. };
  541. if (!_doAfter.TryStartDoAfter(doAfterEventArgs))
  542. return;
  543. _adminLog.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user)} is trying to uncuff {ToPrettyString(target)}");
  544. var popupText = user == target
  545. ? "cuffable-component-start-uncuffing-self-observer"
  546. : "cuffable-component-start-uncuffing-observer";
  547. _popup.PopupEntity(
  548. Loc.GetString(popupText,
  549. ("user", Identity.Name(user, EntityManager)),
  550. ("target", Identity.Entity(target, EntityManager))),
  551. target,
  552. Filter.Pvs(target, entityManager: EntityManager)
  553. .RemoveWhere(e => e.AttachedEntity == target || e.AttachedEntity == user),
  554. true);
  555. if (target == user)
  556. {
  557. _popup.PopupClient(Loc.GetString("cuffable-component-start-uncuffing-self"), user, user);
  558. }
  559. else
  560. {
  561. _popup.PopupClient(Loc.GetString("cuffable-component-start-uncuffing-target-message",
  562. ("targetName", Identity.Name(target, EntityManager, user))),
  563. user,
  564. user);
  565. _popup.PopupEntity(Loc.GetString("cuffable-component-start-uncuffing-by-other-message",
  566. ("otherName", Identity.Name(user, EntityManager, target))),
  567. target,
  568. target);
  569. }
  570. _audio.PlayPredicted(isOwner ? cuff.StartBreakoutSound : cuff.StartUncuffSound, target, user);
  571. }
  572. public void Uncuff(EntityUid target, EntityUid? user, EntityUid cuffsToRemove, CuffableComponent? cuffable = null, HandcuffComponent? cuff = null)
  573. {
  574. if (!Resolve(target, ref cuffable) || !Resolve(cuffsToRemove, ref cuff))
  575. return;
  576. if (!cuff.Used || cuff.Removing || TerminatingOrDeleted(cuffsToRemove) || TerminatingOrDeleted(target))
  577. return;
  578. if (user != null)
  579. {
  580. var attempt = new UncuffAttemptEvent(user.Value, target);
  581. RaiseLocalEvent(user.Value, ref attempt);
  582. if (attempt.Cancelled)
  583. return;
  584. }
  585. cuff.Removing = true;
  586. cuff.Used = false;
  587. _audio.PlayPredicted(cuff.EndUncuffSound, target, user);
  588. _container.Remove(cuffsToRemove, cuffable.Container);
  589. if (_net.IsServer)
  590. {
  591. // Handles spawning broken cuffs on server to avoid client misprediction
  592. if (cuff.BreakOnRemove)
  593. {
  594. QueueDel(cuffsToRemove);
  595. var trash = Spawn(cuff.BrokenPrototype, Transform(cuffsToRemove).Coordinates);
  596. _hands.PickupOrDrop(user, trash);
  597. }
  598. else
  599. {
  600. _hands.PickupOrDrop(user, cuffsToRemove);
  601. }
  602. }
  603. if (cuffable.CuffedHandCount == 0)
  604. {
  605. if (user != null)
  606. _popup.PopupClient(Loc.GetString("cuffable-component-remove-cuffs-success-message"), user.Value, user.Value);
  607. if (target != user && user != null)
  608. {
  609. _popup.PopupEntity(Loc.GetString("cuffable-component-remove-cuffs-by-other-success-message",
  610. ("otherName", Identity.Name(user.Value, EntityManager, user))), target, target);
  611. _adminLog.Add(LogType.Action, LogImpact.Medium,
  612. $"{ToPrettyString(user):player} has successfully uncuffed {ToPrettyString(target):player}");
  613. }
  614. else
  615. {
  616. _adminLog.Add(LogType.Action, LogImpact.Medium,
  617. $"{ToPrettyString(user):player} has successfully uncuffed themselves");
  618. }
  619. }
  620. else if (user != null)
  621. {
  622. if (user != target)
  623. {
  624. _popup.PopupClient(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message",
  625. ("cuffedHandCount", cuffable.CuffedHandCount),
  626. ("otherName", Identity.Name(user.Value, EntityManager, user.Value))), user.Value, user.Value);
  627. _popup.PopupEntity(Loc.GetString(
  628. "cuffable-component-remove-cuffs-by-other-partial-success-message",
  629. ("otherName", Identity.Name(user.Value, EntityManager, user.Value)),
  630. ("cuffedHandCount", cuffable.CuffedHandCount)), target, target);
  631. }
  632. else
  633. {
  634. _popup.PopupClient(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message",
  635. ("cuffedHandCount", cuffable.CuffedHandCount)), user.Value, user.Value);
  636. }
  637. }
  638. cuff.Removing = false;
  639. }
  640. #region ActionBlocker
  641. private void CheckAct(EntityUid uid, CuffableComponent component, CancellableEntityEventArgs args)
  642. {
  643. if (!component.CanStillInteract)
  644. args.Cancel();
  645. }
  646. private void OnEquipAttempt(EntityUid uid, CuffableComponent component, IsEquippingAttemptEvent args)
  647. {
  648. // is this a self-equip, or are they being stripped?
  649. if (args.Equipee == uid)
  650. CheckAct(uid, component, args);
  651. }
  652. private void OnUnequipAttempt(EntityUid uid, CuffableComponent component, IsUnequippingAttemptEvent args)
  653. {
  654. // is this a self-equip, or are they being stripped?
  655. if (args.Unequipee == uid)
  656. CheckAct(uid, component, args);
  657. }
  658. #endregion
  659. public IReadOnlyList<EntityUid> GetAllCuffs(CuffableComponent component)
  660. {
  661. return component.Container.ContainedEntities;
  662. }
  663. [Serializable, NetSerializable]
  664. private sealed partial class UnCuffDoAfterEvent : SimpleDoAfterEvent
  665. {
  666. }
  667. [Serializable, NetSerializable]
  668. private sealed partial class AddCuffDoAfterEvent : SimpleDoAfterEvent
  669. {
  670. }
  671. }
  672. }