Fix pulling when already pulling The TryStopPull were failing due to wrong arguments provided. Replacing the virtual item in hand with a different pull was failing due to the hand not being cleared. Fix stop pulling checks that had the wrong variables provided. VirtualItems are already queue deleted at the end of HandleEntityRemoved.
492 lines
17 KiB
C#
492 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, doSpin: 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 _)
|
|
&& pullerComp.Pulling == null)
|
|
{
|
|
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,
|
|
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.
|
|
if (TryComp<PullableComponent>(pullerComp.Pulling, out var oldPullable)
|
|
&& !TryStopPull(pullerComp.Pulling.Value, oldPullable, pullerUid))
|
|
return false;
|
|
|
|
// Stop anyone else pulling the entity we want to pull
|
|
if (pullableComp.Puller != null)
|
|
{
|
|
// We're already pulling this item
|
|
if (pullableComp.Puller == pullerUid)
|
|
return false;
|
|
|
|
if (!TryStopPull(pullableUid, pullableComp, pullableComp.Puller))
|
|
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 true;
|
|
|
|
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;
|
|
}
|
|
}
|