Files
wwdpublic/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs
SimpleStation14 04c45bb9da Mirror: Prevent dead players from turning bar stools (#213)
## Mirror of PR #24308: [Prevent dead players from turning bar
stools](https://github.com/space-wizards/space-station-14/pull/24308)
from <img src="https://avatars.githubusercontent.com/u/10567778?v=4"
alt="space-wizards" width="22"/>
[space-wizards](https://github.com/space-wizards)/[space-station-14](https://github.com/space-wizards/space-station-14)

###### `15a7520df17a6fa95cbd8ce8914edab5b0d7ed50`

PR opened by <img
src="https://avatars.githubusercontent.com/u/4543739?v=4" width="16"/><a
href="https://github.com/Nopey"> Nopey</a> at 2024-01-19 22:47:45 UTC

---

PR changed 3 files with 11 additions and 18 deletions.

The PR had the following labels:


---

<details open="true"><summary><h1>Original Body</h1></summary>

> <!-- Please read these guidelines before opening your PR:
https://docs.spacestation14.io/en/getting-started/pr-guideline -->
> <!-- The text between the arrows are comments - they will not be
visible on your PR. -->
> 
> ## About the PR
> Bugfix.
> 
> ## Why / Balance
> Previously, players could always turn a bar stool (or office chair,
etc..) they were buckled into; even while stone cold dead.
> 
> ## Technical details
> RotateToFaceSystem now only lets players rotate barstools (& similar)
when the actionBlockerSystem reports that they CanChangeDirection; this
also means players who are frozen by admins (see AdminFrozenSystem) can
no longer spin barstools.
> 
> SharedBuckleSystem no longer tries to prevent buckled players from
being able to change direction, instead relying on RotateToFaceSystem to
ensure that players on beds and such don't rotate.
> 
> ## Media
> <!-- 
> PRs which make ingame changes (adding clothing, items, new features,
etc) are required to have media attached that showcase the changes.
> Small fixes/refactors are exempt.
> Any media may be used in SS14 progress reports, with clear credit
given.
> 
> If you're unsure whether your PR will require media, ask a maintainer.
> 
> Check the box below to confirm that you have in fact seen this (put an
X in the brackets, like [X]):
> -->
> 
> - [X] I have added screenshots/videos to this PR showcasing its
changes ingame
> 
> Video showcasing prior broken behavior: 
> 
>
https://github.com/space-wizards/space-station-14/assets/4543739/74be203a-0961-4850-842a-768d927f6691
> 
> 
> 
> 
> ## Breaking changes
> <!--
> List any breaking changes, including namespace, public
class/method/field changes, prototype renames; and provide instructions
for fixing them. This will be pasted in #codebase-changes.
> -->
> 
> **Changelog**
> 🆑
> - fix: Dead players can no longer spin on a bar stool.
> <!--
> Make players aware of new features and changes that could affect how
they play the game by adding a Changelog entry. Please read the
Changelog guidelines located at:
https://docs.spacestation14.io/en/getting-started/pr-guideline#changelog
> -->
> 
> <!--
> Make sure to take this Changelog template out of the comment block in
order for it to show up.
> 🆑
> - add: Added fun!
> - remove: Removed fun!
> - tweak: Changed fun!
> - fix: Fixed fun!
> -->
> 


</details>

Co-authored-by: SimpleStation14 <Unknown>
2024-05-09 01:19:09 -04:00

526 lines
20 KiB
C#

using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Content.Shared.Alert;
using Content.Shared.Bed.Sleep;
using Content.Shared.Buckle.Components;
using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Events;
using Content.Shared.Popups;
using Content.Shared.Pulling.Components;
using Content.Shared.Standing;
using Content.Shared.Storage.Components;
using Content.Shared.Stunnable;
using Content.Shared.Throwing;
using Content.Shared.Verbs;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Utility;
namespace Content.Shared.Buckle;
public abstract partial class SharedBuckleSystem
{
private void InitializeBuckle()
{
SubscribeLocalEvent<BuckleComponent, ComponentStartup>(OnBuckleComponentStartup);
SubscribeLocalEvent<BuckleComponent, ComponentShutdown>(OnBuckleComponentShutdown);
SubscribeLocalEvent<BuckleComponent, MoveEvent>(OnBuckleMove);
SubscribeLocalEvent<BuckleComponent, InteractHandEvent>(OnBuckleInteractHand);
SubscribeLocalEvent<BuckleComponent, GetVerbsEvent<InteractionVerb>>(AddUnbuckleVerb);
SubscribeLocalEvent<BuckleComponent, InsertIntoEntityStorageAttemptEvent>(OnBuckleInsertIntoEntityStorageAttempt);
SubscribeLocalEvent<BuckleComponent, PreventCollideEvent>(OnBucklePreventCollide);
SubscribeLocalEvent<BuckleComponent, DownAttemptEvent>(OnBuckleDownAttempt);
SubscribeLocalEvent<BuckleComponent, StandAttemptEvent>(OnBuckleStandAttempt);
SubscribeLocalEvent<BuckleComponent, ThrowPushbackAttemptEvent>(OnBuckleThrowPushbackAttempt);
SubscribeLocalEvent<BuckleComponent, UpdateCanMoveEvent>(OnBuckleUpdateCanMove);
}
private void OnBuckleComponentStartup(EntityUid uid, BuckleComponent component, ComponentStartup args)
{
UpdateBuckleStatus(uid, component);
}
private void OnBuckleComponentShutdown(EntityUid uid, BuckleComponent component, ComponentShutdown args)
{
TryUnbuckle(uid, uid, true, component);
component.BuckleTime = default;
}
private void OnBuckleMove(EntityUid uid, BuckleComponent component, ref MoveEvent ev)
{
if (component.BuckledTo is not {} strapUid)
return;
if (!TryComp<StrapComponent>(strapUid, out var strapComp))
return;
var strapPosition = Transform(strapUid).Coordinates;
if (ev.NewPosition.InRange(EntityManager, _transform, strapPosition, strapComp.MaxBuckleDistance))
return;
TryUnbuckle(uid, uid, true, component);
}
private void OnBuckleInteractHand(EntityUid uid, BuckleComponent component, InteractHandEvent args)
{
if (!component.Buckled)
return;
if (TryUnbuckle(uid, args.User, buckleComp: component))
args.Handled = true;
}
private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent<InteractionVerb> args)
{
if (!args.CanAccess || !args.CanInteract || !component.Buckled)
return;
InteractionVerb verb = new()
{
Act = () => TryUnbuckle(uid, args.User, buckleComp: component),
Text = Loc.GetString("verb-categories-unbuckle"),
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/unbuckle.svg.192dpi.png"))
};
if (args.Target == args.User && args.Using == null)
{
// A user is left clicking themselves with an empty hand, while buckled.
// It is very likely they are trying to unbuckle themselves.
verb.Priority = 1;
}
args.Verbs.Add(verb);
}
private void OnBuckleInsertIntoEntityStorageAttempt(EntityUid uid, BuckleComponent component, ref InsertIntoEntityStorageAttemptEvent args)
{
if (component.Buckled)
args.Cancelled = true;
}
private void OnBucklePreventCollide(EntityUid uid, BuckleComponent component, ref PreventCollideEvent args)
{
if (args.OtherEntity != component.BuckledTo)
return;
if (component.Buckled || component.DontCollide)
args.Cancelled = true;
}
private void OnBuckleDownAttempt(EntityUid uid, BuckleComponent component, DownAttemptEvent args)
{
if (component.Buckled)
args.Cancel();
}
private void OnBuckleStandAttempt(EntityUid uid, BuckleComponent component, StandAttemptEvent args)
{
if (component.Buckled)
args.Cancel();
}
private void OnBuckleThrowPushbackAttempt(EntityUid uid, BuckleComponent component, ThrowPushbackAttemptEvent args)
{
if (component.Buckled)
args.Cancel();
}
private void OnBuckleUpdateCanMove(EntityUid uid, BuckleComponent component, UpdateCanMoveEvent args)
{
if (component.LifeStage > ComponentLifeStage.Running)
return;
if (component.Buckled) // buckle shitcode
args.Cancel();
}
public bool IsBuckled(EntityUid uid, BuckleComponent? component = null)
{
return Resolve(uid, ref component, false) && component.Buckled;
}
/// <summary>
/// Shows or hides the buckled status effect depending on if the
/// entity is buckled or not.
/// </summary>
/// <param name="uid"> Entity that we want to show the alert </param>
/// <param name="buckleComp"> buckle component of the entity </param>
/// <param name="strapComp"> strap component of the thing we are strapping to </param>
private void UpdateBuckleStatus(EntityUid uid, BuckleComponent buckleComp, StrapComponent? strapComp = null)
{
Appearance.SetData(uid, StrapVisuals.State, buckleComp.Buckled);
if (buckleComp.BuckledTo != null)
{
if (!Resolve(buckleComp.BuckledTo.Value, ref strapComp))
return;
var alertType = strapComp.BuckledAlertType;
_alerts.ShowAlert(uid, alertType);
}
else
{
_alerts.ClearAlertCategory(uid, AlertCategory.Buckled);
}
}
/// <summary>
/// Sets the <see cref="BuckleComponent.BuckledTo"/> field in the component to a value
/// </summary>
/// <param name="strapUid"> Value tat with be assigned to the field </param>
private void SetBuckledTo(EntityUid buckleUid, EntityUid? strapUid, StrapComponent? strapComp, BuckleComponent buckleComp)
{
buckleComp.BuckledTo = strapUid;
if (strapUid == null)
{
buckleComp.Buckled = false;
}
else
{
buckleComp.LastEntityBuckledTo = strapUid;
buckleComp.DontCollide = true;
buckleComp.Buckled = true;
buckleComp.BuckleTime = _gameTiming.CurTime;
}
ActionBlocker.UpdateCanMove(buckleUid);
UpdateBuckleStatus(buckleUid, buckleComp, strapComp);
Dirty(buckleComp);
}
/// <summary>
/// Checks whether or not buckling is possible
/// </summary>
/// <param name="buckleUid"> Uid of the owner of BuckleComponent </param>
/// <param name="userUid">
/// Uid of a third party entity,
/// i.e, the uid of someone else you are dragging to a chair.
/// Can equal buckleUid sometimes
/// </param>
/// <param name="strapUid"> Uid of the owner of strap component </param>
private bool CanBuckle(
EntityUid buckleUid,
EntityUid userUid,
EntityUid strapUid,
[NotNullWhen(true)] out StrapComponent? strapComp,
BuckleComponent? buckleComp = null)
{
strapComp = null;
if (userUid == strapUid ||
!Resolve(buckleUid, ref buckleComp, false) ||
!Resolve(strapUid, ref strapComp, false))
{
return false;
}
// Does it pass the Whitelist
if (strapComp.AllowedEntities != null &&
!strapComp.AllowedEntities.IsValid(userUid, EntityManager))
{
if (_netManager.IsServer)
_popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), userUid, buckleUid, PopupType.Medium);
return false;
}
// Is it within range
bool Ignored(EntityUid entity) => entity == buckleUid || entity == userUid || entity == strapUid;
if (!_interaction.InRangeUnobstructed(buckleUid, strapUid, buckleComp.Range, predicate: Ignored,
popup: true))
{
return false;
}
// If in a container
if (_container.TryGetContainingContainer(buckleUid, out var ownerContainer))
{
// And not in the same container as the strap
if (!_container.TryGetContainingContainer(strapUid, out var strapContainer) ||
ownerContainer != strapContainer)
{
return false;
}
}
if (!HasComp<HandsComponent>(userUid))
{
// PopupPredicted when
if (_netManager.IsServer)
_popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), userUid, userUid);
return false;
}
if (buckleComp.Buckled)
{
var message = Loc.GetString(buckleUid == userUid
? "buckle-component-already-buckled-message"
: "buckle-component-other-already-buckled-message",
("owner", Identity.Entity(buckleUid, EntityManager)));
if (_netManager.IsServer)
_popup.PopupEntity(message, userUid, userUid);
return false;
}
var parent = Transform(strapUid).ParentUid;
while (parent.IsValid())
{
if (parent == userUid)
{
var message = Loc.GetString(buckleUid == userUid
? "buckle-component-cannot-buckle-message"
: "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
if (_netManager.IsServer)
_popup.PopupEntity(message, userUid, userUid);
return false;
}
parent = Transform(parent).ParentUid;
}
if (!StrapHasSpace(strapUid, buckleComp, strapComp))
{
var message = Loc.GetString(buckleUid == userUid
? "buckle-component-cannot-fit-message"
: "buckle-component-other-cannot-fit-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
if (_netManager.IsServer)
_popup.PopupEntity(message, userUid, userUid);
return false;
}
var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, true);
RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent);
RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent);
if (attemptEvent.Cancelled)
return false;
return true;
}
/// <summary>
/// Attempts to buckle an entity to a strap
/// </summary>
/// <param name="buckleUid"> Uid of the owner of BuckleComponent </param>
/// <param name="userUid">
/// Uid of a third party entity,
/// i.e, the uid of someone else you are dragging to a chair.
/// Can equal buckleUid sometimes
/// </param>
/// <param name="strapUid"> Uid of the owner of strap component </param>
public bool TryBuckle(EntityUid buckleUid, EntityUid userUid, EntityUid strapUid, BuckleComponent? buckleComp = null)
{
if (!Resolve(buckleUid, ref buckleComp, false))
return false;
if (!CanBuckle(buckleUid, userUid, strapUid, out var strapComp, buckleComp))
return false;
if (!StrapTryAdd(strapUid, buckleUid, buckleComp, false, strapComp))
{
var message = Loc.GetString(buckleUid == userUid
? "buckle-component-cannot-buckle-message"
: "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
if (_netManager.IsServer)
_popup.PopupEntity(message, userUid, userUid);
return false;
}
if (TryComp<AppearanceComponent>(buckleUid, out var appearance))
Appearance.SetData(buckleUid, BuckleVisuals.Buckled, true, appearance);
_rotationVisuals.SetHorizontalAngle(buckleUid, strapComp.Rotation);
ReAttach(buckleUid, strapUid, buckleComp, strapComp);
SetBuckledTo(buckleUid, strapUid, strapComp, buckleComp);
// TODO user is currently set to null because if it isn't the sound fails to play in some situations, fix that
_audio.PlayPredicted(strapComp.BuckleSound, strapUid, userUid);
var ev = new BuckleChangeEvent(strapUid, buckleUid, true);
RaiseLocalEvent(ev.BuckledEntity, ref ev);
RaiseLocalEvent(ev.StrapEntity, ref ev);
if (TryComp<SharedPullableComponent>(buckleUid, out var ownerPullable))
{
if (ownerPullable.Puller != null)
{
_pulling.TryStopPull(ownerPullable);
}
}
if (TryComp<PhysicsComponent>(buckleUid, out var physics))
{
_physics.ResetDynamics(physics);
}
if (!buckleComp.PullStrap && TryComp<SharedPullableComponent>(strapUid, out var toPullable))
{
if (toPullable.Puller == buckleUid)
{
// can't pull it and buckle to it at the same time
_pulling.TryStopPull(toPullable);
}
}
// Logging
if (userUid != buckleUid)
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} buckled {ToPrettyString(buckleUid)} to {ToPrettyString(strapUid)}");
else
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} buckled themselves to {ToPrettyString(strapUid)}");
return true;
}
/// <summary>
/// Tries to unbuckle the Owner of this component from its current strap.
/// </summary>
/// <param name="buckleUid">The entity to unbuckle.</param>
/// <param name="userUid">The entity doing the unbuckling.</param>
/// <param name="force">
/// Whether to force the unbuckling or not. Does not guarantee true to
/// be returned, but guarantees the owner to be unbuckled afterwards.
/// </param>
/// <param name="buckleComp">The buckle component of the entity to unbuckle.</param>
/// <returns>
/// true if the owner was unbuckled, otherwise false even if the owner
/// was previously already unbuckled.
/// </returns>
public bool TryUnbuckle(EntityUid buckleUid, EntityUid userUid, bool force = false, BuckleComponent? buckleComp = null)
{
if (!Resolve(buckleUid, ref buckleComp, false) ||
buckleComp.BuckledTo is not { } strapUid)
return false;
if (!force)
{
var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, false);
RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent);
RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent);
if (attemptEvent.Cancelled)
return false;
if (_gameTiming.CurTime < buckleComp.BuckleTime + buckleComp.Delay)
return false;
if (!_interaction.InRangeUnobstructed(userUid, strapUid, buckleComp.Range, popup: true))
return false;
if (HasComp<SleepingComponent>(buckleUid) && buckleUid == userUid)
return false;
// If the person is crit or dead in any kind of strap, return. This prevents people from unbuckling themselves while incapacitated.
if (_mobState.IsIncapacitated(buckleUid) && userUid == buckleUid)
return false;
}
// Logging
if (userUid != buckleUid)
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} unbuckled {ToPrettyString(buckleUid)} from {ToPrettyString(strapUid)}");
else
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} unbuckled themselves from {ToPrettyString(strapUid)}");
SetBuckledTo(buckleUid, null, null, buckleComp);
if (!TryComp<StrapComponent>(strapUid, out var strapComp))
return false;
var buckleXform = Transform(buckleUid);
var oldBuckledXform = Transform(strapUid);
if (buckleXform.ParentUid == strapUid && !Terminating(buckleXform.ParentUid))
{
_container.AttachParentToContainerOrGrid((buckleUid, buckleXform));
var oldBuckledToWorldRot = _transform.GetWorldRotation(strapUid);
_transform.SetWorldRotation(buckleXform, oldBuckledToWorldRot);
if (strapComp.UnbuckleOffset != Vector2.Zero)
buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strapComp.UnbuckleOffset);
}
if (TryComp(buckleUid, out AppearanceComponent? appearance))
Appearance.SetData(buckleUid, BuckleVisuals.Buckled, false, appearance);
_rotationVisuals.ResetHorizontalAngle(buckleUid);
if (TryComp<MobStateComponent>(buckleUid, out var mobState)
&& _mobState.IsIncapacitated(buckleUid, mobState)
|| HasComp<KnockedDownComponent>(buckleUid))
{
_standing.Down(buckleUid);
}
else
{
_standing.Stand(buckleUid);
}
if (_mobState.IsIncapacitated(buckleUid, mobState))
{
_standing.Down(buckleUid);
}
if (strapComp.BuckledEntities.Remove(buckleUid))
{
strapComp.OccupiedSize -= buckleComp.Size;
//Dirty(strapUid);
Dirty(strapComp);
}
_joints.RefreshRelay(buckleUid);
Appearance.SetData(strapUid, StrapVisuals.State, strapComp.BuckledEntities.Count != 0);
// TODO: Buckle listening to moveevents is sussy anyway.
if (!TerminatingOrDeleted(strapUid))
_audio.PlayPredicted(strapComp.UnbuckleSound, strapUid, userUid);
var ev = new BuckleChangeEvent(strapUid, buckleUid, false);
RaiseLocalEvent(buckleUid, ref ev);
RaiseLocalEvent(strapUid, ref ev);
return true;
}
/// <summary>
/// Makes an entity toggle the buckling status of the owner to a
/// specific entity.
/// </summary>
/// <param name="buckleUid">The entity to buckle/unbuckle from <see cref="to"/>.</param>
/// <param name="userUid">The entity doing the buckling/unbuckling.</param>
/// <param name="strapUid">
/// The entity to toggle the buckle status of the owner to.
/// </param>
/// <param name="force">
/// Whether to force the unbuckling or not, if it happens. Does not
/// guarantee true to be returned, but guarantees the owner to be
/// unbuckled afterwards.
/// </param>
/// <param name="buckle">The buckle component of the entity to buckle/unbuckle from <see cref="to"/>.</param>
/// <returns>true if the buckling status was changed, false otherwise.</returns>
public bool ToggleBuckle(
EntityUid buckleUid,
EntityUid userUid,
EntityUid strapUid,
bool force = false,
BuckleComponent? buckle = null)
{
if (!Resolve(buckleUid, ref buckle, false))
return false;
if (!buckle.Buckled)
{
return TryBuckle(buckleUid, userUid, strapUid, buckle);
}
else
{
return TryUnbuckle(buckleUid, userUid, force, buckle);
}
}
}