diff --git a/Content.Client/Pulling/PullingSystem.cs b/Content.Client/Pulling/PullingSystem.cs index 04b2195a46..556dadd00d 100644 --- a/Content.Client/Pulling/PullingSystem.cs +++ b/Content.Client/Pulling/PullingSystem.cs @@ -13,6 +13,9 @@ namespace Content.Client.Pulling base.Initialize(); UpdatesAfter.Add(typeof(PhysicsSystem)); + + SubscribeLocalEvent(OnPullableMove); + SubscribeLocalEvent(OnPullableStopMove); } } } diff --git a/Content.Server/Physics/Controllers/PullController.cs b/Content.Server/Physics/Controllers/PullController.cs new file mode 100644 index 0000000000..fc93d4cf46 --- /dev/null +++ b/Content.Server/Physics/Controllers/PullController.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using Content.Shared.Pulling; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Maths; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Controllers; + +namespace Content.Server.Physics.Controllers +{ + public sealed class PullController : VirtualController + { + // Parameterization for pulling: + // Speeds. Note that the speed is mass-independent (multiplied by mass). + // Instead, tuning to mass is done via the mass values below. + // Note that setting the speed too high results in overshoots (stabilized by drag, but bad) + private const float AccelModifierHigh = 15f; + private const float AccelModifierLow = 60.0f; + // High/low-mass marks. Curve is constant-lerp-constant, i.e. if you can even pull an item, + // you'll always get at least AccelModifierLow and no more than AccelModifierHigh. + private const float AccelModifierHighMass = 70.0f; // roundstart saltern emergency closet + private const float AccelModifierLowMass = 5.0f; // roundstart saltern emergency crowbar + // Used to control settling (turns off pulling). + private const float MaximumSettleVelocity = 0.1f; + private const float MaximumSettleDistance = 0.01f; + // Settle shutdown control. + // Mustn't be too massive, as that causes severe mispredicts *and can prevent it ever resolving*. + // Exists to bleed off "I pulled my crowbar" overshoots. + // Minimum velocity for shutdown to be necessary. This prevents stuff getting stuck b/c too much shutdown. + private const float SettleMinimumShutdownVelocity = 0.25f; + // Distance in which settle shutdown multiplier is at 0. It then scales upwards linearly with closer distances. + private const float SettleShutdownDistance = 1.0f; + // Velocity change of -LinearVelocity * frameTime * this + private const float SettleShutdownMultiplier = 20.0f; + + [Dependency] private readonly SharedPullingSystem _pullableSystem = default!; + + public override void Initialize() + { + UpdatesAfter.Add(typeof(MoverController)); + + base.Initialize(); + } + + public override void UpdateBeforeSolve(bool prediction, float frameTime) + { + base.UpdateBeforeSolve(prediction, frameTime); + + foreach (var pullable in _pullableSystem.Moving) + { + // There's a 1-frame delay between stopping moving something and it leaving the Moving set. + // This can include if leaving the Moving set due to not being pulled anymore, + // or due to being deleted. + + if (pullable.Deleted) + { + continue; + } + + if (pullable.MovingTo == null) + { + continue; + } + + if (pullable.Puller is not {Valid: true} puller) + { + continue; + } + + // Now that's over with... + + var pullerPosition = EntityManager.GetComponent(puller).MapPosition; + var movingTo = pullable.MovingTo.Value.ToMap(EntityManager); + if (movingTo.MapId != pullerPosition.MapId) + { + _pullableSystem.StopMoveTo(pullable); + continue; + } + + if (!EntityManager.TryGetComponent(pullable.Owner, out var physics) || + physics.BodyType == BodyType.Static || + movingTo.MapId != EntityManager.GetComponent(pullable.Owner).MapID) + { + _pullableSystem.StopMoveTo(pullable); + continue; + } + + var movingPosition = movingTo.Position; + var ownerPosition = EntityManager.GetComponent(pullable.Owner).MapPosition.Position; + + var diff = movingPosition - ownerPosition; + var diffLength = diff.Length; + + if ((diffLength < MaximumSettleDistance) && (physics.LinearVelocity.Length < MaximumSettleVelocity)) + { + physics.LinearVelocity = Vector2.Zero; + _pullableSystem.StopMoveTo(pullable); + continue; + } + + var impulseModifierLerp = Math.Min(1.0f, Math.Max(0.0f, (physics.Mass - AccelModifierLowMass) / (AccelModifierHighMass - AccelModifierLowMass))); + var impulseModifier = MathHelper.Lerp(AccelModifierLow, AccelModifierHigh, impulseModifierLerp); + var multiplier = diffLength < 1 ? impulseModifier * diffLength : impulseModifier; + // Note the implication that the real rules of physics don't apply to pulling control. + var accel = diff.Normalized * multiplier; + // Now for the part where velocity gets shutdown... + if ((diffLength < SettleShutdownDistance) && (physics.LinearVelocity.Length >= SettleMinimumShutdownVelocity)) + { + // Shutdown velocity increases as we get closer to centre + var scaling = (SettleShutdownDistance - diffLength) / SettleShutdownDistance; + accel -= physics.LinearVelocity * SettleShutdownMultiplier * scaling; + } + physics.WakeBody(); + var impulse = accel * physics.Mass * frameTime; + physics.ApplyLinearImpulse(impulse); + } + } + } +} diff --git a/Content.Server/Pulling/PullingSystem.cs b/Content.Server/Pulling/PullingSystem.cs index 3aae7dfd57..f7ea0aae57 100644 --- a/Content.Server/Pulling/PullingSystem.cs +++ b/Content.Server/Pulling/PullingSystem.cs @@ -1,11 +1,9 @@ -using Content.Server.Throwing; using Content.Shared.Input; using Content.Shared.Pulling; using Content.Shared.Pulling.Components; using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Shared.Input.Binding; -using Robust.Shared.Map; using Robust.Shared.Players; namespace Content.Server.Pulling @@ -19,6 +17,9 @@ namespace Content.Server.Pulling UpdatesAfter.Add(typeof(PhysicsSystem)); + SubscribeLocalEvent(OnPullableMove); + SubscribeLocalEvent(OnPullableStopMove); + CommandBinds.Builder .Bind(ContentKeyFunctions.ReleasePulledObject, InputCmdHandler.FromDelegate(HandleReleasePulledObject)) .Register(); @@ -43,32 +44,5 @@ namespace Content.Server.Pulling TryStopPull(pullable); } - - public override bool TryMoveTo(SharedPullableComponent pullable, EntityCoordinates to) - { - if (!base.TryMoveTo(pullable, to)) return false; - - var xformQuery = GetEntityQuery(); - var pullableXform = xformQuery.GetComponent(pullable.Owner); - var targetPos = to.ToMap(EntityManager); - - if (targetPos.MapId != pullableXform.MapID) return false; - - var pullablePos = pullableXform.WorldPosition; - - if (pullablePos.EqualsApprox(targetPos.Position, 0.01f)) return false; - - var vec = (targetPos.Position - pullablePos); - - // Just move it to the spot for finetuning - if (vec.Length < 0.15f) - { - pullableXform.WorldPosition = targetPos.Position; - return true; - } - - pullable.Owner.TryThrow(vec, 3f); - return true; - } } } diff --git a/Content.Shared/Pulling/Components/SharedPullerComponent.cs b/Content.Shared/Pulling/Components/SharedPullerComponent.cs index d77a6cdc32..4e3976168b 100644 --- a/Content.Shared/Pulling/Components/SharedPullerComponent.cs +++ b/Content.Shared/Pulling/Components/SharedPullerComponent.cs @@ -28,7 +28,6 @@ namespace Content.Shared.Pulling.Components if (Pulling != default) { // This is absolute paranoia but it's also absolutely necessary. Too many puller state bugs. - 20kdc - // Good thing Pulling is nullable now. Logger.ErrorS("c.go.c.pulling", "PULLING STATE CORRUPTION IMMINENT IN PULLER {0} - OnRemove called when Pulling is set!", Owner); } base.OnRemove(); diff --git a/Content.Shared/Pulling/Systems/SharedPullingStateManagementSystem.cs b/Content.Shared/Pulling/Systems/SharedPullingStateManagementSystem.cs index 579dc5f4e0..95f9d7a8cb 100644 --- a/Content.Shared/Pulling/Systems/SharedPullingStateManagementSystem.cs +++ b/Content.Shared/Pulling/Systems/SharedPullingStateManagementSystem.cs @@ -1,8 +1,24 @@ +using System; using System.Linq; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Content.Shared.GameTicking; +using Content.Shared.Input; using Content.Shared.Physics.Pull; using Content.Shared.Pulling.Components; +using Content.Shared.Pulling.Events; using JetBrains.Annotations; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.Input.Binding; +using Robust.Shared.IoC; +using Robust.Shared.Log; +using Robust.Shared.Map; +using Robust.Shared.Maths; using Robust.Shared.Physics; +using Robust.Shared.Physics.Dynamics.Joints; +using Robust.Shared.Players; +using Robust.Shared.Utility; namespace Content.Shared.Pulling { @@ -37,6 +53,9 @@ namespace Content.Shared.Pulling var pullerPhysics = EntityManager.GetComponent(puller.Owner); var pullablePhysics = EntityManager.GetComponent(pullable.Owner); + // MovingTo shutdown + ForceSetMovingTo(pullable, null); + // Joint shutdown if (EntityManager.TryGetComponent(puller.Owner, out var jointComp)) { @@ -134,5 +153,31 @@ namespace Content.Shared.Pulling // DO NOT ADD ADDITIONAL LOGIC IN THIS FUNCTION. Do it in ForceRelationship. ForceRelationship(null, pullable); } + + public void ForceSetMovingTo(SharedPullableComponent pullable, EntityCoordinates? movingTo) + { + 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; + + if (movingTo == null) + { + RaiseLocalEvent(pullable.Owner, new PullableStopMovingMessage()); + } + else + { + RaiseLocalEvent(pullable.Owner, new PullableMoveMessage()); + } + } } } diff --git a/Content.Shared/Pulling/Systems/SharedPullingSystem.Actions.cs b/Content.Shared/Pulling/Systems/SharedPullingSystem.Actions.cs index dc9dfca3a2..9406f58fca 100644 --- a/Content.Shared/Pulling/Systems/SharedPullingSystem.Actions.cs +++ b/Content.Shared/Pulling/Systems/SharedPullingSystem.Actions.cs @@ -4,8 +4,11 @@ using Content.Shared.Physics.Pull; using Content.Shared.Pulling.Components; using Content.Shared.Pulling.Events; using Robust.Shared.Containers; +using Robust.Shared.GameObjects; using Robust.Shared.Map; +using Robust.Shared.IoC; using Robust.Shared.Physics; +using Robust.Shared.Log; namespace Content.Shared.Pulling { @@ -26,12 +29,12 @@ namespace Content.Shared.Pulling return false; } - if (!EntityManager.TryGetComponent(pulled, out var physics)) + if (!EntityManager.TryGetComponent(pulled, out var _physics)) { return false; } - if (physics.BodyType == BodyType.Static) + if (_physics.BodyType == BodyType.Static) { return false; } @@ -178,7 +181,7 @@ namespace Content.Shared.Pulling return true; } - public virtual bool TryMoveTo(SharedPullableComponent pullable, EntityCoordinates to) + public bool TryMoveTo(SharedPullableComponent pullable, EntityCoordinates to) { if (pullable.Puller == null) { @@ -190,8 +193,13 @@ namespace Content.Shared.Pulling return false; } - // Action handled under server + _pullSm.ForceSetMovingTo(pullable, to); return true; } + + public void StopMoveTo(SharedPullableComponent pullable) + { + _pullSm.ForceSetMovingTo(pullable, null); + } } } diff --git a/Content.Shared/Pulling/Systems/SharedPullingSystem.cs b/Content.Shared/Pulling/Systems/SharedPullingSystem.cs index 632459649f..a36aec2a14 100644 --- a/Content.Shared/Pulling/Systems/SharedPullingSystem.cs +++ b/Content.Shared/Pulling/Systems/SharedPullingSystem.cs @@ -4,7 +4,6 @@ using System.Diagnostics.CodeAnalysis; using Content.Shared.Alert; using Content.Shared.GameTicking; using Content.Shared.Input; -using Content.Shared.Movement.Components; using Content.Shared.Physics.Pull; using Content.Shared.Pulling.Components; using Content.Shared.Rotatable; @@ -34,6 +33,9 @@ namespace Content.Shared.Pulling private readonly Dictionary _pullers = new(); + private readonly HashSet _moving = new(); + private readonly HashSet _stoppedMoving = new(); + /// /// If distance between puller and pulled entity lower that this threshold, /// pulled entity will not change its rotation. @@ -49,6 +51,8 @@ namespace Content.Shared.Pulling /// private const float ThresholdRotAngle = 22.5f; + public IReadOnlySet Moving => _moving; + public override void Initialize() { base.Initialize(); @@ -102,7 +106,7 @@ namespace Content.Shared.Pulling { if (args.Pulled.Owner != uid) return; - + _alertsSystem.ShowAlert(component.Owner, AlertType.Pulled); } @@ -114,9 +118,19 @@ namespace Content.Shared.Pulling _alertsSystem.ClearAlert(component.Owner, AlertType.Pulled); } + 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) @@ -129,6 +143,16 @@ namespace Content.Shared.Pulling 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); + } + private void PullerMoved(ref MoveEvent ev) { var puller = ev.Sender; @@ -193,9 +217,6 @@ namespace Content.Shared.Pulling return false; } - // No leverage until this is tweaked more - if (player.IsWeightless(entityManager: EntityManager)) return false; - TryMoveTo(pullable, coords); return false;