Files
wwdpublic/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
Mnemotechnican a7a9cf9e84 Physical Pulling (#883)
# Description
Fixes pulling being obnoxiously unphysical. Entities now exert a
specific amount of force defined as the product of their mass and the
SpecificForce set in their PullerComponent. Doing a ctrl-right click now
sets a target position where your entity begins to continuously push the
pulled entity towards, instead of throwing it or pushing it once
(however, it should still be possible to drag people and disposals and
the like). Additionally, your entity will properly experience recoil
from dragging someone around.

# TODO
- [X] Do stuff
- [X] Fix stuff
- [X] Fix aghosts not being able to push (?)
- [X] Fix pulling giving you free kinetic energy while in spess (how?)
- [ ] Test if throwing felinids into disposals via pulling still works
:trollface:

<details><summary><h1>Media</h1></summary>
<p>



https://github.com/user-attachments/assets/2498ef8c-f743-4702-b73c-b2da0c16e9aa


</p>
</details>

# Changelog
🆑
- add: Pulling has been reworked. Your character will now try to
continuously push the pulled entity when you use the push keybind.
- remove: You can no longer push the pulled entity while walking, and
pushing now follows the momentum conservation laws.
2024-10-19 12:54:31 +07:00

581 lines
22 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.Components;
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.Projectiles;
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.Network;
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 INetManager _net = 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 ThrownItemSystem _thrownItem = 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<PullableComponent, StartCollideEvent>(OnPullableCollide);
SubscribeLocalEvent<PullerComponent, MoveInputEvent>(OnPullerMoveInput);
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>();
}
public override void Shutdown()
{
base.Shutdown();
CommandBinds.Unregister<PullingSystem>();
}
public override void Update(float frameTime)
{
if (_net.IsClient) // Client cannot predict this
return;
var query = EntityQueryEnumerator<PullerComponent, PhysicsComponent, TransformComponent>();
while (query.MoveNext(out var puller, out var pullerComp, out var pullerPhysics, out var pullerXForm))
{
// If not pulling, reset the pushing cooldowns and exit
if (pullerComp.Pulling is not { } pulled || !TryComp<PullableComponent>(pulled, out var pulledComp))
{
pullerComp.PushingTowards = null;
pullerComp.NextPushTargetChange = TimeSpan.Zero;
continue;
}
pulledComp.BeingActivelyPushed = false; // Temporarily set to false; if the checks below pass, it will be set to true again
// If pulling but the pullee is invalid or is on a different map, stop pulling
var pulledXForm = Transform(pulled);
if (!TryComp<PhysicsComponent>(pulled, out var pulledPhysics)
|| pulledPhysics.BodyType == BodyType.Static
|| pulledXForm.MapUid != pullerXForm.MapUid)
{
StopPulling(pulled, pulledComp);
continue;
}
if (pullerComp.PushingTowards is null)
continue;
// If pushing but the target position is invalid, or the push action has expired or finished, stop pushing
if (pullerComp.NextPushStop < _timing.CurTime
|| !(pullerComp.PushingTowards.Value.ToMap(EntityManager, _xformSys) is var pushCoordinates)
|| pushCoordinates.MapId != pulledXForm.MapID)
{
pullerComp.PushingTowards = null;
pullerComp.NextPushTargetChange = TimeSpan.Zero;
continue;
}
// Actual force calculation. All the Vector2's below are in map coordinates.
var desiredDeltaPos = pushCoordinates.Position - Transform(pulled).Coordinates.ToMapPos(EntityManager, _xformSys);
if (desiredDeltaPos.LengthSquared() < 0.1f)
{
pullerComp.PushingTowards = null;
continue;
}
var velocityAndDirectionAngle = new Angle(pulledPhysics.LinearVelocity) - new Angle(desiredDeltaPos);
var currentRelativeSpeed = pulledPhysics.LinearVelocity.Length() * (float) Math.Cos(velocityAndDirectionAngle.Theta);
var desiredAcceleration = MathF.Max(0f, pullerComp.MaxPushSpeed - currentRelativeSpeed);
var desiredImpulse = pulledPhysics.Mass * desiredDeltaPos;
var maxSourceImpulse = MathF.Min(pullerComp.PushAcceleration, desiredAcceleration) * pullerPhysics.Mass;
var actualImpulse = desiredImpulse.LengthSquared() > maxSourceImpulse * maxSourceImpulse ? desiredDeltaPos.Normalized() * maxSourceImpulse : desiredImpulse;
// Ideally we'd want to apply forces instead of impulses, however...
// We cannot use ApplyForce here because it will be cleared on the next physics substep which will render it ultimately useless
// The alternative is to run this function on every physics substep, but that is way too expensive for such a minor system
_physics.ApplyLinearImpulse(pulled, actualImpulse);
_physics.ApplyLinearImpulse(puller, -actualImpulse);
pulledComp.BeingActivelyPushed = true;
}
query.Dispose();
}
private void OnPullerMoveInput(EntityUid uid, PullerComponent component, ref MoveInputEvent args)
{
// Stop pushing
component.PushingTowards = null;
component.NextPushStop = TimeSpan.Zero;
}
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);
}
private void OnPullableCollide(Entity<PullableComponent> ent, ref StartCollideEvent args)
{
if (!ent.Comp.BeingActivelyPushed || args.OtherEntity == ent.Comp.Puller)
return;
// This component isn't actually needed anywhere besides the thrownitemsyste`m itself, so we just fake it
var fakeThrown = new ThrownItemComponent()
{
Owner = ent.Owner,
Animate = false,
Landed = false,
PlayLandSound = false,
Thrower = ent.Comp.Puller
};
_thrownItem.ThrowCollideInteraction(fakeThrown, ent, args.OtherEntity);
}
private void OnPullerUnpaused(EntityUid uid, PullerComponent component, ref EntityUnpausedEvent args)
{
component.NextPushTargetChange += 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()
|| !TryComp<PullerComponent>(player, out var pullerComp))
return false;
var pulled = pullerComp.Pulling;
if (!HasComp<PullableComponent>(pulled)
|| _containerSystem.IsEntityInContainer(player)
|| _timing.CurTime < pullerComp.NextPushTargetChange)
return false;
pullerComp.NextPushTargetChange = _timing.CurTime + pullerComp.PushChangeCooldown;
pullerComp.NextPushStop = _timing.CurTime + pullerComp.PushDuration;
// Cap the distance
var range = pullerComp.MaxPushRange;
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);
}
pullerComp.PushingTowards = fromUserCoords;
Dirty(player, pullerComp);
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;
}
}