SharedCuffableSystem.cs 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802
  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. // Shitmed Change Start
  399. EnsureComp<HandcuffComponent>(handcuff, out var handcuffsComp);
  400. handcuffsComp.Used = true;
  401. Dirty(handcuff, handcuffsComp);
  402. // Success!
  403. _hands.TryDrop(user, handcuff);
  404. var result = _container.Insert(handcuff, component.Container);
  405. // Shitmed Change End
  406. UpdateHeldItems(target, handcuff, component);
  407. return true;
  408. }
  409. /// <returns>False if the target entity isn't cuffable.</returns>
  410. public bool TryCuffing(EntityUid user, EntityUid target, EntityUid handcuff, HandcuffComponent? handcuffComponent = null, CuffableComponent? cuffable = null)
  411. {
  412. if (!Resolve(handcuff, ref handcuffComponent) || !Resolve(target, ref cuffable, false))
  413. return false;
  414. if (!TryComp<HandsComponent>(target, out var hands))
  415. {
  416. _popup.PopupClient(Loc.GetString("handcuff-component-target-has-no-hands-error",
  417. ("targetName", Identity.Name(target, EntityManager, user))), user, user);
  418. return true;
  419. }
  420. if (cuffable.CuffedHandCount >= hands.Count)
  421. {
  422. _popup.PopupClient(Loc.GetString("handcuff-component-target-has-no-free-hands-error",
  423. ("targetName", Identity.Name(target, EntityManager, user))), user, user);
  424. return true;
  425. }
  426. if (!_hands.CanDrop(user, handcuff))
  427. {
  428. _popup.PopupClient(Loc.GetString("handcuff-component-cannot-drop-cuffs", ("target", Identity.Name(target, EntityManager, user))), user, user);
  429. return false;
  430. }
  431. var cuffTime = handcuffComponent.CuffTime;
  432. if (HasComp<StunnedComponent>(target))
  433. cuffTime = MathF.Max(0.1f, cuffTime - handcuffComponent.StunBonus);
  434. if (HasComp<DisarmProneComponent>(target))
  435. cuffTime = 0.0f; // cuff them instantly.
  436. var doAfterEventArgs = new DoAfterArgs(EntityManager, user, cuffTime, new AddCuffDoAfterEvent(), handcuff, target, handcuff)
  437. {
  438. BreakOnMove = true,
  439. BreakOnWeightlessMove = false,
  440. BreakOnDamage = true,
  441. NeedHand = true,
  442. DistanceThreshold = 1f // shorter than default but still feels good
  443. };
  444. if (!_doAfter.TryStartDoAfter(doAfterEventArgs))
  445. return true;
  446. var popupText = (user == target)
  447. ? "handcuff-component-start-cuffing-self-observer"
  448. : "handcuff-component-start-cuffing-observer";
  449. _popup.PopupEntity(Loc.GetString(popupText,
  450. ("user", Identity.Name(user, EntityManager)), ("target", Identity.Entity(target, EntityManager))),
  451. target, Filter.Pvs(target, entityManager: EntityManager)
  452. .RemoveWhere(e => e.AttachedEntity == target || e.AttachedEntity == user), true);
  453. if (target == user)
  454. {
  455. _popup.PopupClient(Loc.GetString("handcuff-component-target-self"), user, user);
  456. }
  457. else
  458. {
  459. _popup.PopupClient(Loc.GetString("handcuff-component-start-cuffing-target-message",
  460. ("targetName", Identity.Name(target, EntityManager, user))), user, user);
  461. _popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-by-other-message",
  462. ("otherName", Identity.Name(user, EntityManager, target))), target, target);
  463. }
  464. _audio.PlayPredicted(handcuffComponent.StartCuffSound, handcuff, user);
  465. return true;
  466. }
  467. /// <summary>
  468. /// Checks if the target is handcuffed.
  469. /// </summary>
  470. /// /// <param name="target">The entity to be checked</param>
  471. /// <param name="requireFullyCuffed">when true, return false if the target is only partially cuffed (for things with more than 2 hands)</param>
  472. /// <returns></returns>
  473. public bool IsCuffed(Entity<CuffableComponent> target, bool requireFullyCuffed = true)
  474. {
  475. if (!TryComp<HandsComponent>(target, out var hands))
  476. return false;
  477. if (target.Comp.CuffedHandCount <= 0)
  478. return false;
  479. if (requireFullyCuffed && hands.Count > target.Comp.CuffedHandCount)
  480. return false;
  481. return true;
  482. }
  483. /// <summary>
  484. /// Attempt to uncuff a cuffed entity. Can be called by the cuffed entity, or another entity trying to help uncuff them.
  485. /// If the uncuffing succeeds, the cuffs will drop on the floor.
  486. /// </summary>
  487. /// <param name="target"></param>
  488. /// <param name="user">The cuffed entity</param>
  489. /// <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>
  490. /// <param name="cuffable"></param>
  491. /// <param name="cuff"></param>
  492. public void TryUncuff(EntityUid target, EntityUid user, EntityUid? cuffsToRemove = null, CuffableComponent? cuffable = null, HandcuffComponent? cuff = null)
  493. {
  494. if (!Resolve(target, ref cuffable))
  495. return;
  496. var isOwner = user == target;
  497. if (cuffsToRemove == null)
  498. {
  499. if (cuffable.Container.ContainedEntities.Count == 0)
  500. {
  501. return;
  502. }
  503. cuffsToRemove = cuffable.LastAddedCuffs;
  504. }
  505. else
  506. {
  507. if (!cuffable.Container.ContainedEntities.Contains(cuffsToRemove.Value))
  508. {
  509. Log.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!");
  510. }
  511. }
  512. if (!Resolve(cuffsToRemove.Value, ref cuff))
  513. return;
  514. var attempt = new UncuffAttemptEvent(user, target);
  515. RaiseLocalEvent(user, ref attempt, true);
  516. if (attempt.Cancelled)
  517. {
  518. return;
  519. }
  520. if (!isOwner && !_interaction.InRangeUnobstructed(user, target))
  521. {
  522. _popup.PopupClient(Loc.GetString("cuffable-component-cannot-remove-cuffs-too-far-message"), user, user);
  523. return;
  524. }
  525. var ev = new ModifyUncuffDurationEvent(user, target, isOwner ? cuff.BreakoutTime : cuff.UncuffTime);
  526. RaiseLocalEvent(user, ref ev);
  527. var uncuffTime = ev.Duration;
  528. if (isOwner)
  529. {
  530. if (!TryComp(cuffsToRemove.Value, out UseDelayComponent? useDelay))
  531. return;
  532. if (!_delay.TryResetDelay((cuffsToRemove.Value, useDelay), true))
  533. {
  534. return;
  535. }
  536. }
  537. var doAfterEventArgs = new DoAfterArgs(EntityManager, user, uncuffTime, new UnCuffDoAfterEvent(), target, target, cuffsToRemove)
  538. {
  539. BreakOnMove = true,
  540. BreakOnWeightlessMove = false,
  541. BreakOnDamage = true,
  542. NeedHand = true,
  543. RequireCanInteract = false, // Trust in UncuffAttemptEvent
  544. DistanceThreshold = 1f // shorter than default but still feels good
  545. };
  546. if (!_doAfter.TryStartDoAfter(doAfterEventArgs))
  547. return;
  548. _adminLog.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user)} is trying to uncuff {ToPrettyString(target)}");
  549. var popupText = user == target
  550. ? "cuffable-component-start-uncuffing-self-observer"
  551. : "cuffable-component-start-uncuffing-observer";
  552. _popup.PopupEntity(
  553. Loc.GetString(popupText,
  554. ("user", Identity.Name(user, EntityManager)),
  555. ("target", Identity.Entity(target, EntityManager))),
  556. target,
  557. Filter.Pvs(target, entityManager: EntityManager)
  558. .RemoveWhere(e => e.AttachedEntity == target || e.AttachedEntity == user),
  559. true);
  560. if (target == user)
  561. {
  562. _popup.PopupClient(Loc.GetString("cuffable-component-start-uncuffing-self"), user, user);
  563. }
  564. else
  565. {
  566. _popup.PopupClient(Loc.GetString("cuffable-component-start-uncuffing-target-message",
  567. ("targetName", Identity.Name(target, EntityManager, user))),
  568. user,
  569. user);
  570. _popup.PopupEntity(Loc.GetString("cuffable-component-start-uncuffing-by-other-message",
  571. ("otherName", Identity.Name(user, EntityManager, target))),
  572. target,
  573. target);
  574. }
  575. _audio.PlayPredicted(isOwner ? cuff.StartBreakoutSound : cuff.StartUncuffSound, target, user);
  576. }
  577. public void Uncuff(EntityUid target, EntityUid? user, EntityUid cuffsToRemove, CuffableComponent? cuffable = null, HandcuffComponent? cuff = null)
  578. {
  579. if (!Resolve(target, ref cuffable) || !Resolve(cuffsToRemove, ref cuff))
  580. return;
  581. if (!cuff.Used || cuff.Removing || TerminatingOrDeleted(cuffsToRemove) || TerminatingOrDeleted(target))
  582. return;
  583. if (user != null)
  584. {
  585. var attempt = new UncuffAttemptEvent(user.Value, target);
  586. RaiseLocalEvent(user.Value, ref attempt);
  587. if (attempt.Cancelled)
  588. return;
  589. }
  590. cuff.Removing = true;
  591. cuff.Used = false;
  592. _audio.PlayPredicted(cuff.EndUncuffSound, target, user);
  593. _container.Remove(cuffsToRemove, cuffable.Container);
  594. if (_net.IsServer)
  595. {
  596. // Handles spawning broken cuffs on server to avoid client misprediction
  597. if (cuff.BreakOnRemove)
  598. {
  599. QueueDel(cuffsToRemove);
  600. var trash = Spawn(cuff.BrokenPrototype, Transform(cuffsToRemove).Coordinates);
  601. _hands.PickupOrDrop(user, trash);
  602. }
  603. else
  604. {
  605. _hands.PickupOrDrop(user, cuffsToRemove);
  606. }
  607. }
  608. if (cuffable.CuffedHandCount == 0)
  609. {
  610. if (user != null)
  611. _popup.PopupClient(Loc.GetString("cuffable-component-remove-cuffs-success-message"), user.Value, user.Value);
  612. if (target != user && user != null)
  613. {
  614. _popup.PopupEntity(Loc.GetString("cuffable-component-remove-cuffs-by-other-success-message",
  615. ("otherName", Identity.Name(user.Value, EntityManager, user))), target, target);
  616. _adminLog.Add(LogType.Action, LogImpact.Medium,
  617. $"{ToPrettyString(user):player} has successfully uncuffed {ToPrettyString(target):player}");
  618. }
  619. else
  620. {
  621. _adminLog.Add(LogType.Action, LogImpact.Medium,
  622. $"{ToPrettyString(user):player} has successfully uncuffed themselves");
  623. }
  624. }
  625. else if (user != null)
  626. {
  627. if (user != target)
  628. {
  629. _popup.PopupClient(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message",
  630. ("cuffedHandCount", cuffable.CuffedHandCount),
  631. ("otherName", Identity.Name(user.Value, EntityManager, user.Value))), user.Value, user.Value);
  632. _popup.PopupEntity(Loc.GetString(
  633. "cuffable-component-remove-cuffs-by-other-partial-success-message",
  634. ("otherName", Identity.Name(user.Value, EntityManager, user.Value)),
  635. ("cuffedHandCount", cuffable.CuffedHandCount)), target, target);
  636. }
  637. else
  638. {
  639. _popup.PopupClient(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message",
  640. ("cuffedHandCount", cuffable.CuffedHandCount)), user.Value, user.Value);
  641. }
  642. }
  643. cuff.Removing = false;
  644. }
  645. #region ActionBlocker
  646. private void CheckAct(EntityUid uid, CuffableComponent component, CancellableEntityEventArgs args)
  647. {
  648. if (!component.CanStillInteract)
  649. args.Cancel();
  650. }
  651. private void OnEquipAttempt(EntityUid uid, CuffableComponent component, IsEquippingAttemptEvent args)
  652. {
  653. // is this a self-equip, or are they being stripped?
  654. if (args.Equipee == uid)
  655. CheckAct(uid, component, args);
  656. }
  657. private void OnUnequipAttempt(EntityUid uid, CuffableComponent component, IsUnequippingAttemptEvent args)
  658. {
  659. // is this a self-equip, or are they being stripped?
  660. if (args.Unequipee == uid)
  661. CheckAct(uid, component, args);
  662. }
  663. #endregion
  664. public IReadOnlyList<EntityUid> GetAllCuffs(CuffableComponent component)
  665. {
  666. return component.Container.ContainedEntities;
  667. }
  668. [Serializable, NetSerializable]
  669. private sealed partial class UnCuffDoAfterEvent : SimpleDoAfterEvent
  670. {
  671. }
  672. [Serializable, NetSerializable]
  673. private sealed partial class AddCuffDoAfterEvent : SimpleDoAfterEvent
  674. {
  675. }
  676. }
  677. }