Files
wwdpublic/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
VMSolidus 125896184e Cherry-Pick PR #26312: Fix Puller Being Improperly Unset When Pulling Stops. (#532)
## Mirror of PR #26312: [Fix puller being improperly unset when pulling
stops.](https://github.com/space-wizards/space-station-14/pull/26312)
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)

###### `a6c018d755ee4f35fb8e3ab597fa90c7592fe920`

PR opened by <img
src="https://avatars.githubusercontent.com/u/32041239?v=4"
width="16"/><a href="https://github.com/nikthechampiongr">
nikthechampiongr</a> at 2024-03-21 15:07:50 UTC

---

PR changed 1 files with 8 additions and 6 deletions.

The PR had the following labels:


---

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

> fixes #26310
> 
> <!-- 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
> <!-- What did you change in this PR? -->
> This pr fixes the bug with people being unable to move while in cuffs
if they were previously pulled even if they were no longer being pulled.
> 
> ## Why / Balance
> <!-- Why was it changed? Link any discussions or issues here. Please
discuss how this would affect game balance. -->
> A security cadet should not be able to merge my legs with the ground
by simply touching me while I'm in cuffs.
> 
> ## Technical details
> <!-- If this is a code change, summarize at high level how your new
code works. This makes it easier to review. -->
> 
> When unpulled, the pullableComp has its puller field set to null after
the message signifying the pulling has stopped has been sent. Since the
component has a field to determine whether its owner is being pulled
which is determined by the puller field, systems listening on the event
would think that the owner of the component was still being pulled.
> 
> As a result, when the message was fired and the SharedCuffableSystem
checked whether it should allow the cuffed person to move it thought
they were still being pulled and as such didn't allow them to move.
> 
> I spent some time trying to determine whether setting the puller to
null earlier would have other negative effects, however to my
understanding of the code and the systems that listen to the message I
believe this is now the correct behavior. Still just in case
@metalgearsloth please check this out thanks.
> ## 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, **or** this PR does not require an ingame showcase
> 
> 
> **Changelog**
> <!--
> 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!
> -->
> 🆑 
> - fix: You can now move again if you stop being pulled while in cuffs.


</details>

Co-authored-by: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com>
2024-07-09 17:43:45 -07:00

495 lines
17 KiB
C#

using System.Numerics;
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
using Content.Shared.Alert;
using Content.Shared.Buckle.Components;
using Content.Shared.Database;
using Content.Shared.Hands;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Input;
using Content.Shared.Interaction;
using Content.Shared.Movement.Events;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Pulling.Events;
using Content.Shared.Movement.Systems;
using Content.Shared.Pulling.Events;
using Content.Shared.Throwing;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
using Robust.Shared.Input.Binding;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Shared.Movement.Pulling.Systems;
/// <summary>
/// Allows one entity to pull another behind them via a physics distance joint.
/// </summary>
public sealed class PullingSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly MovementSpeedModifierSystem _modifierSystem = default!;
[Dependency] private readonly SharedJointSystem _joints = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedTransformSystem _xformSys = default!;
[Dependency] private readonly ThrowingSystem _throwing = default!;
public override void Initialize()
{
base.Initialize();
UpdatesAfter.Add(typeof(SharedPhysicsSystem));
UpdatesOutsidePrediction = true;
SubscribeLocalEvent<PullableComponent, MoveInputEvent>(OnPullableMoveInput);
SubscribeLocalEvent<PullableComponent, CollisionChangeEvent>(OnPullableCollisionChange);
SubscribeLocalEvent<PullableComponent, JointRemovedEvent>(OnJointRemoved);
SubscribeLocalEvent<PullableComponent, GetVerbsEvent<Verb>>(AddPullVerbs);
SubscribeLocalEvent<PullableComponent, EntGotInsertedIntoContainerMessage>(OnPullableContainerInsert);
SubscribeLocalEvent<PullerComponent, EntGotInsertedIntoContainerMessage>(OnPullerContainerInsert);
SubscribeLocalEvent<PullerComponent, EntityUnpausedEvent>(OnPullerUnpaused);
SubscribeLocalEvent<PullerComponent, VirtualItemDeletedEvent>(OnVirtualItemDeleted);
SubscribeLocalEvent<PullerComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
CommandBinds.Builder
.Bind(ContentKeyFunctions.MovePulledObject, new PointerInputCmdHandler(OnRequestMovePulledObject))
.Bind(ContentKeyFunctions.ReleasePulledObject, InputCmdHandler.FromDelegate(OnReleasePulledObject, handle: false))
.Register<PullingSystem>();
}
private void OnPullerContainerInsert(Entity<PullerComponent> ent, ref EntGotInsertedIntoContainerMessage args)
{
if (ent.Comp.Pulling == null) return;
if (!TryComp(ent.Comp.Pulling.Value, out PullableComponent? pulling))
return;
TryStopPull(ent.Comp.Pulling.Value, pulling, ent.Owner);
}
private void OnPullableContainerInsert(Entity<PullableComponent> ent, ref EntGotInsertedIntoContainerMessage args)
{
TryStopPull(ent.Owner, ent.Comp);
}
public override void Shutdown()
{
base.Shutdown();
CommandBinds.Unregister<PullingSystem>();
}
private void OnPullerUnpaused(EntityUid uid, PullerComponent component, ref EntityUnpausedEvent args)
{
component.NextThrow += args.PausedTime;
}
private void OnVirtualItemDeleted(EntityUid uid, PullerComponent component, VirtualItemDeletedEvent args)
{
// If client deletes the virtual hand then stop the pull.
if (component.Pulling == null)
return;
if (component.Pulling != args.BlockingEntity)
return;
if (EntityManager.TryGetComponent(args.BlockingEntity, out PullableComponent? comp))
{
TryStopPull(args.BlockingEntity, comp, uid);
}
}
private void AddPullVerbs(EntityUid uid, PullableComponent component, GetVerbsEvent<Verb> args)
{
if (!args.CanAccess || !args.CanInteract)
return;
// Are they trying to pull themselves up by their bootstraps?
if (args.User == args.Target)
return;
//TODO VERB ICONS add pulling icon
if (component.Puller == args.User)
{
Verb verb = new()
{
Text = Loc.GetString("pulling-verb-get-data-text-stop-pulling"),
Act = () => TryStopPull(uid, component, user: args.User),
DoContactInteraction = false // pulling handle its own contact interaction.
};
args.Verbs.Add(verb);
}
else if (CanPull(args.User, args.Target))
{
Verb verb = new()
{
Text = Loc.GetString("pulling-verb-get-data-text"),
Act = () => TryStartPull(args.User, args.Target),
DoContactInteraction = false // pulling handle its own contact interaction.
};
args.Verbs.Add(verb);
}
}
private void OnRefreshMovespeed(EntityUid uid, PullerComponent component, RefreshMovementSpeedModifiersEvent args)
{
args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier);
}
private void OnPullableMoveInput(EntityUid uid, PullableComponent component, ref MoveInputEvent args)
{
// If someone moves then break their pulling.
if (!component.BeingPulled)
return;
var entity = args.Entity;
if (!_blocker.CanMove(entity))
return;
TryStopPull(uid, component, user: uid);
}
private void OnPullableCollisionChange(EntityUid uid, PullableComponent component, ref CollisionChangeEvent args)
{
// IDK what this is supposed to be.
if (!_timing.ApplyingState && component.PullJointId != null && !args.CanCollide)
{
_joints.RemoveJoint(uid, component.PullJointId);
}
}
private void OnJointRemoved(EntityUid uid, PullableComponent component, JointRemovedEvent args)
{
// Just handles the joint getting nuked without going through pulling system (valid behavior).
// Not relevant / pullable state handle it.
if (component.Puller != args.OtherEntity ||
args.Joint.ID != component.PullJointId ||
_timing.ApplyingState)
{
return;
}
if (args.Joint.ID != component.PullJointId || component.Puller == null)
return;
StopPulling(uid, component);
}
/// <summary>
/// Forces pulling to stop and handles cleanup.
/// </summary>
private void StopPulling(EntityUid pullableUid, PullableComponent pullableComp)
{
if (!_timing.ApplyingState)
{
if (TryComp<PhysicsComponent>(pullableUid, out var pullablePhysics))
{
_physics.SetFixedRotation(pullableUid, pullableComp.PrevFixedRotation, body: pullablePhysics);
}
}
var oldPuller = pullableComp.Puller;
pullableComp.PullJointId = null;
pullableComp.Puller = null;
Dirty(pullableUid, pullableComp);
// No more joints with puller -> force stop pull.
if (TryComp<PullerComponent>(oldPuller, out var pullerComp))
{
var pullerUid = oldPuller.Value;
_alertsSystem.ClearAlert(pullerUid, AlertType.Pulling);
pullerComp.Pulling = null;
Dirty(oldPuller.Value, pullerComp);
// Messaging
var message = new PullStoppedMessage(pullerUid, pullableUid);
_modifierSystem.RefreshMovementSpeedModifiers(pullerUid);
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(pullerUid):user} stopped pulling {ToPrettyString(pullableUid):target}");
RaiseLocalEvent(pullerUid, message);
RaiseLocalEvent(pullableUid, message);
}
_alertsSystem.ClearAlert(pullableUid, AlertType.Pulled);
}
public bool IsPulled(EntityUid uid, PullableComponent? component = null)
{
return Resolve(uid, ref component, false) && component.BeingPulled;
}
private bool OnRequestMovePulledObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
if (session?.AttachedEntity is not { } player ||
!player.IsValid())
{
return false;
}
if (!TryComp<PullerComponent>(player, out var pullerComp))
return false;
var pulled = pullerComp.Pulling;
if (!HasComp<PullableComponent>(pulled))
return false;
if (_containerSystem.IsEntityInContainer(player))
return false;
// Cooldown buddy
if (_timing.CurTime < pullerComp.NextThrow)
return false;
pullerComp.NextThrow = _timing.CurTime + pullerComp.ThrowCooldown;
// Cap the distance
const float range = 2f;
var fromUserCoords = coords.WithEntityId(player, EntityManager);
var userCoords = new EntityCoordinates(player, Vector2.Zero);
if (!userCoords.InRange(EntityManager, _xformSys, fromUserCoords, range))
{
var userDirection = fromUserCoords.Position - userCoords.Position;
fromUserCoords = userCoords.Offset(userDirection.Normalized() * range);
}
Dirty(player, pullerComp);
_throwing.TryThrow(pulled.Value, fromUserCoords, user: player, strength: 4f, animated: false, recoil: false, playSound: false);
return false;
}
public bool IsPulling(EntityUid puller, PullerComponent? component = null)
{
return Resolve(puller, ref component, false) && component.Pulling != null;
}
private void OnReleasePulledObject(ICommonSession? session)
{
if (session?.AttachedEntity is not {Valid: true} player)
{
return;
}
if (!TryComp(player, out PullerComponent? pullerComp) ||
!TryComp(pullerComp.Pulling, out PullableComponent? pullableComp))
{
return;
}
TryStopPull(pullerComp.Pulling.Value, pullableComp, user: player);
}
public bool CanPull(EntityUid puller, EntityUid pullableUid, PullerComponent? pullerComp = null)
{
if (!Resolve(puller, ref pullerComp, false))
{
return false;
}
if (pullerComp.NeedsHands && !_handsSystem.TryGetEmptyHand(puller, out _))
{
return false;
}
if (!_blocker.CanInteract(puller, pullableUid))
{
return false;
}
if (!EntityManager.TryGetComponent<PhysicsComponent>(pullableUid, out var physics))
{
return false;
}
if (physics.BodyType == BodyType.Static)
{
return false;
}
if (puller == pullableUid)
{
return false;
}
if (!_containerSystem.IsInSameOrNoContainer(puller, pullableUid))
{
return false;
}
if (EntityManager.TryGetComponent(puller, out BuckleComponent? buckle))
{
// Prevent people pulling the chair they're on, etc.
if (buckle is { PullStrap: false, Buckled: true } && (buckle.LastEntityBuckledTo == pullableUid))
{
return false;
}
}
var getPulled = new BeingPulledAttemptEvent(puller, pullableUid);
RaiseLocalEvent(pullableUid, getPulled, true);
var startPull = new StartPullAttemptEvent(puller, pullableUid);
RaiseLocalEvent(puller, startPull, true);
return !startPull.Cancelled && !getPulled.Cancelled;
}
public bool TogglePull(EntityUid pullableUid, EntityUid pullerUid, PullableComponent pullable)
{
if (pullable.Puller == pullerUid)
{
return TryStopPull(pullableUid, pullable);
}
return TryStartPull(pullerUid, pullableUid, pullableComp: pullable);
}
public bool TogglePull(EntityUid pullerUid, PullerComponent puller)
{
if (!TryComp<PullableComponent>(puller.Pulling, out var pullable))
return false;
return TogglePull(puller.Pulling.Value, pullerUid, pullable);
}
public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid, EntityUid? user = null,
PullerComponent? pullerComp = null, PullableComponent? pullableComp = null)
{
if (!Resolve(pullerUid, ref pullerComp, false) ||
!Resolve(pullableUid, ref pullableComp, false))
{
return false;
}
if (pullerComp.Pulling == pullableUid)
return true;
if (!CanPull(pullerUid, pullableUid))
return false;
if (!EntityManager.TryGetComponent<PhysicsComponent>(pullerUid, out var pullerPhysics) ||
!EntityManager.TryGetComponent<PhysicsComponent>(pullableUid, out var pullablePhysics))
{
return false;
}
// Ensure that the puller is not currently pulling anything.
var oldPullable = pullerComp.Pulling;
if (oldPullable != null)
{
// Well couldn't stop the old one.
if (!TryStopPull(oldPullable.Value, pullableComp, user))
return false;
}
// Is the pullable currently being pulled by something else?
if (pullableComp.Puller != null)
{
// Uhhh
if (pullableComp.Puller == pullerUid)
return false;
if (!TryStopPull(pullableUid, pullableComp, pullerUid))
return false;
}
var pullAttempt = new PullAttemptEvent(pullerUid, pullableUid);
RaiseLocalEvent(pullerUid, pullAttempt);
if (pullAttempt.Cancelled)
return false;
RaiseLocalEvent(pullableUid, pullAttempt);
if (pullAttempt.Cancelled)
return false;
// Pulling confirmed
_interaction.DoContactInteraction(pullableUid, pullerUid);
// Use net entity so it's consistent across client and server.
pullableComp.PullJointId = $"pull-joint-{GetNetEntity(pullableUid)}";
pullerComp.Pulling = pullableUid;
pullableComp.Puller = pullerUid;
// joint state handling will manage its own state
if (!_timing.ApplyingState)
{
// Joint startup
var union = _physics.GetHardAABB(pullerUid).Union(_physics.GetHardAABB(pullableUid, body: pullablePhysics));
var length = Math.Max((float) union.Size.X, (float) union.Size.Y) * 0.75f;
var joint = _joints.CreateDistanceJoint(pullableUid, pullerUid, id: pullableComp.PullJointId);
joint.CollideConnected = false;
// This maximum has to be there because if the object is constrained too closely, the clamping goes backwards and asserts.
joint.MaxLength = Math.Max(1.0f, length);
joint.Length = length * 0.75f;
joint.MinLength = 0f;
joint.Stiffness = 1f;
_physics.SetFixedRotation(pullableUid, pullableComp.FixedRotationOnPull, body: pullablePhysics);
}
pullableComp.PrevFixedRotation = pullablePhysics.FixedRotation;
// Messaging
var message = new PullStartedMessage(pullerUid, pullableUid);
_alertsSystem.ShowAlert(pullerUid, AlertType.Pulling);
_alertsSystem.ShowAlert(pullableUid, AlertType.Pulled);
RaiseLocalEvent(pullerUid, message);
RaiseLocalEvent(pullableUid, message);
Dirty(pullerUid, pullerComp);
Dirty(pullableUid, pullableComp);
_adminLogger.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(pullerUid):user} started pulling {ToPrettyString(pullableUid):target}");
return true;
}
public bool TryStopPull(EntityUid pullableUid, PullableComponent pullable, EntityUid? user = null)
{
var pullerUidNull = pullable.Puller;
if (pullerUidNull == null)
return false;
var msg = new AttemptStopPullingEvent(user);
RaiseLocalEvent(pullableUid, msg, true);
if (msg.Cancelled)
return false;
// Stop pulling confirmed!
if (!_timing.ApplyingState)
{
// Joint shutdown
if (pullable.PullJointId != null)
{
_joints.RemoveJoint(pullableUid, pullable.PullJointId);
pullable.PullJointId = null;
}
}
StopPulling(pullableUid, pullable);
return true;
}
}