Files
wwdpublic/Content.Shared/Cuffs/SharedCuffableSystem.cs
RedFoxIV daf4f66414 "Proper" "Softcrit" "Support" (#1545)
# Description

Implements the softcrit functionality.
Similiar to critical state but spessmen will be able to communicate and
crawl around, but not pick up items.
Also supports configuring what is and isn't allowed in different
MobStates (per mob prototype): you can enable picking up items while in
softcrit so people can pick up their lasgun and continue shooting after
taking a 40x46mm to their ass cheeks from the guest nukies while being
dragged to safety.

![escape-from-tarkov-raid](https://github.com/user-attachments/assets/7f31702d-5677-4daf-a13d-8a9525fd3f9f)

<details> <summary><h1>Technical details</h1></summary>
New prototype type: "mobStateParams" (`MobStateParametersPrototype`)
Used to specify what can and can't be done when in a certain mobstate.
Of note that they are not actually bound to any `MobState` by
themselves. To assign a params prototype to a mobstate, use
`InitMobStateParams` in `MobStateComponent`.
It has to be a prototype because if I just did something akin to
`Dictionary<MobState, Dictionary<string, bool>>`, you'd have to check
the parent and copy every flag besides the one you wish to modify. That
is, if I understand how the prototype system works correctly, which I
frankly doubt. <!-- Working on softcrit made me hate prototypes. -->

MobStateComponent now has:
- `Dictionary<string, string> InitMobStateParams`, for storing "mobstate
- parameter prototype" pairs. `<string, string>` because it has to be
editable via mob prototypes. Named "mobStateParams" for mob prototypes.
- `public Dictionary<MobState, MobStateParametersPrototype>
MobStateParams` for actually storing the params for each state
- `public Dictionary<MobState, MobStateParametersOverride>
MobStateParamsOverrides` for storing overrides.
`MobStateParametersOverride` is a struct which mirrors all
`MobStateParametersPrototype`'s fields, except they're all nullable.
This is meant for code which wants to temporarily override some setting,
like a spell which allows dead people to talk. This is not the best
solution, but it should do at first. A better option would be tracking
each change separately, instead of hoping different systems overriding
the same flag will play nicely with eachother.
- a shitton of getter methods

TraitModifyMobState now has:
- `public Dictionary<string, string> Params` to specify a new prototype
to use.
- Important note: All values of `MobStateParametersPrototype` are
nullable, which is a hack to support `TraitModifyMobState`. This trait
takes one `MobStateParametersPrototype` per mobstate and applies all of
its non-null values. This way, a params prototype can be created which
will only have `pointing: true` and the trait can apply it (e.g. to
critstate, so we can spam pointing while dying like it's a game of turbo
dota)
- The above is why that wall of getters exists: They check the relevant
override struct, then the relevant prototype. If both are null, they
default to false (0f for floats.) The only exception is
OxyDamageOverlay, because it's used both for oxy damage overlay (if
null) and as a vision-limiting black void in crit..

MobStateSystem now has:
- a bunch of new "IsSomething"/"CanDoSomething" methods to check the
various flags, alongside rewritten old ones.
-
![image](https://github.com/user-attachments/assets/33a6b296-c12c-4311-9abe-90ca4288e871)
lookin ahh predicate factory

</details>
---

# TODO

done:
- [x] Make proper use of `MobStateSystem.IsIncapacitated()`.
done: some checks were changed, some left as they did what was (more or
less) intended.
<details>Previous `IsIncapacitated()` implementation simply checked if
person was in crit or dead. Now there is a `IsIncapacitated` flag in the
parameters, but it's heavily underutilized. I may need some help on this
one, since I don't know where would be a good place to check for it and
I absolutely will not just scour the entire build in search for them.
</details>

- [x] Separate force-dropping items from being downed
done: dropItemsOnEntering bool field. If true, will drop items upon
entering linked mobstate.
- [x] Don't drop items if `ForceDown` is true but `PickingUp` is also
true.
done: dropItemsOnEntering bool field. If true, will drop items upon
entering linked mobstate.
- [x] Actually check what are "conscious attempts" are used for
done: whether or not mob is conscious. Renamed the bool field
accordingly.
- [x] Look into adding a way to make people choke "slowly" in softcrit
as opposed to choking at "regular speed" in crit. Make that into a param
option? Make that into a float so the speed can be finetuned?
done: `BreathingMultiplier` float field added.
<details>
1f is regular breathing, 0.25 is "quarter-breathing". Air taken is
multiplied by `BreathingMultiplier` and suffocation damage taken (that
is dealt by RespiratorSystem, not all oxy damage) is multiplied by
`1-BreathingMultiplier`.
</details>

- [x] make sure the serializer actually does its job
done: it doesn't. Removed.
- [x] Make an option to prohibit using radio headsets while in softcrit
done: Requires Incapacitated parameter to be false to be able to use
headset radio.
- [x] Make sure it at least compiles

not done:
- [ ] probably move some other stuff to Params if it makes sense. Same
thing as with `IsIncapacitated` though: I kinda don't want to, at least
for now.

---

<details><summary><h1>No media</h1></summary>
<p>

:p

</p>
</details>

---

# Changelog

🆑
- add: Soft critical state. Crawl to safety, or to your doom - whatever
is closer.

---------

Signed-off-by: RedFoxIV <38788538+RedFoxIV@users.noreply.github.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>

(cherry picked from commit 9a357c1774f1a783844a07b5414f504ca574d84c)
2025-02-15 00:12:50 +03:00

787 lines
33 KiB
C#

using System.Linq;
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Alert;
using Content.Shared.Buckle.Components;
using Content.Shared.Contests;
using Content.Shared.Cuffs.Components;
using Content.Shared.Database;
using Content.Shared.Flight;
using Content.Shared.DoAfter;
using Content.Shared.Hands;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Components;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory.Events;
using Content.Shared.Inventory.VirtualItem;
using Content.Shared.Item;
using Content.Shared.Movement.Events;
using Content.Shared.Movement.Pulling.Events;
using Content.Shared.Popups;
using Content.Shared.Pulling.Events;
using Content.Shared.Rejuvenate;
using Content.Shared.Stunnable;
using Content.Shared.Timing;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Melee.Events;
using Robust.Shared.Audio.Systems;
using Content.Shared.Mood;
using Robust.Shared.Containers;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using PullableComponent = Content.Shared.Movement.Pulling.Components.PullableComponent;
namespace Content.Shared.Cuffs
{
// TODO remove all the IsServer() checks.
public abstract partial class SharedCuffableSystem : EntitySystem
{
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedVirtualItemSystem _virtualItem = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly UseDelaySystem _delay = default!;
[Dependency] private readonly ContestsSystem _contests = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HandCountChangedEvent>(OnHandCountChanged);
SubscribeLocalEvent<UncuffAttemptEvent>(OnUncuffAttempt);
SubscribeLocalEvent<CuffableComponent, EntRemovedFromContainerMessage>(OnCuffsRemovedFromContainer);
SubscribeLocalEvent<CuffableComponent, EntInsertedIntoContainerMessage>(OnCuffsInsertedIntoContainer);
SubscribeLocalEvent<CuffableComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<CuffableComponent, ComponentInit>(OnStartup);
SubscribeLocalEvent<CuffableComponent, AttemptStopPullingEvent>(HandleStopPull);
SubscribeLocalEvent<CuffableComponent, UpdateCanMoveEvent>(HandleMoveAttempt);
SubscribeLocalEvent<CuffableComponent, IsEquippingAttemptEvent>(OnEquipAttempt);
SubscribeLocalEvent<CuffableComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
SubscribeLocalEvent<CuffableComponent, BeingPulledAttemptEvent>(OnBeingPulledAttempt);
SubscribeLocalEvent<CuffableComponent, BuckleAttemptEvent>(OnBuckleAttemptEvent);
SubscribeLocalEvent<CuffableComponent, UnbuckleAttemptEvent>(OnUnbuckleAttemptEvent);
SubscribeLocalEvent<CuffableComponent, GetVerbsEvent<Verb>>(AddUncuffVerb);
SubscribeLocalEvent<CuffableComponent, UnCuffDoAfterEvent>(OnCuffableDoAfter);
SubscribeLocalEvent<CuffableComponent, PullStartedMessage>(OnPull);
SubscribeLocalEvent<CuffableComponent, PullStoppedMessage>(OnPull);
SubscribeLocalEvent<CuffableComponent, DropAttemptEvent>(CheckAct);
SubscribeLocalEvent<CuffableComponent, PickupAttemptEvent>(CheckAct);
SubscribeLocalEvent<CuffableComponent, AttackAttemptEvent>(CheckAct);
SubscribeLocalEvent<CuffableComponent, UseAttemptEvent>(CheckAct);
SubscribeLocalEvent<CuffableComponent, InteractionAttemptEvent>(CheckInteract);
SubscribeLocalEvent<HandcuffComponent, AfterInteractEvent>(OnCuffAfterInteract);
SubscribeLocalEvent<HandcuffComponent, MeleeHitEvent>(OnCuffMeleeHit);
SubscribeLocalEvent<HandcuffComponent, AddCuffDoAfterEvent>(OnAddCuffDoAfter);
SubscribeLocalEvent<HandcuffComponent, VirtualItemDeletedEvent>(OnCuffVirtualItemDeleted);
}
private void CheckInteract(Entity<CuffableComponent> ent, ref InteractionAttemptEvent args)
{
if (!ent.Comp.CanStillInteract)
args.Cancel();
}
private void OnUncuffAttempt(ref UncuffAttemptEvent args)
{
if (args.Cancelled)
return;
if (!Exists(args.User) || Deleted(args.User))
{
// Should this even be possible?
args.Cancelled = true;
return;
}
// If the user is the target, special logic applies.
// This is because the CanInteract blocking of the cuffs prevents self-uncuff.
if (args.User == args.Target)
{
if (!TryComp<CuffableComponent>(args.User, out var cuffable))
{
DebugTools.Assert($"{args.User} tried to uncuff themselves but they are not cuffable.");
return;
}
// 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);
}
else
{
// Check if the user can interact.
if (!_actionBlocker.CanInteract(args.User, args.Target))
args.Cancelled = true;
}
if (args.Cancelled)
{
_popup.PopupClient(Loc.GetString("cuffable-component-cannot-interact-message"), args.Target, args.User);
}
}
private void OnStartup(EntityUid uid, CuffableComponent component, ComponentInit args)
{
component.Container = _container.EnsureContainer<Container>(uid, _componentFactory.GetComponentName(component.GetType()));
}
private void OnRejuvenate(EntityUid uid, CuffableComponent component, RejuvenateEvent args)
{
_container.EmptyContainer(component.Container, true);
}
private void OnCuffsRemovedFromContainer(EntityUid uid, CuffableComponent component, EntRemovedFromContainerMessage args)
{
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
if (args.Container.ID != component.Container?.ID)
return;
_virtualItem.DeleteInHandsMatching(uid, args.Entity);
UpdateCuffState(uid, component);
}
private void OnCuffsInsertedIntoContainer(EntityUid uid, CuffableComponent component, ContainerModifiedMessage args)
{
if (args.Container == component.Container)
UpdateCuffState(uid, component);
}
public void UpdateCuffState(EntityUid uid, CuffableComponent component)
{
var canInteract = TryComp(uid, out HandsComponent? hands) && hands.Hands.Count > component.CuffedHandCount;
if (canInteract == component.CanStillInteract)
return;
component.CanStillInteract = canInteract;
Dirty(uid, component);
_actionBlocker.UpdateCanMove(uid);
if (component.CanStillInteract)
{
_alerts.ClearAlert(uid, component.CuffedAlert);
RaiseLocalEvent(uid, new MoodRemoveEffectEvent("Handcuffed"));
}
else
{
_alerts.ShowAlert(uid, component.CuffedAlert);
RaiseLocalEvent(uid, new MoodEffectEvent("Handcuffed"));
}
var ev = new CuffedStateChangeEvent();
RaiseLocalEvent(uid, ref ev);
}
private void OnBeingPulledAttempt(EntityUid uid, CuffableComponent component, BeingPulledAttemptEvent args)
{
if (!TryComp<PullableComponent>(uid, out var pullable))
return;
if (pullable.Puller != null && !component.CanStillInteract) // If we are being pulled already and cuffed, we can't get pulled again.
args.Cancel();
}
private void OnBuckleAttempt(EntityUid uid, CuffableComponent comp, EntityUid? user, CancellableEntityEventArgs args, bool buckling, bool popup)
{
if (args.Cancelled || user != uid)
return;
if (!TryComp<HandsComponent>(uid, out var hands) || comp.CuffedHandCount < hands.Count)
return;
args.Cancel();
if (!popup)
return;
var message = buckling
? Loc.GetString("handcuff-component-cuff-interrupt-buckled-message")
: Loc.GetString("handcuff-component-cuff-interrupt-unbuckled-message");
_popup.PopupClient(message, uid, user);
}
private void OnBuckleAttemptEvent(EntityUid uid, CuffableComponent comp, BuckleAttemptEvent args)
{
OnBuckleAttempt(uid, comp, args.User, args, true, args.Popup);
}
private void OnUnbuckleAttemptEvent(EntityUid uid, CuffableComponent comp, UnbuckleAttemptEvent args)
{
OnBuckleAttempt(uid, comp, args.User, args, false, args.Popup);
}
private void OnPull(EntityUid uid, CuffableComponent component, PullMessage args)
{
if (!component.CanStillInteract)
_actionBlocker.UpdateCanMove(uid);
}
private void HandleMoveAttempt(EntityUid uid, CuffableComponent component, UpdateCanMoveEvent args)
{
if (component.CanStillInteract || !EntityManager.TryGetComponent(uid, out PullableComponent? pullable) || !pullable.BeingPulled)
return;
args.Cancel();
}
private void HandleStopPull(EntityUid uid, CuffableComponent component, AttemptStopPullingEvent args)
{
if (args.User == null || !Exists(args.User.Value))
return;
if (args.User.Value == uid && !component.CanStillInteract)
args.Cancelled = true;
}
private void AddUncuffVerb(EntityUid uid, CuffableComponent component, GetVerbsEvent<Verb> args)
{
// Can the user access the cuffs, and is there even anything to uncuff?
if (!args.CanAccess || component.CuffedHandCount == 0 || args.Hands == null)
return;
// We only check can interact if the user is not uncuffing themselves. As a result, the verb will show up
// when the user is incapacitated & trying to uncuff themselves, but TryUncuff() will still fail when
// attempted.
if (args.User != args.Target && !args.CanInteract)
return;
Verb verb = new()
{
Act = () => TryUncuff(uid, args.User, cuffable: component),
DoContactInteraction = true,
Text = Loc.GetString("uncuff-verb-get-data-text")
};
//TODO VERB ICON add uncuffing symbol? may re-use the alert symbol showing that you are currently cuffed?
args.Verbs.Add(verb);
}
private void OnCuffableDoAfter(EntityUid uid, CuffableComponent component, UnCuffDoAfterEvent args)
{
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);
}
else
{
_popup.PopupClient(Loc.GetString("cuffable-component-remove-cuffs-fail-message"), user, user);
}
}
private void OnCuffAfterInteract(EntityUid uid, HandcuffComponent component, AfterInteractEvent args)
{
if (args.Target is not { Valid: true } target)
return;
if (!args.CanReach)
{
_popup.PopupClient(Loc.GetString("handcuff-component-too-far-away-error"), args.User, args.User);
return;
}
var result = TryCuffing(args.User, target, uid, component);
args.Handled = result;
}
private void OnCuffMeleeHit(EntityUid uid, HandcuffComponent component, MeleeHitEvent args)
{
if (!args.HitEntities.Any())
return;
TryCuffing(args.User, args.HitEntities.First(), uid, component);
args.Handled = true;
}
private void OnAddCuffDoAfter(EntityUid uid, HandcuffComponent component, AddCuffDoAfterEvent args)
{
var user = args.Args.User;
if (!TryComp<CuffableComponent>(args.Args.Target, out var cuffable))
return;
var target = args.Args.Target.Value;
if (args.Handled)
return;
args.Handled = true;
if (!args.Cancelled && TryAddNewCuffs(target, user, uid, cuffable))
{
component.Used = true;
_audio.PlayPredicted(component.EndCuffSound, uid, user);
_popup.PopupEntity(Loc.GetString("handcuff-component-cuff-observer-success-message",
("user", Identity.Name(user, EntityManager)), ("target", Identity.Name(target, EntityManager))),
target, Filter.Pvs(target, entityManager: EntityManager)
.RemoveWhere(e => e.AttachedEntity == target || e.AttachedEntity == user), true);
if (target == user)
{
_popup.PopupClient(Loc.GetString("handcuff-component-cuff-self-success-message"), user, user);
_adminLog.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(user):player} has cuffed himself");
}
else
{
_popup.PopupClient(Loc.GetString("handcuff-component-cuff-other-success-message",
("otherName", Identity.Name(target, EntityManager, user))), user, user);
_popup.PopupClient(Loc.GetString("handcuff-component-cuff-by-other-success-message",
("otherName", Identity.Name(user, EntityManager, target))), target, target);
_adminLog.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(user):player} has cuffed {ToPrettyString(target):player}");
}
}
else
{
if (target == user)
{
_popup.PopupClient(Loc.GetString("handcuff-component-cuff-interrupt-self-message"), user, user);
}
else
{
// TODO Fix popup message wording
// This message assumes that the user being handcuffed is the one that caused the handcuff to fail.
_popup.PopupClient(Loc.GetString("handcuff-component-cuff-interrupt-message",
("targetName", Identity.Name(target, EntityManager, user))), user, user);
_popup.PopupClient(Loc.GetString("handcuff-component-cuff-interrupt-other-message",
("otherName", Identity.Name(user, EntityManager, target))), target, target);
}
}
}
private void OnCuffVirtualItemDeleted(EntityUid uid, HandcuffComponent component, VirtualItemDeletedEvent args)
{
Uncuff(args.User, null, uid, cuff: component);
}
/// <summary>
/// Check the current amount of hands the owner has, and if there's less hands than active cuffs we remove some cuffs.
/// </summary>
private void OnHandCountChanged(HandCountChangedEvent message)
{
var owner = message.Sender;
if (!TryComp(owner, out CuffableComponent? cuffable) ||
!cuffable.Initialized)
{
return;
}
var dirty = false;
var handCount = CompOrNull<HandsComponent>(owner)?.Count ?? 0;
while (cuffable.CuffedHandCount > handCount && cuffable.CuffedHandCount > 0)
{
dirty = true;
var container = cuffable.Container;
var entity = container.ContainedEntities[^1];
_container.Remove(entity, container);
_transform.SetWorldPosition(entity, _transform.GetWorldPosition(owner));
}
if (dirty)
{
UpdateCuffState(owner, cuffable);
}
}
/// <summary>
/// Adds virtual cuff items to the user's hands.
/// </summary>
private void UpdateHeldItems(EntityUid uid, EntityUid handcuff, CuffableComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
// TODO we probably don't just want to use the generic virtual-item entity, and instead
// want to add our own item, so that use-in-hand triggers an uncuff attempt and the like.
if (!TryComp<HandsComponent>(uid, out var handsComponent))
return;
var freeHands = 0;
foreach (var hand in _hands.EnumerateHands(uid, handsComponent))
{
if (hand.HeldEntity == null)
{
freeHands++;
continue;
}
// Is this entity removable? (it might be an existing handcuff blocker)
if (HasComp<UnremoveableComponent>(hand.HeldEntity))
continue;
_hands.DoDrop(uid, hand, true, handsComponent);
freeHands++;
if (freeHands == 2)
break;
}
if (_virtualItem.TrySpawnVirtualItemInHand(handcuff, uid, out var virtItem1))
EnsureComp<UnremoveableComponent>(virtItem1.Value);
if (_virtualItem.TrySpawnVirtualItemInHand(handcuff, uid, out var virtItem2))
EnsureComp<UnremoveableComponent>(virtItem2.Value);
}
/// <summary>
/// Add a set of cuffs to an existing CuffedComponent.
/// </summary>
public bool TryAddNewCuffs(EntityUid target, EntityUid user, EntityUid handcuff, CuffableComponent? component = null, HandcuffComponent? cuff = null)
{
if (!Resolve(target, ref component) || !Resolve(handcuff, ref cuff))
return false;
if (!_interaction.InRangeUnobstructed(handcuff, target))
return false;
// Success!
_hands.TryDrop(user, handcuff);
_container.Insert(handcuff, component.Container);
UpdateHeldItems(target, handcuff, component);
return true;
}
/// <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, float distanceThreshold = 0.3f)
{
if (!Resolve(handcuff, ref handcuffComponent) || !Resolve(target, ref cuffable, false))
return false;
if (!TryComp<HandsComponent>(target, out var hands))
{
_popup.PopupClient(Loc.GetString("handcuff-component-target-has-no-hands-error",
("targetName", Identity.Name(target, EntityManager, user))), user, user);
return true;
}
if (cuffable.CuffedHandCount >= hands.Count)
{
_popup.PopupClient(Loc.GetString("handcuff-component-target-has-no-free-hands-error",
("targetName", Identity.Name(target, EntityManager, user))), user, user);
return true;
}
if (TryComp<FlightComponent>(target, out var flight) && flight.On)
{
_popup.PopupClient(Loc.GetString("handcuff-component-target-flying-error",
("targetName", Identity.Name(target, EntityManager, user))), user, user);
return true;
}
var cuffTime = handcuffComponent.CuffTime;
if (HasComp<StunnedComponent>(target))
cuffTime = MathF.Max(0.1f, cuffTime - handcuffComponent.StunBonus);
if (HasComp<DisarmProneComponent>(target))
cuffTime = 0.0f; // cuff them instantly.
var doAfterEventArgs = new DoAfterArgs(EntityManager, user, cuffTime, new AddCuffDoAfterEvent(), handcuff, target, handcuff)
{
BreakOnMove = true,
BreakOnWeightlessMove = false,
BreakOnDamage = true,
NeedHand = true,
DistanceThreshold = distanceThreshold
};
if (!_doAfter.TryStartDoAfter(doAfterEventArgs))
return true;
_popup.PopupEntity(Loc.GetString("handcuff-component-start-cuffing-observer",
("user", Identity.Name(user, EntityManager)), ("target", Identity.Name(target, EntityManager))),
target, Filter.Pvs(target, entityManager: EntityManager)
.RemoveWhere(e => e.AttachedEntity == target || e.AttachedEntity == user), true);
if (target == user)
{
_popup.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);
_popup.PopupClient(Loc.GetString("handcuff-component-start-cuffing-by-other-message",
("otherName", Identity.Name(user, EntityManager, target))), target, target);
}
_audio.PlayPredicted(handcuffComponent.StartCuffSound, handcuff, user);
return true;
}
/// <summary>
/// Checks if the target is handcuffed.
/// </summary>
/// <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;
}
/// <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))
{
Log.Warning("A user is trying to remove handcuffs that aren't in the owner's container. This should never happen!");
}
}
if (!Resolve(cuffsToRemove.Value, ref cuff))
return;
var attempt = new UncuffAttemptEvent(user, target);
RaiseLocalEvent(user, ref attempt, true);
if (attempt.Cancelled)
{
return;
}
if (!isOwner && !_interaction.InRangeUnobstructed(user, target))
{
_popup.PopupClient(Loc.GetString("cuffable-component-cannot-remove-cuffs-too-far-message"), user, user);
return;
}
var uncuffTime = (isOwner ? cuff.BreakoutTime : cuff.UncuffTime) * (cuff.UncuffEasierWhenLarge ? 1 / _contests.MassContest(user) : _contests.MassContest(user));
if (isOwner)
{
if (!TryComp(cuffsToRemove.Value, out UseDelayComponent? useDelay))
return;
if (!_delay.TryResetDelay((cuffsToRemove.Value, useDelay), true))
{
return;
}
}
var doAfterEventArgs = new DoAfterArgs(EntityManager, user, uncuffTime, new UnCuffDoAfterEvent(), target, target, cuffsToRemove)
{
BreakOnMove = true,
BreakOnWeightlessMove = false,
BreakOnDamage = true,
NeedHand = true,
RequireCanInteract = false, // Trust in UncuffAttemptEvent
DistanceThreshold = 0.3f
};
if (!_doAfter.TryStartDoAfter(doAfterEventArgs))
return;
_adminLog.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user)} is trying to uncuff {ToPrettyString(target)}");
_popup.PopupEntity(Loc.GetString("cuffable-component-start-uncuffing-observer",
("user", Identity.Name(user, EntityManager)), ("target", Identity.Name(target, EntityManager))),
target, Filter.Pvs(target, entityManager: EntityManager)
.RemoveWhere(e => e.AttachedEntity == target || e.AttachedEntity == user), true);
if (target == user)
{
_popup.PopupClient(Loc.GetString("cuffable-component-start-uncuffing-self"), user, user);
}
else
{
_popup.PopupClient(Loc.GetString("cuffable-component-start-uncuffing-target-message",
("targetName", Identity.Name(target, EntityManager, user))), user, user);
_popup.PopupClient(Loc.GetString("cuffable-component-start-uncuffing-by-other-message",
("otherName", Identity.Name(user, EntityManager, target))), target, target);
}
_audio.PlayPredicted(isOwner ? cuff.StartBreakoutSound : cuff.StartUncuffSound, target, user);
}
public void Uncuff(EntityUid target, EntityUid? user, EntityUid cuffsToRemove, CuffableComponent? cuffable = null, HandcuffComponent? cuff = null)
{
if (!Resolve(target, ref cuffable) || !Resolve(cuffsToRemove, ref cuff))
return;
if (!cuff.Used || cuff.Removing || TerminatingOrDeleted(cuffsToRemove) || TerminatingOrDeleted(target))
return;
if (user != null)
{
var attempt = new UncuffAttemptEvent(user.Value, target);
RaiseLocalEvent(user.Value, ref attempt);
if (attempt.Cancelled)
return;
}
cuff.Removing = true;
cuff.Used = false;
_audio.PlayPredicted(cuff.EndUncuffSound, target, user);
_container.Remove(cuffsToRemove, cuffable.Container);
if (_net.IsServer)
{
// Handles spawning broken cuffs on server to avoid client misprediction
if (cuff.BreakOnRemove)
{
QueueDel(cuffsToRemove);
if (cuff.BrokenPrototype.HasValue)
{
var trash = Spawn(cuff.BrokenPrototype, Transform(cuffsToRemove).Coordinates);
_hands.PickupOrDrop(user, trash);
}
}
else
{
_hands.PickupOrDrop(user, cuffsToRemove);
}
}
if (cuffable.CuffedHandCount == 0)
{
if (user != null)
_popup.PopupPredicted(Loc.GetString("cuffable-component-remove-cuffs-success-message"), user.Value, user.Value);
if (target != user && user != null)
{
_popup.PopupPredicted(Loc.GetString("cuffable-component-remove-cuffs-by-other-success-message",
("otherName", Identity.Name(user.Value, EntityManager, user))), target, target);
_adminLog.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(user):player} has successfully uncuffed {ToPrettyString(target):player}");
}
else
{
_adminLog.Add(LogType.Action, LogImpact.Medium,
$"{ToPrettyString(user):player} has successfully uncuffed themselves");
}
}
else if (user != null)
{
if (user != target)
{
_popup.PopupPredicted(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message",
("cuffedHandCount", cuffable.CuffedHandCount),
("otherName", Identity.Name(user.Value, EntityManager, user.Value))), user.Value, user.Value);
_popup.PopupPredicted(Loc.GetString(
"cuffable-component-remove-cuffs-by-other-partial-success-message",
("otherName", Identity.Name(user.Value, EntityManager, user.Value)),
("cuffedHandCount", cuffable.CuffedHandCount)), target, target);
}
else
{
_popup.PopupPredicted(Loc.GetString("cuffable-component-remove-cuffs-partial-success-message",
("cuffedHandCount", cuffable.CuffedHandCount)), user.Value, user.Value);
}
}
cuff.Removing = false;
}
#region ActionBlocker
private void CheckAct(EntityUid uid, CuffableComponent component, CancellableEntityEventArgs args)
{
if (!component.CanStillInteract)
args.Cancel();
}
private void OnEquipAttempt(EntityUid uid, CuffableComponent component, IsEquippingAttemptEvent args)
{
// is this a self-equip, or are they being stripped?
if (args.Equipee == uid)
CheckAct(uid, component, args);
}
private void OnUnequipAttempt(EntityUid uid, CuffableComponent component, IsUnequippingAttemptEvent args)
{
// is this a self-equip, or are they being stripped?
if (args.Unequipee == uid)
CheckAct(uid, component, args);
}
#endregion
public IReadOnlyList<EntityUid> GetAllCuffs(CuffableComponent component)
{
return component.Container.ContainedEntities;
}
[Serializable, NetSerializable]
private sealed partial class UnCuffDoAfterEvent : SimpleDoAfterEvent
{
}
[Serializable, NetSerializable]
private sealed partial class AddCuffDoAfterEvent : SimpleDoAfterEvent
{
}
}
}