Pulling rework (#20906)

* Pulling rework

Fixing up the FOUR systems managing pulling, all the shitcode, and also making it nicer ingame.

* More pulling cleanup

* stats

* More cleanup

* First draft

* More pulling

* weh

* Fix puller

* Pulling working

* Fix merge

* Dunked

* Self-merge time
This commit is contained in:
metalgearsloth
2024-02-03 14:36:09 +11:00
committed by GitHub
parent 79e3a6630d
commit 0d8254b2a2
53 changed files with 764 additions and 1359 deletions

View File

@@ -1,28 +0,0 @@
using Content.Shared.ActionBlocker;
using Content.Shared.Mobs.Systems;
using Content.Shared.Pulling.Components;
using Content.Shared.Movement.Events;
namespace Content.Shared.Pulling.Systems
{
public sealed class SharedPullableSystem : EntitySystem
{
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedPullingSystem _pullSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SharedPullableComponent, MoveInputEvent>(OnRelayMoveInput);
}
private void OnRelayMoveInput(EntityUid uid, SharedPullableComponent component, ref MoveInputEvent args)
{
var entity = args.Entity;
if (_mobState.IsIncapacitated(entity) || !_blocker.CanMove(entity)) return;
_pullSystem.TryStopPull(component);
}
}
}

View File

@@ -1,90 +0,0 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Alert;
using Content.Shared.Database;
using Content.Shared.Hands;
using Content.Shared.Movement.Systems;
using Content.Shared.Physics.Pull;
using Content.Shared.Pulling.Components;
using JetBrains.Annotations;
namespace Content.Shared.Pulling.Systems
{
[UsedImplicitly]
public sealed class SharedPullerSystem : EntitySystem
{
[Dependency] private readonly SharedPullingStateManagementSystem _why = default!;
[Dependency] private readonly SharedPullingSystem _pullSystem = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifierSystem = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SharedPullerComponent, PullStartedMessage>(PullerHandlePullStarted);
SubscribeLocalEvent<SharedPullerComponent, PullStoppedMessage>(PullerHandlePullStopped);
SubscribeLocalEvent<SharedPullerComponent, VirtualItemDeletedEvent>(OnVirtualItemDeleted);
SubscribeLocalEvent<SharedPullerComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
SubscribeLocalEvent<SharedPullerComponent, ComponentShutdown>(OnPullerShutdown);
}
private void OnPullerShutdown(EntityUid uid, SharedPullerComponent component, ComponentShutdown args)
{
_why.ForceDisconnectPuller(component);
}
private void OnVirtualItemDeleted(EntityUid uid, SharedPullerComponent component, VirtualItemDeletedEvent args)
{
if (component.Pulling == null)
return;
if (component.Pulling == args.BlockingEntity)
{
if (EntityManager.TryGetComponent<SharedPullableComponent>(args.BlockingEntity, out var comp))
{
_pullSystem.TryStopPull(comp, uid);
}
}
}
private void PullerHandlePullStarted(
EntityUid uid,
SharedPullerComponent component,
PullStartedMessage args)
{
if (args.Puller.Owner != uid)
return;
_alertsSystem.ShowAlert(component.Owner, AlertType.Pulling);
RefreshMovementSpeed(component);
}
private void PullerHandlePullStopped(
EntityUid uid,
SharedPullerComponent component,
PullStoppedMessage args)
{
if (args.Puller.Owner != uid)
return;
var euid = component.Owner;
if (_alertsSystem.IsShowingAlert(euid, AlertType.Pulling))
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(euid):user} stopped pulling {ToPrettyString(args.Pulled.Owner):target}");
_alertsSystem.ClearAlert(euid, AlertType.Pulling);
RefreshMovementSpeed(component);
}
private void OnRefreshMovespeed(EntityUid uid, SharedPullerComponent component, RefreshMovementSpeedModifiersEvent args)
{
args.ModifySpeed(component.WalkSpeedModifier, component.SprintSpeedModifier);
}
private void RefreshMovementSpeed(SharedPullerComponent component)
{
_movementSpeedModifierSystem.RefreshMovementSpeedModifiers(component.Owner);
}
}
}

View File

@@ -1,196 +0,0 @@
using Content.Shared.Physics.Pull;
using Content.Shared.Pulling.Components;
using JetBrains.Annotations;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Timing;
namespace Content.Shared.Pulling
{
/// <summary>
/// This is the core of pulling state management.
/// Because pulling state is such a mess to get right, all writes to pulling state must go through this class.
/// </summary>
[UsedImplicitly]
public sealed class SharedPullingStateManagementSystem : EntitySystem
{
[Dependency] private readonly SharedJointSystem _jointSystem = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SharedPullableComponent, ComponentShutdown>(OnShutdown);
}
private void OnShutdown(EntityUid uid, SharedPullableComponent component, ComponentShutdown args)
{
if (component.Puller != null)
ForceRelationship(null, component);
}
// A WARNING:
// The following 2 functions are the most internal part of the pulling system's relationship management.
// They do not expect to be cancellable.
private void ForceDisconnect(SharedPullerComponent puller, SharedPullableComponent pullable)
{
var pullerPhysics = EntityManager.GetComponent<PhysicsComponent>(puller.Owner);
var pullablePhysics = EntityManager.GetComponent<PhysicsComponent>(pullable.Owner);
// MovingTo shutdown
ForceSetMovingTo(pullable, null);
// Joint shutdown
if (!_timing.ApplyingState && // During state-handling, joint component will handle its own state.
pullable.PullJointId != null &&
TryComp(puller.Owner, out JointComponent? jointComp))
{
if (jointComp.GetJoints.TryGetValue(pullable.PullJointId, out var j))
_jointSystem.RemoveJoint(j);
}
pullable.PullJointId = null;
// State shutdown
puller.Pulling = null;
pullable.Puller = null;
// Messaging
var message = new PullStoppedMessage(pullerPhysics, pullablePhysics);
RaiseLocalEvent(puller.Owner, message, broadcast: false);
if (Initialized(pullable.Owner))
RaiseLocalEvent(pullable.Owner, message, true);
// Networking
Dirty(puller);
Dirty(pullable);
}
public void ForceRelationship(SharedPullerComponent? puller, SharedPullableComponent? pullable)
{
if (_timing.ApplyingState)
return;
;
if (pullable != null && puller != null && (puller.Pulling == pullable.Owner))
{
// Already done
return;
}
// Start by disconnecting the pullable from whatever it is currently connected to.
var pullableOldPullerE = pullable?.Puller;
if (pullableOldPullerE != null)
{
ForceDisconnect(EntityManager.GetComponent<SharedPullerComponent>(pullableOldPullerE.Value), pullable!);
}
// Continue with the puller.
var pullerOldPullableE = puller?.Pulling;
if (pullerOldPullableE != null)
{
ForceDisconnect(puller!, EntityManager.GetComponent<SharedPullableComponent>(pullerOldPullableE.Value));
}
// And now for the actual connection (if any).
if (puller != null && pullable != null)
{
var pullerPhysics = EntityManager.GetComponent<PhysicsComponent>(puller.Owner);
var pullablePhysics = EntityManager.GetComponent<PhysicsComponent>(pullable.Owner);
pullable.PullJointId = $"pull-joint-{pullable.Owner}";
// State startup
puller.Pulling = pullable.Owner;
pullable.Puller = puller.Owner;
// joint state handling will manage its own state
if (!_timing.ApplyingState)
{
// Joint startup
var union = _physics.GetHardAABB(puller.Owner).Union(_physics.GetHardAABB(pullable.Owner, body: pullablePhysics));
var length = Math.Max(union.Size.X, union.Size.Y) * 0.75f;
var joint = _jointSystem.CreateDistanceJoint(pullablePhysics.Owner, pullerPhysics.Owner, id: pullable.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;
}
// Messaging
var message = new PullStartedMessage(pullerPhysics, pullablePhysics);
RaiseLocalEvent(puller.Owner, message, broadcast: false);
RaiseLocalEvent(pullable.Owner, message, true);
// Networking
Dirty(puller);
Dirty(pullable);
}
}
// For OnRemove use only.
public void ForceDisconnectPuller(SharedPullerComponent puller)
{
// DO NOT ADD ADDITIONAL LOGIC IN THIS FUNCTION. Do it in ForceRelationship.
ForceRelationship(puller, null);
}
// For OnRemove use only.
public void ForceDisconnectPullable(SharedPullableComponent pullable)
{
// DO NOT ADD ADDITIONAL LOGIC IN THIS FUNCTION. Do it in ForceRelationship.
ForceRelationship(null, pullable);
}
public void ForceSetMovingTo(SharedPullableComponent pullable, EntityCoordinates? movingTo)
{
if (_timing.ApplyingState)
return;
if (pullable.MovingTo == movingTo)
{
return;
}
// Don't allow setting a MovingTo if there's no puller.
// The other half of this guarantee (shutting down a MovingTo if the puller goes away) is enforced in ForceRelationship.
if (pullable.Puller == null && movingTo != null)
{
return;
}
pullable.MovingTo = movingTo;
Dirty(pullable);
if (movingTo == null)
{
RaiseLocalEvent(pullable.Owner, new PullableStopMovingMessage(), true);
}
else
{
RaiseLocalEvent(pullable.Owner, new PullableMoveMessage(), true);
}
}
/// <summary>
/// Changes if the entity needs a hand in order to be able to pull objects.
/// </summary>
public void ChangeHandRequirement(EntityUid uid, bool needsHands, SharedPullerComponent? comp)
{
if (!Resolve(uid, ref comp, false))
return;
comp.NeedsHands = needsHands;
Dirty(uid, comp);
}
}
}

View File

@@ -1,239 +0,0 @@
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
using Content.Shared.Buckle.Components;
using Content.Shared.Database;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Physics.Pull;
using Content.Shared.Pulling.Components;
using Content.Shared.Pulling.Events;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Shared.Pulling
{
public abstract partial class SharedPullingSystem
{
[Dependency] private readonly ActionBlockerSystem _blocker = 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 ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public bool CanPull(EntityUid puller, EntityUid pulled)
{
if (!EntityManager.TryGetComponent<SharedPullerComponent>(puller, out var comp))
{
return false;
}
if (comp.NeedsHands && !_handsSystem.TryGetEmptyHand(puller, out _))
{
return false;
}
if (!_blocker.CanInteract(puller, pulled))
{
return false;
}
if (!EntityManager.TryGetComponent<PhysicsComponent>(pulled, out var physics))
{
return false;
}
if (physics.BodyType == BodyType.Static)
{
return false;
}
if (puller == pulled)
{
return false;
}
if(_containerSystem.IsEntityInContainer(puller) || _containerSystem.IsEntityInContainer(pulled))
{
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 == pulled))
{
return false;
}
}
var getPulled = new BeingPulledAttemptEvent(puller, pulled);
RaiseLocalEvent(pulled, getPulled, true);
var startPull = new StartPullAttemptEvent(puller, pulled);
RaiseLocalEvent(puller, startPull, true);
return (!startPull.Cancelled && !getPulled.Cancelled);
}
public bool TogglePull(EntityUid puller, SharedPullableComponent pullable)
{
if (pullable.Puller == puller)
{
return TryStopPull(pullable);
}
return TryStartPull(puller, pullable.Owner);
}
// -- Core attempted actions --
public bool TryStopPull(SharedPullableComponent pullable, EntityUid? user = null)
{
if (_timing.ApplyingState)
return false;
if (!pullable.BeingPulled)
{
return false;
}
var msg = new StopPullingEvent(user);
RaiseLocalEvent(pullable.Owner, msg, true);
if (msg.Cancelled) return false;
// Stop pulling confirmed!
if (TryComp<PhysicsComponent>(pullable.Owner, out var pullablePhysics))
{
_physics.SetFixedRotation(pullable.Owner, pullable.PrevFixedRotation, body: pullablePhysics);
}
_pullSm.ForceRelationship(null, pullable);
return true;
}
public bool TryStartPull(EntityUid puller, EntityUid pullable)
{
if (!EntityManager.TryGetComponent(puller, out SharedPullerComponent? pullerComp))
{
return false;
}
if (!EntityManager.TryGetComponent(pullable, out SharedPullableComponent? pullableComp))
{
return false;
}
return TryStartPull(pullerComp, pullableComp);
}
// The main "start pulling" function.
public bool TryStartPull(SharedPullerComponent puller, SharedPullableComponent pullable)
{
if (_timing.ApplyingState)
return false;
if (puller.Pulling == pullable.Owner)
return true;
// Pulling a new object : Perform sanity checks.
if (!CanPull(puller.Owner, pullable.Owner))
{
return false;
}
if (!EntityManager.TryGetComponent<PhysicsComponent>(puller.Owner, out var pullerPhysics))
{
return false;
}
if (!EntityManager.TryGetComponent<PhysicsComponent>(pullable.Owner, out var pullablePhysics))
{
return false;
}
// Ensure that the puller is not currently pulling anything.
// If this isn't done, then it happens too late, and the start/stop messages go out of order,
// and next thing you know it thinks it's not pulling anything even though it is!
var oldPullable = puller.Pulling;
if (oldPullable != null)
{
if (EntityManager.TryGetComponent(oldPullable.Value, out SharedPullableComponent? oldPullableComp))
{
if (!TryStopPull(oldPullableComp))
{
return false;
}
}
else
{
Log.Warning("Well now you've done it, haven't you? Someone transferred pulling (onto {0}) while presently pulling something that has no Pullable component (on {1})!", pullable.Owner, oldPullable);
return false;
}
}
// Ensure that the pullable is not currently being pulled.
// Same sort of reasons as before.
var oldPuller = pullable.Puller;
if (oldPuller != null)
{
if (!TryStopPull(pullable))
{
return false;
}
}
// Continue with pulling process.
var pullAttempt = new PullAttemptEvent(pullerPhysics, pullablePhysics);
RaiseLocalEvent(puller.Owner, pullAttempt, broadcast: false);
if (pullAttempt.Cancelled)
{
return false;
}
RaiseLocalEvent(pullable.Owner, pullAttempt, true);
if (pullAttempt.Cancelled)
return false;
_interaction.DoContactInteraction(pullable.Owner, puller.Owner);
_pullSm.ForceRelationship(puller, pullable);
pullable.PrevFixedRotation = pullablePhysics.FixedRotation;
_physics.SetFixedRotation(pullable.Owner, pullable.FixedRotationOnPull, body: pullablePhysics);
_adminLogger.Add(LogType.Action, LogImpact.Low,
$"{ToPrettyString(puller.Owner):user} started pulling {ToPrettyString(pullable.Owner):target}");
return true;
}
public bool TryMoveTo(SharedPullableComponent pullable, EntityCoordinates to)
{
if (pullable.Puller == null)
{
return false;
}
if (!EntityManager.HasComponent<PhysicsComponent>(pullable.Owner))
{
return false;
}
_pullSm.ForceSetMovingTo(pullable, to);
return true;
}
public void StopMoveTo(SharedPullableComponent pullable)
{
_pullSm.ForceSetMovingTo(pullable, null);
}
}
}

View File

@@ -1,243 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Alert;
using Content.Shared.GameTicking;
using Content.Shared.Input;
using Content.Shared.Physics.Pull;
using Content.Shared.Pulling.Components;
using Content.Shared.Verbs;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.Input.Binding;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
namespace Content.Shared.Pulling
{
[UsedImplicitly]
public abstract partial class SharedPullingSystem : EntitySystem
{
[Dependency] private readonly SharedPullingStateManagementSystem _pullSm = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly SharedJointSystem _joints = default!;
/// <summary>
/// A mapping of pullers to the entity that they are pulling.
/// </summary>
private readonly Dictionary<EntityUid, EntityUid> _pullers =
new();
private readonly HashSet<SharedPullableComponent> _moving = new();
private readonly HashSet<SharedPullableComponent> _stoppedMoving = new();
public IReadOnlySet<SharedPullableComponent> Moving => _moving;
public override void Initialize()
{
base.Initialize();
UpdatesOutsidePrediction = true;
SubscribeLocalEvent<RoundRestartCleanupEvent>(Reset);
SubscribeLocalEvent<PullStartedMessage>(OnPullStarted);
SubscribeLocalEvent<PullStoppedMessage>(OnPullStopped);
SubscribeLocalEvent<EntInsertedIntoContainerMessage>(HandleContainerInsert);
SubscribeLocalEvent<SharedPullableComponent, JointRemovedEvent>(OnJointRemoved);
SubscribeLocalEvent<SharedPullableComponent, CollisionChangeEvent>(OnPullableCollisionChange);
SubscribeLocalEvent<SharedPullableComponent, PullStartedMessage>(PullableHandlePullStarted);
SubscribeLocalEvent<SharedPullableComponent, PullStoppedMessage>(PullableHandlePullStopped);
SubscribeLocalEvent<SharedPullableComponent, GetVerbsEvent<Verb>>(AddPullVerbs);
CommandBinds.Builder
.Bind(ContentKeyFunctions.MovePulledObject, new PointerInputCmdHandler(HandleMovePulledObject))
.Register<SharedPullingSystem>();
}
private void OnPullableCollisionChange(EntityUid uid, SharedPullableComponent component, ref CollisionChangeEvent args)
{
if (component.PullJointId != null && !args.CanCollide)
{
_joints.RemoveJoint(uid, component.PullJointId);
}
}
private void OnJointRemoved(EntityUid uid, SharedPullableComponent component, JointRemovedEvent args)
{
if (component.Puller != args.OtherEntity)
return;
// Do we have some other join with our Puller?
// or alternatively:
// TODO track the relevant joint.
if (TryComp(uid, out JointComponent? joints))
{
foreach (var jt in joints.GetJoints.Values)
{
if (jt.BodyAUid == component.Puller || jt.BodyBUid == component.Puller)
return;
}
}
// No more joints with puller -> force stop pull.
_pullSm.ForceDisconnectPullable(component);
}
private void AddPullVerbs(EntityUid uid, SharedPullableComponent 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(component, 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);
}
}
// Raise a "you are being pulled" alert if the pulled entity has alerts.
private void PullableHandlePullStarted(EntityUid uid, SharedPullableComponent component, PullStartedMessage args)
{
if (args.Pulled.Owner != uid)
return;
_alertsSystem.ShowAlert(uid, AlertType.Pulled);
}
private void PullableHandlePullStopped(EntityUid uid, SharedPullableComponent component, PullStoppedMessage args)
{
if (args.Pulled.Owner != uid)
return;
_alertsSystem.ClearAlert(uid, AlertType.Pulled);
}
public bool IsPulled(EntityUid uid, SharedPullableComponent? component = null)
{
return Resolve(uid, ref component, false) && component.BeingPulled;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
_moving.ExceptWith(_stoppedMoving);
_stoppedMoving.Clear();
}
public void Reset(RoundRestartCleanupEvent ev)
{
_pullers.Clear();
_moving.Clear();
_stoppedMoving.Clear();
}
private void OnPullStarted(PullStartedMessage message)
{
SetPuller(message.Puller.Owner, message.Pulled.Owner);
}
private void OnPullStopped(PullStoppedMessage message)
{
RemovePuller(message.Puller.Owner);
}
protected void OnPullableMove(EntityUid uid, SharedPullableComponent component, PullableMoveMessage args)
{
_moving.Add(component);
}
protected void OnPullableStopMove(EntityUid uid, SharedPullableComponent component, PullableStopMovingMessage args)
{
_stoppedMoving.Add(component);
}
// TODO: When Joint networking is less shitcodey fix this to use a dedicated joints message.
private void HandleContainerInsert(EntInsertedIntoContainerMessage message)
{
if (TryComp(message.Entity, out SharedPullableComponent? pullable))
{
TryStopPull(pullable);
}
if (TryComp(message.Entity, out SharedPullerComponent? puller))
{
if (puller.Pulling == null) return;
if (!TryComp(puller.Pulling.Value, out SharedPullableComponent? pulling))
return;
TryStopPull(pulling);
}
}
private bool HandleMovePulledObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
if (session?.AttachedEntity is not { } player ||
!player.IsValid())
return false;
if (!TryGetPulled(player, out var pulled))
return false;
if (!TryComp(pulled.Value, out SharedPullableComponent? pullable))
return false;
if (_containerSystem.IsEntityInContainer(player))
return false;
TryMoveTo(pullable, coords);
return false;
}
private void SetPuller(EntityUid puller, EntityUid pulled)
{
_pullers[puller] = pulled;
}
private bool RemovePuller(EntityUid puller)
{
return _pullers.Remove(puller);
}
public EntityUid GetPulled(EntityUid by)
{
return _pullers.GetValueOrDefault(by);
}
public bool TryGetPulled(EntityUid by, [NotNullWhen(true)] out EntityUid? pulled)
{
return (pulled = GetPulled(by)) != null;
}
public bool IsPulling(EntityUid puller)
{
return _pullers.ContainsKey(puller);
}
}
}